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">
|
||||
|
||||
Reference in New Issue
Block a user