Files
Investidor-V2/resources/js/components/simulation-irpc-normal.tsx
T
2026-11-15 18:56:39 +02:00

634 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { SimulationIrpcSpecial } from '@/components/simulation-irpc-special';
import FiscalOperationPicker from '@/components/fiscal-operation-picker';
import { inputText } from '@/lib/input-text';
import { SimulationIrpcFormPreparation } from '@/components/simulation-irpc-form-preparation';
import { SimulationIrpcPeriodFx } from '@/components/simulation-irpc-period-fx';
import { SimulationIrpcDepreciation } from '@/components/simulation-irpc-depreciation';
import { SimulationIrpcCosts } from '@/components/simulation-irpc-costs';
import { SimulationIrpcSchedules } from '@/components/simulation-irpc-schedules';
import { SimulationIrpcEconomicInclusion } from '@/components/simulation-irpc-economic-inclusion';
import { SimulationIrpcAssessment } from '@/components/simulation-irpc-assessment';
import { useState } from 'react';
import { SimulationIrpcTaxableBase } from '@/components/simulation-irpc-taxable-base';
import {
newNormalProfitInput,
profitTopics,
type NormalProfitInput,
type ProfitEvidence,
} from '@/lib/irpc-normal-profit';
import {
normalBook,
saveNormalBook,
normalFinancialSource,
newNormalAdjustment,
recordNormal,
normalHtml,
normalReportRows,
type NormalBook,
type NormalAdjustment,
} from '@/lib/irpc-normal';
export function SimulationIrpcNormal({
draft,
onChange,
}: {
draft: Record<string, unknown>;
onChange: (field: string, value: string | string[]) => void;
}) {
const [error, setError] = useState('');
if (draft.fiscal_component !== 'irpc') return null;
try {
const b = normalBook(draft),
source = normalFinancialSource(draft);
const save = (next: NormalBook) => {
try {
onChange('modelo_a_snapshot', saveNormalBook(draft, next));
setError('');
} catch (e) {
setError(String(e));
}
};
const selectedPeriod = inputText(draft.fiscal_period ?? '');
if (selectedPeriod && selectedPeriod !== b.year)
return (
<aside className="rounded border p-3" role="alert">
<p>
O enquadramento escolheu {selectedPeriod}, mas esta
preparação de IRPC está em {b.year}. Confira o período
antes de calcular; os dados anteriores ficam preservados
e nenhum ano foi alterado.
</p>
<button
type="button"
className="underline"
disabled={!/^\d{4}$/.test(selectedPeriod)}
onClick={() => save({ ...b, year: selectedPeriod })}
>
Preparar o ano escolhido, sujeito à cobertura disponível
</button>
</aside>
);
const patch = (id: string, p: Partial<NormalAdjustment>) =>
save({
...b,
rows: b.rows.map((r) => (r.id === id ? { ...r, ...p } : r)),
});
const profit = (patch: Partial<NormalProfitInput>) =>
save({
...b,
profit: { ...(b.profit ?? newNormalProfitInput()), ...patch },
});
const field = (
r: NormalAdjustment,
k: 'document' | 'line' | 'amount' | 'reference',
label: string,
) => (
<label className="block">
{label}
<input
value={r[k]}
onChange={(e) => patch(r.id, { [k]: e.target.value })}
/>
</label>
);
return (
<section
aria-label="IRPC normal — ajustamentos"
className="space-y-3 rounded border p-4"
>
<h4>IRPC — ajustamentos ao resultado financeiro</h4>
<p>
Acréscimos de multas sem origem contratual e juros
compensatórios incluídos nos custos do plano, artigo 36(1)c.
A conferência anual pode determinar o lucro tributável
simulado no recorte ordinário documentado. A matéria
colectável exige conferência específica de deduções e
perdas; o imposto estimado e a estimativa anual têm cálculos
próprios. Os juros da simulação de empréstimo não são
adoptados automaticamente como custo fiscal.
</p>
{error && <p role="alert">{error}</p>}
{source ? (
<p>
Origem: plano financeiro desta oportunidade;{' '}
{source.periods} meses; {source.currency}; resultado
antes de imposto: {source.accountingProfitBeforeTax}.
Cobranças, pagamentos e reembolso de capital ficam fora
do resultado.
</p>
) : (
<p>
{b.periodsFx
? 'Complete a correspondência do período e moeda abaixo para reutilizar o plano existente emMZN.'
: 'Prepare o plano financeiro para reutilizar o resultado e os custos reconciliados.'}
</p>
)}
<label className="block">
Exercício fiscal
<input
value={b.year}
onChange={(e) => save({ ...b, year: e.target.value })}
/>
</label>
<label className="block">
{b.periodsFx
? 'Correspondência do plano ao período documentado'
: b.schedules
? 'Correspondência dos12 meses do plano ao período anual documentado'
: 'Correspondência dos 12 meses do plano a Janeiro–Dezembro de 2026'}
<input
value={b.mappingReference}
onChange={(e) =>
save({ ...b, mappingReference: e.target.value })
}
/>
</label>
<label className="block">
Âmbito
<select
value={b.scope}
onChange={(e) =>
save({
...b,
scope: e.target.value as NormalBook['scope'],
})
}
>
<option value="">Por identificar</option>
<option value="resident_general">
Sujeito residente — regime geral, fora de
transparência e regimes especiais
</option>
<option value="ep_general">
Não residente com estabelecimento estável — regime
geral, só resultado imputável ao EP (mapas
complementares)
</option>
<option value="other">
Outro âmbito — por corresponder
</option>
</select>
</label>
<label className="block">
Suporte do âmbito e da imputação integral do resultado ao
sujeito/EP
<input
value={b.scopeReference}
onChange={(e) =>
save({ ...b, scopeReference: e.target.value })
}
/>
</label>
<button
type="button"
onClick={() =>
save({
...b,
rows: [
...b.rows,
newNormalAdjustment(crypto.randomUUID()),
],
})
}
>
Acrescentar ajustamento ao resultado
</button>
{b.rows.map((r) => (
<fieldset
key={r.id}
className="space-y-2 rounded border p-3"
>
<legend>
{r.document || 'Ajustamento por identificar'}
</legend>
<label className="block">
Custo de origem na ponte
<select
value={r.sourceCode}
onChange={(e) =>
patch(r.id, { sourceCode: e.target.value })
}
>
<option value="">Seleccione</option>
{source?.accountingLines
.filter(
(l) => l.accountingEffect === 'expense',
)
.map((l) => (
<option key={l.code} value={l.code}>
{l.origin}: {l.total}{' '}
{source.currency}
</option>
))}
</select>
</label>
{field(
r,
'document',
'Documento ou pressuposto identificado',
)}
{field(r, 'line', 'Linha distinta do documento')}
{field(r, 'amount', 'Parcela do custo em MZN')}
<label className="block">
Natureza documentada
<select
value={r.rule}
onChange={(e) =>
patch(r.id, {
rule: e.target
.value as NormalAdjustment['rule'],
})
}
>
<option value="">Por classificar</option>
<option value="noncontractual_fine">
Multa/encargo por infracção sem origem
contratual — 36(1)c
</option>
<option value="compensatory_interest">
Juros compensatórios — 36(1)c
</option>
<option value="other">
Outro ajustamento — por corresponder
</option>
</select>
</label>
<label className="block">
Parcela incluída nos custos do resultado de partida
<select
value={r.included}
onChange={(e) =>
patch(r.id, {
included: e.target
.value as NormalAdjustment['included'],
})
}
>
<option value="">Por demonstrar</option>
<option value="yes">
Sim, reconciliada com a linha de origem
</option>
<option value="no">
Não — não acrescentar novamente
</option>
</select>
</label>
{field(
r,
'reference',
'Suporte da qualificação, ocorrência e inclusão contabilística',
)}
<p>
Juros bancários e penalidades contratuais não são
classificados automaticamente como infracções.
</p>
</fieldset>
))}
{!b.profit ? (
<button type="button" onClick={() => profit({})}>
Iniciar conferência até ao lucro tributável
</button>
) : (
<fieldset
aria-label="Conferência do lucro tributável"
className="space-y-3 rounded border p-3"
>
<legend>
Resultado financeiro → ajustamentos → lucro
tributável simulado
</legend>
<p>
Conferência do sujeito passivo/exercício completo.
Cada receita remanescente deve ser ordinária do
exercício;cada custo remanescente deve estar
documentado e ser indispensável à fonte
produtora,sem outras exclusões ou limites. Outras
situações conhecidas permanecem discriminadas.
Ausência declarada exige suporte e não é informação
em falta.
</p>
<label className="block">
Suporte da completude do sujeito passivo no
exercício
<input
value={b.profit.completenessReference}
onChange={(e) =>
profit({
completenessReference: e.target.value,
})
}
/>
</label>
{source?.accountingLines.map((l) => {
const review = b.profit!.lines.find(
(r) => r.code === l.code,
) ?? {
code: l.code,
state: '' as const,
reference: '',
};
const change = (p: Partial<typeof review>) =>
profit({
lines: [
...b.profit!.lines.filter(
(r) => r.code !== l.code,
),
{ ...review, ...p },
],
});
return (
<fieldset
key={l.code}
className="rounded border p-2"
>
<legend>
{l.origin} — {l.total} {source.currency}
</legend>
<label className="block">
Estado fiscal — {l.origin}
<select
aria-label={
'Estado fiscal — ' + l.origin
}
value={review.state}
onChange={(e) =>
change({
state: e.target
.value as ProfitEvidence['state'],
})
}
>
<option value="">
Informação em falta
</option>
<option value="known">
Valor conhecido e qualificação
ordinária documentada
</option>
<option value="absent">
Ausência declarada — conferir
origem zero
</option>
</select>
</label>
<label className="block">
Suporte fiscal — {l.origin}
<input
value={review.reference}
onChange={(e) =>
change({
reference: e.target.value,
})
}
/>
</label>
</fieldset>
);
})}
<p>
Depreciação positiva e juros financeiros não
integralmente acrescidos exigem correspondência
própria;uma declaração de ausência não apaga os
valores do plano.
</p>
{profitTopics.map(([k, label]) => {
const t = b.profit!.topics[k];
const change = (p: Partial<ProfitEvidence>) =>
profit({
topics: {
...b.profit!.topics,
[k]: { ...t, ...p },
},
});
return (
<fieldset
key={k}
className="rounded border p-2"
>
<legend>{label}</legend>
<label className="block">
Estado — {label}
<select
aria-label={'Estado — ' + label}
value={t.state}
onChange={(e) =>
change({
state: e.target
.value as ProfitEvidence['state'],
})
}
>
<option value="">
Informação em falta
</option>
<option value="known">
Valor conhecido
</option>
<option value="absent">
Ausência declarada com suporte
</option>
</select>
</label>
{(t.state === 'known' ||
t.amount !== '') && (
<label className="block">
Montante conhecido — {label}
<input
value={t.amount}
onChange={(e) =>
change({
amount: e.target.value,
})
}
/>
</label>
)}
<label className="block">
Suporte — {label}
<input
value={t.reference}
onChange={(e) =>
change({
reference: e.target.value,
})
}
/>
</label>
</fieldset>
);
})}
</fieldset>
)}
<FiscalOperationPicker
family="irpc"
selected={draft.fiscal_tools_selection}
onChange={(selected) =>
onChange('fiscal_tools_selection', selected)
}
operations={[
{
id: 'period',
label: 'Período e moeda do plano',
description:
'Acrescente quando necessário ao seu cenário e confira os pressupostos e fontes da operação.',
render: () => (
<SimulationIrpcPeriodFx
draft={draft}
book={b}
save={save}
/>
),
},
{
id: 'schedules',
label: 'Mapas complementares',
description:
'Acrescente quando necessário ao seu cenário e confira os pressupostos e fontes da operação.',
render: () => (
<SimulationIrpcSchedules book={b} save={save} />
),
},
{
id: 'economic',
label: 'Inclusões e correcções económicas',
description:
'Acrescente quando necessário ao seu cenário e confira os pressupostos e fontes da operação.',
render: () => (
<SimulationIrpcEconomicInclusion
draft={draft}
book={b}
save={save}
/>
),
},
{
id: 'depreciation',
label: 'Amortizações e depreciações',
description:
'Acrescente quando necessário ao seu cenário e confira os pressupostos e fontes da operação.',
render: () => (
<SimulationIrpcDepreciation
draft={draft}
book={b}
save={save}
/>
),
},
{
id: 'costs',
label: 'Tratamento dos custos',
description:
'Acrescente quando necessário ao seu cenário e confira os pressupostos e fontes da operação.',
render: () => (
<SimulationIrpcCosts
draft={draft}
book={b}
save={save}
/>
),
},
{
id: 'taxable',
label: 'Matéria colectável',
description:
'Acrescente quando necessário ao seu cenário e confira os pressupostos e fontes da operação.',
render: () => (
<SimulationIrpcTaxableBase
draft={draft}
book={b}
save={save}
/>
),
},
{
id: 'assessment',
label: 'Estimativa do imposto',
description:
'Acrescente quando necessário ao seu cenário e confira os pressupostos e fontes da operação.',
render: () => (
<SimulationIrpcAssessment
book={b}
save={save}
/>
),
},
{
id: 'special',
label: 'Situações fiscais específicas',
description:
'Acrescente quando necessário ao seu cenário e confira os pressupostos e fontes da operação.',
render: () => (
<SimulationIrpcSpecial
draft={draft}
book={b}
save={save}
/>
),
},
{
id: 'forms',
label: 'Preparação documental fiscal',
description:
'Acrescente quando necessário ao seu cenário e confira os pressupostos e fontes da operação.',
render: () => (
<SimulationIrpcFormPreparation
draft={draft}
book={b}
save={save}
/>
),
},
]}
/>
<button
type="button"
onClick={() => {
try {
save(recordNormal(draft, b));
} catch (e) {
setError(String(e));
}
}}
>
Calcular e guardar L6 e tratamentos L7 preparados
</button>
<button
type="button"
onClick={() => {
try {
const html = normalHtml(draft),
url = URL.createObjectURL(
new Blob([html], {
type: 'text/html;charset=utf-8',
}),
),
a = document.createElement('a');
a.href = url;
a.download = 'IRPC-normal-ajustamentos.html';
a.click();
URL.revokeObjectURL(url);
setError('');
} catch (e) {
setError(String(e));
}
}}
>
Exportar mapa parcial L6
</button>
{normalReportRows(draft).map((r, n) => (
<p key={n}>
{r[1]} — {r[2]} — {r[3]}
</p>
))}
</section>
);
} catch (e) {
return (
<p role="alert">
Preparação L6 não recuperável: {String(e)}. Originais e
histórico conservados.
</p>
);
}
}