Merge verified IVA monthly projection and Modelo A interface into homologation
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -156,7 +156,7 @@ export function SimulationCashflowPlanner({
|
||||
</label>
|
||||
{taxMode === 'fixed' ? (
|
||||
<label className="block max-w-xs text-sm font-medium">
|
||||
Imposto assumido por mês
|
||||
Imposto manual do cenário personalizado por mês
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -175,7 +175,7 @@ export function SimulationCashflowPlanner({
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{inputs.monthlyTaxSchedule.map((value, index) => (
|
||||
<label key={index} className="text-sm font-medium">
|
||||
Imposto assumido — mês {index + 1}
|
||||
Imposto manual do cenário personalizado — mês {index + 1}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fiscalComponentCodes } from '@/lib/simulation-contract';
|
||||
import { ivaIssueText, type IvaDiagnostic, type IvaIssue } from '@/lib/iva-diagnostics';
|
||||
|
||||
type Parameter = {
|
||||
parameter_code: string;
|
||||
@@ -56,6 +57,8 @@ export function SimulationFiscalStatus({
|
||||
periodStart,
|
||||
periodEnd,
|
||||
context,
|
||||
onDiagnostics,
|
||||
onCorrect,
|
||||
}: {
|
||||
salaryView?: string;
|
||||
scenarioMode?: string;
|
||||
@@ -63,8 +66,11 @@ export function SimulationFiscalStatus({
|
||||
periodStart?: string;
|
||||
periodEnd?: string;
|
||||
context?: Record<string, string>;
|
||||
onDiagnostics?: (issues: IvaIssue[]) => void;
|
||||
onCorrect?: (field: string) => void;
|
||||
}) {
|
||||
const [parameters, setParameters] = useState<Parameter[]>([]);
|
||||
const [diagnostics, setDiagnostics] = useState<IvaDiagnostic[]>([]);
|
||||
const [requestState, setRequestState] = useState('idle');
|
||||
const [retry, setRetry] = useState(0);
|
||||
|
||||
@@ -79,10 +85,14 @@ export function SimulationFiscalStatus({
|
||||
!periodEnd
|
||||
) {
|
||||
setParameters([]);
|
||||
setDiagnostics([]);
|
||||
onDiagnostics?.([]);
|
||||
setRequestState('idle');
|
||||
return;
|
||||
}
|
||||
const abort = new AbortController();
|
||||
setDiagnostics([]);
|
||||
onDiagnostics?.([]);
|
||||
setRequestState('loading');
|
||||
void fetch('/api/v1/tax-parameters/resolve', {
|
||||
method: 'POST',
|
||||
@@ -104,11 +114,15 @@ export function SimulationFiscalStatus({
|
||||
if (!response.ok) throw new Error('lookup_failed');
|
||||
const payload = (await response.json()) as {
|
||||
parameters?: Parameter[];
|
||||
diagnostics?: IvaDiagnostic[];
|
||||
};
|
||||
if (abort.signal.aborted) return;
|
||||
setParameters(
|
||||
Array.isArray(payload.parameters) ? payload.parameters : [],
|
||||
);
|
||||
const details = Array.isArray(payload.diagnostics) ? payload.diagnostics : [];
|
||||
setDiagnostics(details);
|
||||
onDiagnostics?.(details.flatMap((detail) => detail.issues));
|
||||
setRequestState('done');
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
@@ -158,7 +172,7 @@ export function SimulationFiscalStatus({
|
||||
{requestState === 'loading'
|
||||
? 'A consultar parâmetros aprovados para o período e âmbito indicados…'
|
||||
: requestState === 'error'
|
||||
? 'Não foi possível consultar os parâmetros; o resultado permanece indeterminado.'
|
||||
? 'Falha na consulta do sistema: não foi possível obter os parâmetros. O problema não é atribuído às respostas; estas foram preservadas. Tente novamente.'
|
||||
: !periodStart || !periodEnd
|
||||
? 'Indique um período exacto. Para periodicidade mensal ou trimestral, seleccione o mês ou trimestre.'
|
||||
: `Período consultado: ${periodStart} a ${periodEnd}.`}
|
||||
@@ -186,6 +200,22 @@ export function SimulationFiscalStatus({
|
||||
{salaryViewMessages[salaryView]}
|
||||
</p>
|
||||
)}
|
||||
{diagnostics.length > 0 && <div role="alert" className="space-y-3 rounded border border-amber-700 p-3">
|
||||
<h4 className="font-semibold">IVA — taxa geral: condições não satisfeitas</h4>
|
||||
<p>Não foi possível resolver este parâmetro. As respostas foram preservadas; esta consulta não executa o cálculo do imposto.</p>
|
||||
{diagnostics.map((detail, index) => <div key={index}>
|
||||
<ul className="list-disc pl-5">{detail.issues.map((issue, issueIndex) => <li key={issueIndex}>
|
||||
{ivaIssueText(issue)}{' '}
|
||||
{issue.field && <a className="underline" href={`#field-${issue.field}`} onClick={(event) => { if (onCorrect) { event.preventDefault(); onCorrect(issue.field!); } }}>Rever {issue.label.toLowerCase()}</a>}
|
||||
{issue.category === 'system_configuration' && issue.field === 'fiscal_operation' && <a className="ml-2 underline" href="#published-iva">Abrir IVA — taxa geral</a>}
|
||||
</li>)}</ul>
|
||||
<details className="mt-2"><summary className="cursor-pointer underline">Ver fundamento</summary>
|
||||
<p>IVA — taxa geral · versão {detail.rule_version}. Vigência: {detail.validity.start} a {detail.validity.end ?? 'sem data final indicada'}.</p>
|
||||
<p>{detail.source.title} — {detail.source.diploma}, {detail.source.article}</p>
|
||||
{detail.source.url?.startsWith('https://') && <a className="underline" href={detail.source.url} target="_blank" rel="noreferrer">Consultar fonte</a>}
|
||||
</details>
|
||||
</div>)}
|
||||
</div>}
|
||||
<ul className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{fiscalComponentCodes.map((code) => {
|
||||
const selected =
|
||||
@@ -211,13 +241,13 @@ export function SimulationFiscalStatus({
|
||||
</p>
|
||||
) : parameters.length === 0 ? (
|
||||
<p className="text-sm">
|
||||
Indeterminado — nenhum parâmetro publicado
|
||||
{code === 'IVA' ? 'Indeterminado — não existe uma versão elegível visível nesta consulta. Consulte o diagnóstico do período acima, quando disponível. Sem esse diagnóstico, não é possível distinguir ausência de cobertura de indisponibilidade da regra ou da fonte; não se atribui o problema às suas respostas.' : <>Indeterminado — nenhum parâmetro publicado
|
||||
aplicável. Faltam parâmetros aprovados para{' '}
|
||||
{code}, no período {periodStart} a{' '}
|
||||
{periodEnd} e no âmbito indicado. A consulta
|
||||
não identifica um conjunto obrigatório de
|
||||
parâmetros; não é possível determinar o
|
||||
imposto nem substituir as lacunas por zero.
|
||||
imposto nem substituir as lacunas por zero.</>}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2 text-sm">
|
||||
@@ -227,7 +257,7 @@ export function SimulationFiscalStatus({
|
||||
className="border-t pt-2"
|
||||
>
|
||||
<strong>
|
||||
{parameter.parameter_code}
|
||||
{parameter.parameter_code === 'IVA_GENERAL_RATE' ? 'IVA — taxa geral' : parameter.parameter_code}
|
||||
</strong>
|
||||
{parameter.status === 'resolved' ? (
|
||||
<>
|
||||
@@ -238,10 +268,10 @@ export function SimulationFiscalStatus({
|
||||
</p>
|
||||
<p>
|
||||
Regra{' '}
|
||||
{parameter.rule_code} v
|
||||
{parameter.parameter_code === 'IVA_GENERAL_RATE' ? 'IVA — taxa geral' : parameter.rule_code} v
|
||||
{parameter.rule_version}
|
||||
; âmbito{' '}
|
||||
{parameter.scope_key}
|
||||
{parameter.parameter_code === 'IVA_GENERAL_RATE' ? 'operação interna geral sobre base líquida em MZN' : parameter.scope_key}
|
||||
</p>
|
||||
<p>
|
||||
Vigência:{' '}
|
||||
@@ -279,13 +309,13 @@ export function SimulationFiscalStatus({
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Consultar fundamento
|
||||
Ver fundamento
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
{labels[parameter.status] ??
|
||||
{(parameter.parameter_code === 'IVA_GENERAL_RATE' && diagnostics.length ? 'Consulte acima cada condição e o fundamento da versão.' : labels[parameter.status]) ??
|
||||
'Indeterminado — não foi possível validar o parâmetro.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SimulationPublishedIva, type PublishedIvaInput } from './simulation-published-iva';
|
||||
import { SimulationMonthlyIva } from './simulation-monthly-iva';
|
||||
import {
|
||||
PublicDocumentContent,
|
||||
type PublicDocument,
|
||||
@@ -56,6 +57,7 @@ import { SimulationFinancialTools } from '@/components/simulation-financial-tool
|
||||
import { SimulationCashflowPlanner } from '@/components/simulation-cashflow-planner';
|
||||
import { SimulationBusinessPlan } from '@/components/simulation-business-plan';
|
||||
import { SimulationFiscalStatus } from '@/components/simulation-fiscal-status';
|
||||
import { ivaIssueText, type IvaIssue } from '@/lib/iva-diagnostics';
|
||||
import {
|
||||
ProjectMilestonePlanner,
|
||||
ProjectTeamPlanner,
|
||||
@@ -810,6 +812,7 @@ export function SimulationForm() {
|
||||
});
|
||||
const [step, setStep] = useState(0);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [ivaIssues, setIvaIssues] = useState<IvaIssue[]>([]);
|
||||
const [serverFieldErrors, setServerFieldErrors] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
@@ -1936,6 +1939,7 @@ export function SimulationForm() {
|
||||
) {
|
||||
if (!ready) return;
|
||||
setContinueLaterSaved(false);
|
||||
setIvaIssues((previous) => previous.filter((issue) => issue.field !== key));
|
||||
setServerFieldErrors((previous) => {
|
||||
const next = { ...previous };
|
||||
delete next[key];
|
||||
@@ -2308,12 +2312,14 @@ export function SimulationForm() {
|
||||
);
|
||||
}
|
||||
if (!publishedInput) sessionStorage.removeItem(idempotencyStorageKey);
|
||||
return simulation ?? null;
|
||||
} catch (cause) {
|
||||
setServerError(
|
||||
cause instanceof Error
|
||||
? cause.message
|
||||
: 'Erro de ligação. Tente novamente.',
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
calculationInFlightRef.current = false;
|
||||
setIsCalculating(false);
|
||||
@@ -3285,6 +3291,7 @@ export function SimulationForm() {
|
||||
: Boolean(field.required);
|
||||
const fieldError =
|
||||
serverFieldErrors[field.key] ?? errors[field.key];
|
||||
const ivaNotes = ivaIssues.filter((issue) => issue.field === field.key).map((issue, index) => <p key={index} role="note" className="mt-1 text-sm text-amber-950">{ivaIssueText(issue)}</p>);
|
||||
if (field.options) {
|
||||
const parentKey =
|
||||
field.key === 'district_id'
|
||||
@@ -3331,6 +3338,7 @@ export function SimulationForm() {
|
||||
}
|
||||
/>
|
||||
<FieldGuidance fieldKey={field.key} />
|
||||
{ivaNotes}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3405,6 +3413,7 @@ export function SimulationForm() {
|
||||
{fieldError}
|
||||
</p>
|
||||
)}
|
||||
{ivaNotes}
|
||||
{field.key === 'reduction' && (
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Hipótese sua, não representa um
|
||||
@@ -3457,15 +3466,16 @@ export function SimulationForm() {
|
||||
{(current.id === 'financial' ||
|
||||
(draft.purpose === 'simulate_only' &&
|
||||
current.id === 'tax')) && (
|
||||
<SimulationCashflowPlanner
|
||||
<details className="rounded border p-3"><summary>Cenário personalizado de tesouraria — imposto manual, separado do IVA calculado</summary><SimulationCashflowPlanner
|
||||
snapshot={String(draft.cashflow_snapshot ?? '')}
|
||||
onSnapshotChange={(value) =>
|
||||
change('cashflow_snapshot', value)
|
||||
}
|
||||
currency={String(draft.currency ?? '')}
|
||||
onCurrencyChange={(value) => change('currency', value)}
|
||||
/>
|
||||
/></details>
|
||||
)}
|
||||
{(current.id === 'financial' || current.id === 'tax') && <SimulationMonthlyIva draft={draft} busy={isCalculating} onCalculate={runSimulation} onChange={change} />}
|
||||
{current.id === 'financial' &&
|
||||
(draft.detail_level === 'advanced' ||
|
||||
draft.journey_type === 'small_business') &&
|
||||
@@ -3609,6 +3619,12 @@ export function SimulationForm() {
|
||||
aria-label="Consulta de parâmetros fiscais"
|
||||
>
|
||||
<SimulationFiscalStatus
|
||||
onDiagnostics={setIvaIssues}
|
||||
onCorrect={(key) => {
|
||||
const target = journeySections.findIndex((section) => (simulationFieldGroups[section.id] ?? []).some((field) => field.key === key));
|
||||
if (target >= 0) setStep(target);
|
||||
window.requestAnimationFrame(() => document.getElementById(`field-${key}`)?.focus());
|
||||
}}
|
||||
salaryView={String(draft.irps_salary_view ?? '')}
|
||||
scenarioMode={
|
||||
openedTool === 'fiscal'
|
||||
@@ -3618,7 +3634,7 @@ export function SimulationForm() {
|
||||
component={String(draft.fiscal_component ?? '')}
|
||||
periodStart={getFiscalPeriodRange(draft)?.start}
|
||||
periodEnd={getFiscalPeriodRange(draft)?.end}
|
||||
context={Object.fromEntries(
|
||||
context={{ ...Object.fromEntries(
|
||||
[
|
||||
'fiscal_periodicity',
|
||||
'fiscal_operation',
|
||||
@@ -3635,7 +3651,7 @@ export function SimulationForm() {
|
||||
? [[key, draft[key] as string]]
|
||||
: [],
|
||||
),
|
||||
)}
|
||||
), ...(typeof draft.fiscal_currency === 'string' && draft.fiscal_currency !== '' ? {currency: draft.fiscal_currency} : {}) }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { modeloAFields, modeloAPurchaseReferences, modeloAReference } from '@/lib/iva-modelo-a';
|
||||
|
||||
export function SimulationModeloA({draft}: {draft:Record<string,unknown>}) {
|
||||
let purchases: ReturnType<typeof modeloAPurchaseReferences> = [];
|
||||
let error = '';
|
||||
try { purchases = modeloAPurchaseReferences(draft); }
|
||||
catch (cause) { error = cause instanceof Error ? cause.message : 'Não foi possível consultar as aquisições projectadas.'; }
|
||||
return <section aria-label="Correspondência com o Modelo A" className="space-y-3 rounded border p-3">
|
||||
<h4 className="font-semibold">Modelo A — correspondência da simulação</h4>
|
||||
<p>{modeloAReference.status}</p>
|
||||
<p>Os campos 14–18 do apuramento permanecem indeterminados. O IVA das vendas não é imposto final a pagar. Os dados deste percurso são projecções, separados de facturas e declarações efectivas.</p>
|
||||
<details><summary>Aquisições já projectadas — sem dedução presumida</summary>
|
||||
<p>Reutilizam-se os valores das compras/encomendas para existências do plano. O mês da encomenda não prova a data da factura, recepção ou pagamento. Valores declarados ainda sem indicação de base líquida/IVA incluído não são tratados como base tributável. IVA suportado e IVA dedutível continuam por determinar; zero de compras não prova inexistência de outras aquisições.</p>
|
||||
{error && <p role="alert">{error}</p>}
|
||||
{!purchases.length && !error && <p>Sem agenda de compras recuperável do plano empresarial.</p>}
|
||||
{!!purchases.length && <div className="overflow-x-auto"><table className="min-w-full text-sm"><caption>Pressupostos actuais de compras; sem resultado fiscal guardado</caption><thead><tr>{['Mês do plano','Compras declaradas','Sem IVA','IVA suportado','Com IVA','IVA dedutível'].map((name)=><th className="p-2 text-left" key={name}>{name}</th>)}</tr></thead><tbody>{purchases.map((row)=><tr key={row.month}><th className="p-2" scope="row">{row.month}</th><td>{row.declared ?? 'Sem informação'}</td><td>Por classificar</td><td>Indeterminado</td><td>Por classificar</td><td>Indeterminado</td></tr>)}</tbody></table></div>}
|
||||
</details>
|
||||
<details><summary>Ver campos, cálculos, fundamentos e cobertura do Modelo A</summary>
|
||||
<p>{modeloAReference.file}. {modeloAReference.edition} SHA256: {modeloAReference.sha256}.</p>
|
||||
<p>O exemplar contém divergências de numeração/remissões, registadas no confronto documental. Não foram corrigidas no original. A existência de um campo não activa uma regra fiscal.</p>
|
||||
<div className="overflow-x-auto"><table className="min-w-full text-sm"><caption>Campo do Modelo A → finalidade → origem → cálculo → fundamento → cobertura</caption><thead><tr>{['Campo','Finalidade','Origem dos dados','Cálculo','Fundamento','Estado'].map((name)=><th className="p-2 text-left" key={name}>{name}</th>)}</tr></thead><tbody>{modeloAFields.map((field)=><tr key={field.field}>{[field.field,field.purpose,field.origin,field.calculation,field.basis,field.status].map((value,index)=><td className="border-t p-2 align-top" key={index}>{value}</td>)}</tr>)}</tbody></table></div>
|
||||
</details>
|
||||
</section>;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { monthlyIvaInput, monthlyIvaKey, monthlySalesFromDraft, readMonthlyIvaRecord, readMonthlyIvaSettings, monthlyIvaCashTreatment, type MonthlyIvaInput, type MonthlyIvaSettings } from '@/lib/monthly-iva';
|
||||
import { fiscalQaRows } from '@/lib/fiscal-qa-preview';
|
||||
import { isApiSimulationReadResponseV1, type ApiSimulationExecutionV1 } from '@/lib/simulation-contract';
|
||||
import { SimulationModeloA } from './simulation-modelo-a';
|
||||
|
||||
export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
|
||||
draft: Record<string, unknown>; busy: boolean;
|
||||
onCalculate: (input: MonthlyIvaInput) => Promise<ApiSimulationExecutionV1 | null | undefined>;
|
||||
onChange: (field: string, value: string) => void;
|
||||
}) {
|
||||
const settings = readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? ''));
|
||||
const record = readMonthlyIvaRecord(String(draft.monthly_iva_snapshot ?? ''));
|
||||
const [retry, setRetry] = useState(0);
|
||||
const [error, setError] = useState('');
|
||||
const [available, setAvailable] = useState<boolean | null>(null);
|
||||
const attempt = useRef('');
|
||||
const callbacks = useRef({onCalculate, onChange}); callbacks.current = {onCalculate, onChange};
|
||||
let input: MonthlyIvaInput | null = null; let inputError = ''; let source = '';
|
||||
try { source = monthlySalesFromDraft(draft).source; input = monthlyIvaInput(draft, settings); }
|
||||
catch (cause) { inputError = cause instanceof Error ? cause.message : 'Reveja a projecção de vendas.'; }
|
||||
const key = input ? monthlyIvaKey(input) : '';
|
||||
const current = !!(input && record && monthlyIvaKey(record.input) === key);
|
||||
const completed = current && record?.execution.status === 'completed';
|
||||
const saveSettings = (patch: Partial<MonthlyIvaSettings>) => onChange('monthly_iva_settings', JSON.stringify({...settings, ...patch}));
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
fetch('/api/v1/fiscal-consumer', {credentials: 'same-origin', headers: {Accept:'application/json'}}).then(async (response) => {
|
||||
if (!response.ok) throw new Error(); const payload = await response.json(); if (active) setAvailable(payload?.data?.enabled === true);
|
||||
}).catch(() => {if (active) setError('Não foi possível consultar a disponibilidade da regra. As vendas foram preservadas.');});
|
||||
return () => { active = false; };
|
||||
}, [retry]);
|
||||
useEffect(() => {
|
||||
if (!settings.auto || !input || busy || !available || completed || attempt.current === key) return;
|
||||
const request = input;
|
||||
const pending = current && record && ['queued','running'].includes(record.execution.status) ? record.execution.id : null;
|
||||
const timer = window.setTimeout(() => {
|
||||
attempt.current = key; setError('');
|
||||
void (async () => {
|
||||
try {
|
||||
let execution;
|
||||
if (pending) {
|
||||
const response = await fetch(`/api/v1/simulations/${pending}`, {credentials:'same-origin', headers:{Accept:'application/json'}});
|
||||
const value: unknown = await response.json();
|
||||
if (!response.ok || !isApiSimulationReadResponseV1(value)) throw new Error('Não foi possível recuperar a execução mensal guardada. Não foi criada uma execução alternativa.');
|
||||
execution = value.data;
|
||||
} else execution = await callbacks.current.onCalculate(request);
|
||||
if (!execution) throw new Error('O cálculo mensal não foi concluído. As vendas e a versão anterior foram preservadas; tente novamente.');
|
||||
callbacks.current.onChange('monthly_iva_snapshot', JSON.stringify({input:request, execution}));
|
||||
} catch (cause) {setError(cause instanceof Error ? cause.message : 'Não foi possível calcular o IVA mensal.');}
|
||||
})();
|
||||
}, 700);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [key, settings.auto, busy, available, completed, retry]);
|
||||
|
||||
const snapshot = record?.execution.result_snapshot;
|
||||
const projection = snapshot?.monthly_projection as {rows?: Array<{date:string; sales_declared:string|null; net_sales:string|null; rate:string|null; vat:string|null; gross_sales:string|null; status:string}>} | undefined;
|
||||
return <section className="space-y-3 rounded border border-emerald-700 p-4" aria-label="IVA mensal das vendas projectadas">
|
||||
<h3 className="font-semibold">IVA mensal das vendas projectadas</h3>
|
||||
<p>{source}</p><p>Declare se os preços projectados são sem IVA ou com IVA incluído. Nenhuma taxa é fixada neste formulário; o servidor selecciona a versão publicada.</p>
|
||||
<label className="block">Primeiro mês da projecção<input type="month" value={settings.start} min="2026-01" max="2026-12" onChange={(event) => saveSettings({start:event.target.value})} className="block rounded border p-2" /></label>
|
||||
<label className="block">Tratamento do IVA nos preços projectados<select value={settings.basis} onChange={(event) => saveSettings({basis:event.target.value as MonthlyIvaSettings['basis']})} className="block rounded border p-2"><option value="unknown">Por indicar</option><option value="net">Vendas sem IVA (base líquida)</option><option value="included">Vendas com IVA incluído</option></select></label>
|
||||
<label className="block"><input type="checkbox" checked={settings.declared} onChange={(event) => saveSettings({declared:event.target.checked})} /> Declaro que as vendas projectadas são tributáveis à taxa geral interna e não incluem as operações excluídas deste lote.</label>
|
||||
<p className="text-sm">Sem importações, taxas reduzidas, isenções, regimes especiais, deduções, reembolsos ou apuramento final. Valores com IVA incluído permanecem indeterminados neste lote; não se aplica a taxa ao preço bruto.</p>
|
||||
<p className="text-sm">{monthlyIvaCashTreatment}</p>
|
||||
{inputError && <p role="alert">{inputError}</p>}
|
||||
{available === null && !error && <p role="status">A consultar a disponibilidade da versão publicada…</p>}
|
||||
{available === false && <p role="status">A versão publicada de IVA está indisponível neste ambiente para novas execuções. Os pressupostos e históricos mantêm-se.</p>}
|
||||
{record && !current && <p role="alert">As vendas ou os pressupostos mudaram. Os resultados abaixo são históricos e estão desactualizados; não são usados como resultados da projecção actual.</p>}
|
||||
{settings.auto && !completed && current && <p role="status">Execução mensal guardada: {record?.execution.status === 'queued' ? 'em fila' : record?.execution.status === 'running' ? 'em processamento' : 'não concluída'}. Pode voltar a consultar sem duplicar.</p>}
|
||||
{busy && <p role="status">A actualizar os resultados dependentes das vendas…</p>}
|
||||
{error && <p role="alert">{error}</p>}
|
||||
<button type="button" disabled={busy || !available || !input || input.currency !== 'MZN'} onClick={() => {attempt.current=''; setRetry((value) => value+1); saveSettings({auto:true});}} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50">Calcular / actualizar IVA mensal e guardar versão</button>
|
||||
{settings.auto && <button type="button" className="ml-3 underline" onClick={() => saveSettings({auto:false})}>Pausar actualização automática</button>}
|
||||
{input && <p className="text-sm">{input.monthly_sales.length} meses; moeda {input.currency || 'por indicar'}. Campo vazio significa informação em falta; zero declarado permanece zero. Depois da primeira execução, alterações das vendas actualizam o cálculo enquanto a actualização automática estiver ligada.</p>}
|
||||
{projection?.rows && <div className="overflow-x-auto" tabIndex={0}><table className="min-w-full text-sm"><caption>IVA mensal — {current ? 'versão correspondente às entradas' : 'versão histórica'}</caption><thead><tr>{['Mês','Vendas declaradas','Vendas tributáveis sem IVA','Taxa aplicada (%)','IVA calculado','Total com IVA','Estado'].map((label) => <th key={label} className="p-2 text-left">{label}</th>)}</tr></thead><tbody>{projection.rows.map((row) => <tr key={row.date}><th scope="row" className="p-2">{row.date.slice(0,7)}</th><td>{row.sales_declared ?? 'Sem informação'}</td><td>{row.net_sales ?? 'Indeterminado'}</td><td>{row.rate ?? 'Indeterminada'}</td><td>{row.vat ?? 'Indeterminado'}</td><td>{row.gross_sales ?? 'Indeterminado'}</td><td>{{calculated:'Calculado',missing_sales:'Sem informação de vendas',unsupported_basis:'Base líquida não determinada',indeterminate:'Enquadramento ou regra indeterminados'}[row.status] ?? 'Indeterminado'}</td></tr>)}</tbody></table></div>}
|
||||
{snapshot && <details><summary>Ver fundamento e relatório da versão guardada</summary><dl>{fiscalQaRows(snapshot).map(([label,value], index) => <div key={index}><dt className="font-semibold">{label}</dt><dd>{value}</dd></div>)}</dl></details>}
|
||||
<SimulationModeloA draft={draft} />
|
||||
</section>;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { MonthlyIvaInput } from '@/lib/monthly-iva';
|
||||
|
||||
export type PublishedIvaInput = {
|
||||
export type PublishedIvaInput = MonthlyIvaInput | {
|
||||
scenario_kind: 'fiscal_rule_published';
|
||||
base: string;
|
||||
currency: 'MZN';
|
||||
@@ -11,7 +12,7 @@ export type PublishedIvaInput = {
|
||||
|
||||
export function SimulationPublishedIva({ busy, onCalculate }: {
|
||||
busy: boolean;
|
||||
onCalculate: (input: PublishedIvaInput) => Promise<void>;
|
||||
onCalculate: (input: PublishedIvaInput) => Promise<unknown>;
|
||||
}) {
|
||||
const [available, setAvailable] = useState(false);
|
||||
const [message, setMessage] = useState('A consultar disponibilidade do cálculo de IVA…');
|
||||
@@ -51,8 +52,8 @@ export function SimulationPublishedIva({ busy, onCalculate }: {
|
||||
} catch { /* The execution flow already preserves and reports connection errors. */ }
|
||||
}
|
||||
|
||||
return <section aria-label="IVA geral sobre base líquida" className="space-y-3 rounded border border-emerald-700 p-4">
|
||||
<h4 className="font-semibold">IVA geral sobre base líquida</h4>
|
||||
return <section id="published-iva" tabIndex={-1} aria-label="IVA — taxa geral" className="space-y-3 rounded border border-emerald-700 p-4">
|
||||
<h4 className="font-semibold">IVA — taxa geral</h4>
|
||||
<p role="status">{message}</p>
|
||||
<p className="text-sm">Cobertura: exigibilidade em 2026, operação interna à taxa geral. A base tributável é indicada por si; não é copiada do orçamento. Sem isenções, taxas reduzidas, bases especiais, importações, deduções, declaração ou pagamento. O cálculo não valida o enquadramento jurídico.</p>
|
||||
<fieldset disabled={busy} className="space-y-3">
|
||||
|
||||
@@ -776,6 +776,37 @@ function multiplyQuantity(
|
||||
return (quantityMilli * unitPriceCents + 500n) / 1000n;
|
||||
}
|
||||
|
||||
/** Reuses the sales model, without requiring unrelated cost/cash assumptions. Blank is never zero. */
|
||||
export function monthlyProjectedSales(input: BusinessFinancialPlanInput): Array<string | null> {
|
||||
const periods = input.periods ?? 12;
|
||||
if (!Number.isInteger(periods) || periods < 1 || periods > 60) throw new Error('Horizonte de vendas inválido.');
|
||||
return Array.from({ length: periods }, (_, month) => {
|
||||
if (!input.products.length) return null;
|
||||
let total = 0n;
|
||||
for (const product of input.products) {
|
||||
const units = product.monthlyUnitsByPeriod === undefined ? product.monthlyUnits : product.monthlyUnitsByPeriod[month];
|
||||
const price = product.unitPriceByPeriod === undefined ? product.unitPrice : product.unitPriceByPeriod[month];
|
||||
if (typeof units !== 'string' || typeof price !== 'string' || units.trim() === '' || price.trim() === '') return null;
|
||||
total += multiplyQuantity(quantity(units), cents(price, `Preço de ${product.label}`));
|
||||
}
|
||||
return format(total);
|
||||
});
|
||||
}
|
||||
|
||||
/** Existing purchase orders are planning references, never a proxy for deductible VAT or invoice dates. */
|
||||
export function monthlyProjectedPurchaseOrders(input: BusinessFinancialPlanInput): Array<string | null> {
|
||||
const periods = input.periods ?? 12;
|
||||
if (!Number.isInteger(periods) || periods < 1 || periods > 60) throw new Error('Horizonte de compras inválido.');
|
||||
return Array.from({length: periods}, (_, month) => {
|
||||
const amounts = input.useProductInventorySchedule
|
||||
? input.products.map((product) => product.productInventoryPurchasesByPeriod?.[month]?.amount)
|
||||
: [input.useInventoryPurchaseSchedule
|
||||
? input.inventoryPurchaseSchedule?.[month]?.amount : input.monthlyInventoryPurchases];
|
||||
if (!amounts.length || amounts.some((amount) => typeof amount !== 'string' || amount.trim() === '')) return null;
|
||||
return format(amounts.reduce((total, amount) => total + cents(amount!, 'Compras projectadas'), 0n));
|
||||
});
|
||||
}
|
||||
|
||||
function format(centsValue: bigint): string {
|
||||
const absolute = centsValue < 0n ? -centsValue : centsValue;
|
||||
return `${centsValue < 0n ? '-' : ''}${absolute / 100n}.${String(absolute % 100n).padStart(2, '0')}`;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isApiSimulationResultSnapshotV1 } from "./simulation-contract.ts";
|
||||
import { modeloAComponentRows } from './iva-modelo-a.ts';
|
||||
|
||||
export function fiscalQaRows(value: unknown): Array<[string, string]> {
|
||||
if (!isApiSimulationResultSnapshotV1(value)) return [];
|
||||
@@ -19,7 +20,14 @@ export function fiscalQaRows(value: unknown): Array<[string, string]> {
|
||||
typeof item === "string" || typeof item === "number"
|
||||
? String(item)
|
||||
: "não disponível";
|
||||
const monthly = snapshot.monthly_projection as {rows?: Array<Record<string,unknown>>; cash_treatment?: string} | undefined;
|
||||
return [
|
||||
...modeloAComponentRows(snapshot),
|
||||
...(monthly?.rows ? [
|
||||
["Projecção mensal", "Vendas tributáveis sem IVA → taxa aplicada → IVA calculado → total com IVA; não é apuramento final"],
|
||||
...monthly.rows.map((row) => [`Mês ${text(row.date)}`, `Vendas declaradas: ${text(row.sales_declared)}; base líquida: ${text(row.net_sales)}; taxa: ${row.rate == null ? 'não aplicada' : `${text(row.rate)}%`}; IVA: ${text(row.vat)}; total com IVA: ${text(row.gross_sales)}; ${row.status === 'calculated' ? 'calculado' : row.status === 'missing_sales' ? 'sem informação de vendas' : row.status === 'unsupported_basis' ? 'base líquida não determinada' : 'enquadramento ou regra indeterminados'}`]),
|
||||
["Tratamento na tesouraria", monthly.cash_treatment ?? "IVA separado; não é custo nem imposto a pagar"],
|
||||
] as Array<[string,string]> : []),
|
||||
["Relatório", published ? "IVA pela versão publicada; ver enquadramento abaixo" : "Pré-visualização fiscal QA de rascunho — sem activação"],
|
||||
[
|
||||
"IVA calculado (exacto; não é saldo a pagar)",
|
||||
@@ -29,7 +37,7 @@ export function fiscalQaRows(value: unknown): Array<[string, string]> {
|
||||
["Base tributável declarada", text(snapshot.base)],
|
||||
["Taxa cadastrada (%)", text(parameters?.IVA_GENERAL_RATE?.value)],
|
||||
["Moeda explícita", text(value.context.currency)],
|
||||
["Data de exigibilidade declarada", value.context.reference_date],
|
||||
[monthly ? "Início da projecção mensal" : "Data de exigibilidade declarada", value.context.reference_date],
|
||||
[
|
||||
"Enquadramento",
|
||||
provenance?.scope_status === "undetermined" ? "Indeterminado: falta declaração do enquadramento" : "Declarado pelo utilizador; não validado juridicamente",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export type IvaIssue = {
|
||||
category: 'missing_field' | 'invalid_value' | 'out_of_scope' | 'period_unavailable' | 'system_configuration';
|
||||
field: string | null;
|
||||
label: string;
|
||||
received: unknown;
|
||||
expected: string;
|
||||
message: string;
|
||||
};
|
||||
export type IvaDiagnostic = {
|
||||
label: string;
|
||||
rule_version: number;
|
||||
validity: { start: string; end: string | null };
|
||||
source: { title?: string; diploma?: string; article?: string; url?: string };
|
||||
issues: IvaIssue[];
|
||||
};
|
||||
export const ivaIssueCategories = {
|
||||
missing_field: 'Campo em falta', invalid_value: 'Valor inválido', out_of_scope: 'Fora do âmbito',
|
||||
period_unavailable: 'Regra indisponível para o período', system_configuration: 'Configuração do sistema',
|
||||
};
|
||||
export function ivaReceived(value: unknown): string {
|
||||
const labels: Record<string, string> = { domestic: 'Operação interna', internal_general_net: 'Operação interna à taxa geral, sobre base líquida', import: 'Importação', export: 'Exportação', refund: 'Reembolso', unknown: 'Não sei', non_resident: 'Operação com não residente', other: 'Outra operação' };
|
||||
return typeof value === 'string' && value !== '' ? (labels[value] ?? value) : 'Não recebido';
|
||||
}
|
||||
export function ivaIssueText(issue: IvaIssue): string {
|
||||
return `${issue.label}: ${ivaIssueCategories[issue.category]}. Recebido: ${ivaReceived(issue.received)}. Esperado: ${issue.expected}. ${issue.message}`;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { monthlyProjectedPurchaseOrders, restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
|
||||
|
||||
/** Correspondence to the Tempo exemplar, not an electronic AT declaration or a new tax ruleset. */
|
||||
export const modeloAReference = {
|
||||
file: 'Novo Modelo A.docx',
|
||||
sha256: '62c198e90d8c6792f7d5ccefc6386df2e4b6089573067d3a6e1eae7fc2b9572b',
|
||||
edition: 'Edição/vigência do formulário não estabelecida; original preservado na biblioteca QA.',
|
||||
status: 'Simulação parcial: campos 01/02. Não é declaração submetida ou aceite pela Autoridade Tributária.',
|
||||
};
|
||||
|
||||
export type ModeloAField = { field: string; purpose: string; origin: string; calculation: string; basis: string; status: string };
|
||||
const pending = 'Em falta; não assumir zero';
|
||||
export const modeloAFields: ModeloAField[] = [
|
||||
{field:'01',purpose:'Base das operações à taxa geral',origin:'Vendas mensais já projectadas, declaradas sem IVA e no âmbito geral',calculation:'Base líquida do mês',basis:'CIVA arts.15 e 17; Lei22/2022; regra publicada existente',status:'Implementado no âmbito declarado'},
|
||||
{field:'02',purpose:'IVA à taxa geral',origin:'Resultado mensal congelado no servidor',calculation:'Campo01 × taxa da versão publicada /100',basis:'CIVA art.17(1); mesma regra/versionamento do consumidor',status:'Implementado; componente do apuramento'},
|
||||
{field:'03',purpose:'Base de operações específicas à taxa reduzida',origin:'Operações separadas e enquadradas; ainda não recolhidas',calculation:'Base das operações elegíveis',basis:'Exemplar e CIVA art.17-A, aditado pela Lei22/2022; enquadramento a cadastrar',status:pending},
|
||||
{field:'04',purpose:'IVA à taxa reduzida',origin:'Campo03 e futura versão fundamentada',calculation:'Campo03 × taxa reduzida aplicável; não activa pelo exemplar',basis:'CIVA art.17-A; âmbito/versão temporal a integrar',status:pending},
|
||||
{field:'05',purpose:'Vendas isentas com direito à dedução',origin:'Vendas classificadas por operação e fundamento',calculation:'Soma das bases classificadas; sem taxa presumida',basis:'Exemplar; CIVA arts.9–14 e 18–20 a consolidar por operação',status:pending},
|
||||
{field:'06',purpose:'Operações da alínea b), n.º1, art.18, conforme exemplar',origin:'Operações classificadas com referência verificada',calculation:'Soma separada; não inferir do nome do sector',basis:'Remissão do exemplar ao art.18(1)(b); cotejo do âmbito necessário',status:pending},
|
||||
{field:'07',purpose:'Isentas sem direito à dedução',origin:'Vendas classificadas e uso das aquisições',calculation:'Soma das bases elegíveis',basis:'Exemplar; incidência/isenções e exclusões de dedução a fundamentar',status:pending},
|
||||
{field:'08',purpose:'IVA dedutível — imobilizado',origin:'Aquisições/facturas de activos, não o valor global do investimento',calculation:'Apenas imposto elegível documentado',basis:'CIVA arts.18–21; art.18(2), Lei10/2025: documentos legais e fornecedor inscrito',status:pending},
|
||||
{field:'09',purpose:'IVA dedutível — existências',origin:'Compras projectadas reutilizáveis; falta IVA, documento e elegibilidade',calculation:'IVA elegível das existências, não compras × taxa geral',basis:'CIVA arts.18–21; condições/exclusões, afectação e período',status:pending},
|
||||
{field:'10',purpose:'IVA dedutível — outros bens/serviços',origin:'Aquisições discriminadas; despesas agregadas não provam IVA',calculation:'IVA elegível; excluir operações sem direito',basis:'CIVA arts.18–21; art.19(2), Lei10/2025, inclui exclusão das operações art.17-A',status:pending},
|
||||
{field:'11',purpose:'IVA dedutível — importações',origin:'Documento aduaneiro legal e afectação; ainda não recolhidos',calculation:'Imposto aduaneiro elegível, não taxa sobre orçamento',basis:'CIVA art.18(2), Lei10/2025; demais condições/exclusões a cadastrar',status:pending},
|
||||
{field:'12',purpose:'Regularizações a favor do sujeito passivo',origin:'Ajustamentos individualizados, documento e motivo',calculation:'Soma de regularizações elegíveis',basis:'Exemplar tem remissão interna 11/12 divergente das caixas12/13; fundamento por motivo por conferir',status:pending},
|
||||
{field:'13',purpose:'Regularizações a favor do Estado',origin:'Ajustamentos individualizados, documento e motivo',calculation:'Soma separada das regularizações elegíveis',basis:'Mesma divergência; não corrigir o original nem inventar incidência',status:pending},
|
||||
{field:'14',purpose:'Soma das bases do quadro de apuramento',origin:'Campos01,03,05,06,07 completos',calculation:'01+03+05+06+07',basis:'Fórmula impressa no exemplar; não é apenas vendas tributáveis',status:'Estrutura mapeada; total indeterminado'},
|
||||
{field:'15',purpose:'Total a favor do sujeito passivo',origin:'Campos08–12 completos',calculation:'08+09+10+11+12',basis:'Fórmula do exemplar; depende de deduções/regularizações fundamentadas',status:'Estrutura mapeada; total indeterminado'},
|
||||
{field:'16',purpose:'Total a favor do Estado',origin:'Campos02,04,13 completos',calculation:'02+04+13',basis:'Fórmula impressa no exemplar; 02 sozinho não equivale a16',status:'Estrutura mapeada; total indeterminado'},
|
||||
{field:'17',purpose:'Diferença devedora antes de créditos anteriores',origin:'Totais15/16 completos',calculation:'16−15 quando16>15',basis:'Instrução do exemplar; não é automaticamente pagamento',status:'Indeterminado; sem consolidação completa'},
|
||||
{field:'18',purpose:'Diferença credora antes de créditos anteriores',origin:'Totais15/16 completos',calculation:'15−16 quando15>16',basis:'Instrução do exemplar; não constitui reembolso reconhecido',status:'Indeterminado; sem consolidação completa'},
|
||||
{field:'19',purpose:'Excesso a reportar do período anterior',origin:'Histórico fiscal anterior confirmado, separado de estimativas',calculation:'Crédito anterior utilizável; sem copiar crédito estimado',basis:'Quadro de utilização de créditos do exemplar; condições/prazo por conferir',status:pending},
|
||||
{field:'20',purpose:'Créditos comunicados pelos serviços',origin:'Comunicação efectiva da Administração Fiscal',calculation:'Crédito efectivamente comunicado e utilizável',basis:'Exemplar; não converter pedido ou crédito estimado em comunicação',status:pending},
|
||||
{field:'21',purpose:'Diferencial de declaração de substituição',origin:'Valores17/18 da declaração actual e da substituída',calculation:'Casos impressos no quadro9, com sinal devedor/credor',basis:'Exemplar; ligação às declarações efectivas e regras de substituição em falta',status:pending},
|
||||
{field:'22',purpose:'IVA a entregar no contexto da declaração',origin:'Apuramento e créditos/substituição completos',calculation:'Não derivar apenas do campo02',basis:'Quadro10 do exemplar; procedimento aplicável por fundamentar',status:pending},
|
||||
{field:'23',purpose:'Juros compensatórios',origin:'Factos e períodos que legalmente os determinem',calculation:'Sem taxa, mora ou obrigação presumidas',basis:'Quadro10; fundamento específico não analisado neste lote',status:pending},
|
||||
{field:'24',purpose:'Importância a pagar',origin:'Campos22/23 completos',calculation:'22+23',basis:'Fórmula do exemplar; não é ordem nem execução de pagamento',status:pending},
|
||||
{field:'25',purpose:'Crédito de imposto do quadro11',origin:'Apuramento completo e tratamento dos créditos anteriores',calculation:'Montante recuperável no contexto próprio, por fundamentar',basis:'Caixa25 confirmada visualmente no bloco de recuperação; condições por consolidar',status:pending},
|
||||
{field:'26',purpose:'Reporte para período seguinte',origin:'Crédito utilizável e opção no quadro11',calculation:'Separado de pedido e recebimento',basis:'Quadro11; cotejo de condições/prazos e articulação19/20 necessário',status:pending},
|
||||
{field:'27',purpose:'Pedido de reembolso',origin:'Crédito elegível e pedido, sem presumir reconhecimento/caixa',calculation:'Pedido distinto de aprovação e recebimento',basis:'Exemplar cita art.20; Lei10/2025 altera reembolsos no art.21. Divergência registada.',status:pending},
|
||||
];
|
||||
|
||||
export function modeloAComponentRows(snapshot: Record<string, unknown>): Array<[string,string]> {
|
||||
if (!snapshot.monthly_projection) return [];
|
||||
return [
|
||||
['Correspondência Modelo A',modeloAReference.status],
|
||||
['Campos01/02', 'Base líquida e IVA mensal da versão publicada. Os restantes campos não foram presumidos iguais a zero.'],
|
||||
['Apuramento do período — campos14–18', 'Indeterminado: faltam componentes e enquadramentos do Modelo A. IVA das vendas não é imposto final a pagar.'],
|
||||
['Origem dos dados','Projecção do negócio; não são operações efectivas nem declaração fiscal.'],
|
||||
['Exemplar de referência',`${modeloAReference.file}; SHA256 ${modeloAReference.sha256}. ${modeloAReference.edition}`],
|
||||
];
|
||||
}
|
||||
|
||||
export function modeloAPurchaseReferences(draft: Record<string, unknown>): Array<{month:number; declared:string|null; net:null; vat:null; gross:null; deductible:null}> {
|
||||
if (!draft.business_plan_snapshot) return [];
|
||||
const plan = restoreBusinessPlanSnapshot(String(draft.business_plan_snapshot));
|
||||
if (!plan) throw new Error('Não foi possível recuperar as aquisições projectadas. O original guardado foi preservado.');
|
||||
return monthlyProjectedPurchaseOrders(plan).map((amount, index) => ({month:index+1, declared:amount, net:null, vat:null, gross:null, deductible:null}));
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { monthlyProjectedSales, restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
|
||||
import { restoreCashflowDraftSnapshot } from './monthly-cashflow.ts';
|
||||
import { isApiSimulationReadResponseV1, type ApiSimulationExecutionV1 } from './simulation-contract.ts';
|
||||
|
||||
export type MonthlyIvaInput = {
|
||||
scenario_kind: 'fiscal_rule_published';
|
||||
monthly_sales: Array<{ date: string; amount: string | null }>;
|
||||
sales_vat_basis: 'net' | 'included' | 'unknown';
|
||||
currency: string;
|
||||
scope_declared: boolean;
|
||||
};
|
||||
export type MonthlyIvaSettings = { start: string; basis: MonthlyIvaInput['sales_vat_basis']; declared: boolean; auto: boolean };
|
||||
export type MonthlyIvaRecord = { input: MonthlyIvaInput; execution: ApiSimulationExecutionV1 };
|
||||
export function readMonthlyIvaSettings(raw?: string): MonthlyIvaSettings {
|
||||
try { const value = JSON.parse(raw ?? '');
|
||||
if (typeof value.start === 'string' && ['net', 'included', 'unknown'].includes(value.basis) && typeof value.declared === 'boolean' && typeof value.auto === 'boolean') return value;
|
||||
} catch { /* An invalid setting is not an invented business assumption. */ }
|
||||
return {start: '', basis: 'unknown', declared: false, auto: false};
|
||||
}
|
||||
export function monthlySalesFromDraft(draft: Record<string, unknown>): { amounts: Array<string | null>; source: string } {
|
||||
if (draft.business_plan_snapshot) {
|
||||
const plan = restoreBusinessPlanSnapshot(String(draft.business_plan_snapshot));
|
||||
if (!plan) throw new Error('Não foi possível recuperar a projecção de vendas. Os dados guardados não foram substituídos.');
|
||||
return {amounts: monthlyProjectedSales(plan), source: 'Vendas por produto e mês do plano empresarial; mesmos pressupostos e representação monetária da projecção de vendas.'};
|
||||
}
|
||||
if (draft.cashflow_snapshot) {
|
||||
const saved = restoreCashflowDraftSnapshot(String(draft.cashflow_snapshot));
|
||||
if (!saved) throw new Error('Não foi possível recuperar a receita projectada.');
|
||||
return {amounts: Array.from({length: 12}, () => saved.inputs.monthlyRevenue.trim() === '' ? null : saved.inputs.monthlyRevenue), source: 'Receita mensal constante já declarada na projecção de tesouraria; confirme que corresponde a vendas tributáveis.'};
|
||||
}
|
||||
return {amounts: Array.from({length: 12}, () => null), source: 'Sem projecção de vendas guardada. Prepare as vendas no plano empresarial; não é necessário voltar a introduzi-las aqui.'};
|
||||
}
|
||||
export function monthlyIvaInput(draft: Record<string, unknown>, settings: MonthlyIvaSettings): MonthlyIvaInput {
|
||||
const sales = monthlySalesFromDraft(draft);
|
||||
if (!/^2026-(0[1-9]|1[0-2])$/.test(settings.start)) throw new Error('Indique o primeiro mês real da projecção. Este lote abrange apenas 2026.');
|
||||
const start = Number(settings.start.slice(5));
|
||||
if (sales.amounts.length > 12 || start + sales.amounts.length > 13) throw new Error('O horizonte ultrapassa a cobertura de 2026. Esta versão não calcula os meses fora desse período.');
|
||||
return {scenario_kind: 'fiscal_rule_published', monthly_sales: sales.amounts.map((amount, index) => ({date: `2026-${String(start + index).padStart(2, '0')}-01`, amount})),
|
||||
sales_vat_basis: settings.basis, currency: String(draft.currency ?? ''), scope_declared: settings.declared};
|
||||
}
|
||||
export function monthlyIvaKey(input: Omit<MonthlyIvaInput, 'scenario_kind'>): string {
|
||||
return JSON.stringify([input.currency, input.sales_vat_basis, input.scope_declared, input.monthly_sales.map((row) => [row.date, row.amount])]);
|
||||
}
|
||||
export function readMonthlyIvaRecord(raw?: string): MonthlyIvaRecord | null {
|
||||
try { const value = JSON.parse(raw ?? '');
|
||||
if (value?.input?.scenario_kind === 'fiscal_rule_published' && Array.isArray(value.input.monthly_sales) && isApiSimulationReadResponseV1({data: value.execution})) return value;
|
||||
} catch { /* Keep the stored value intact; no fallback fiscal result. */ }
|
||||
return null;
|
||||
}
|
||||
export const monthlyIvaCashTreatment = 'IVA apresentado separadamente: não é somado novamente aos recebimentos declarados nem deduzido como custo ou imposto final a pagar. A tesouraria conserva os recebimentos e pagamentos introduzidos, antes de uma liquidação fiscal não apurada; não se presume cobrança integral das vendas no mesmo mês.';
|
||||
@@ -173,6 +173,7 @@ const rowSection: Record<string, DossierReportSectionId> = {
|
||||
'Ponto de equilíbrio': 'finance',
|
||||
Orçamento: 'finance',
|
||||
Tesouraria: 'finance',
|
||||
'IVA mensal': 'finance',
|
||||
Equipa: 'team',
|
||||
'Necessidade de negócio': 'resources',
|
||||
Financiamento: 'resources',
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { displayValue } from './display-value.ts';
|
||||
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
|
||||
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
|
||||
import { fiscalQaRows } from './fiscal-qa-preview.ts';
|
||||
import { csvCellWithDelimiter } from './csv.js';
|
||||
import {
|
||||
restoreCashflowDraftSnapshot,
|
||||
@@ -608,6 +611,19 @@ export function buildProjectDossierRows(
|
||||
'Planeado/realizado declarado pelo utilizador',
|
||||
]);
|
||||
}
|
||||
if (draft.monthly_iva_snapshot) {
|
||||
const saved = readMonthlyIvaRecord(String(draft.monthly_iva_snapshot));
|
||||
let current = false;
|
||||
try { current = !!saved && monthlyIvaKey(monthlyIvaInput(draft, readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? '')))) === monthlyIvaKey(saved.input); } catch { /* An old fiscal version is never silently made current. */ }
|
||||
rows.push(['IVA mensal', 'Estado da versão', current ? 'Entradas correspondentes à projecção guardada' : 'Histórico desactualizado face às vendas ou pressupostos actuais; não aplicar como resultado actual', 'Execução versionada no servidor; sem apuramento final']);
|
||||
if (saved?.execution.result_snapshot) for (const [label,value] of fiscalQaRows(saved.execution.result_snapshot)) rows.push(['IVA mensal', label, value, 'Snapshot fiscal preservado; sem recálculo na exportação']);
|
||||
else rows.push(['IVA mensal', 'Resultado', 'Indeterminado ou execução ainda não concluída', 'Sem substituição por zero']);
|
||||
try {
|
||||
for (const purchase of modeloAPurchaseReferences(draft)) rows.push(['IVA mensal', `Compras/encomendas — mês ${purchase.month}`, purchase.declared ?? 'Sem informação', 'Pressuposto actual do plano, não resultado fiscal histórico; base/IVA incluído por classificar; IVA suportado e dedutível indeterminados; não é data de factura ou pagamento']);
|
||||
} catch {
|
||||
rows.push(['IVA mensal','Aquisições projectadas','Não foi possível recuperar; sem substituição por zero','Pressupostos guardados preservados; não calcula deduções']);
|
||||
}
|
||||
}
|
||||
if (draft.cashflow_snapshot) {
|
||||
const saved = restoreCashflowDraftSnapshot(
|
||||
String(draft.cashflow_snapshot),
|
||||
|
||||
@@ -965,6 +965,12 @@ export const simulationFieldGroups: Record<
|
||||
},
|
||||
],
|
||||
tax: [
|
||||
{
|
||||
key: 'fiscal_currency',
|
||||
label: 'Moeda da operação fiscal (sem conversão automática)',
|
||||
options: 'currency',
|
||||
showWhen: (d) => d.fiscal_component === 'iva' && d.fiscal_scenario_mode !== 'custom_planning',
|
||||
},
|
||||
{
|
||||
key: 'fiscal_period',
|
||||
label: 'Ano do período fiscal a simular',
|
||||
|
||||
Reference in New Issue
Block a user