611 lines
28 KiB
TypeScript
611 lines
28 KiB
TypeScript
import { SimulationIrpcInvestment } from './simulation-irpc-investment';
|
|
import { SimulationIrpcEconomic } from './simulation-irpc-economic';
|
|
import { FiscalProofParcelFields } from './fiscal-proof-parcel';
|
|
import { SimulationIrpcCarry } from './simulation-irpc-carry';
|
|
import { SimulationIrpcInternational } from './simulation-irpc-international';
|
|
import { SimulationIrpcPec } from './simulation-irpc-pec';
|
|
import { useState } from 'react';
|
|
import {
|
|
annualBook,
|
|
newAnnualBook,
|
|
saveAnnualBook,
|
|
reuseAnnualAssessment,
|
|
annualAssessment,
|
|
annualWithholdingOptions,
|
|
reuseAnnualWithholding,
|
|
newAnnualPpc,
|
|
recordAnnual,
|
|
annualReportRows,
|
|
annualHtml,
|
|
annualSource,
|
|
type AnnualBook,
|
|
type AnnualPpc,
|
|
} from '@/lib/irpc-annual-settlement';
|
|
|
|
type Props = {
|
|
draft: Record<string, unknown>;
|
|
onChange: (field: string, value: string) => void;
|
|
};
|
|
export function SimulationIrpcAnnual({ draft, onChange }: Props) {
|
|
const [error, setError] = useState('');
|
|
if (draft.fiscal_component !== 'irpc') return null;
|
|
try {
|
|
const b = annualBook(draft),
|
|
save = (next: AnnualBook) => {
|
|
try {
|
|
onChange('modelo_a_snapshot', saveAnnualBook(draft, next));
|
|
setError('');
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
};
|
|
if (!b)
|
|
return (
|
|
<section
|
|
aria-label="IRPC — L8 fecho anual"
|
|
className="rounded border p-4"
|
|
>
|
|
<h4>L8 — da colecta ao saldo anual simulado</h4>
|
|
<p>
|
|
Reutiliza a colecta válida, retenções efectivamente
|
|
sofridas por conta e PPC comprovados do próprio
|
|
sujeito/exercício. PEC comprovado pode ser detalhado no
|
|
próprio exercício; créditos com regras próprias
|
|
permanecem separados.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
try {
|
|
save(
|
|
reuseAnnualAssessment(
|
|
draft,
|
|
newAnnualBook(),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Preparar saldo anual L8
|
|
</button>
|
|
{error && <p role="alert">{error}</p>}
|
|
</section>
|
|
);
|
|
const patch = (p: Partial<AnnualBook>) => save({ ...b, ...p }),
|
|
a = annualAssessment(draft),
|
|
options = annualWithholdingOptions(draft);
|
|
const field = (
|
|
key: 'subjectNuit' | 'year' | 'reference' | 'assessmentReference',
|
|
label: string,
|
|
) => (
|
|
<label className="block">
|
|
{label}
|
|
<input
|
|
aria-label={label}
|
|
value={b[key]}
|
|
onChange={(e) => patch({ [key]: e.target.value })}
|
|
/>
|
|
</label>
|
|
);
|
|
const group = (key: 'withholdings' | 'ppc', label: string) => (
|
|
<>
|
|
<label className="block">
|
|
Estado — {label}
|
|
<select
|
|
aria-label={'Estado L8 — ' + label}
|
|
value={b[key].state}
|
|
onChange={(e) =>
|
|
patch({
|
|
[key]: {
|
|
...b[key],
|
|
state: e.target.value as
|
|
| ''
|
|
| 'known'
|
|
| 'absent',
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<option value="">Informação em falta</option>
|
|
<option value="known">
|
|
Valores conhecidos — seleccionar comprovativos
|
|
</option>
|
|
<option value="absent">
|
|
Ausência declarada com suporte
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label className="block">
|
|
Suporte — {label}
|
|
<input
|
|
aria-label={'Suporte L8 — ' + label}
|
|
value={b[key].reference}
|
|
onChange={(e) =>
|
|
patch({
|
|
[key]: { ...b[key], reference: e.target.value },
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
</>
|
|
);
|
|
return (
|
|
<section
|
|
aria-label="IRPC — L8 fecho anual"
|
|
className="space-y-3 rounded border p-4"
|
|
>
|
|
<h4>L8 — colecta, abatimentos e saldo anual simulado</h4>
|
|
<p>
|
|
A colecta acima é anterior aos abatimentos. Este saldo tem
|
|
cálculo e histórico próprios; retenção entregue pela empresa
|
|
por pagamento a terceiros não é retenção sofrida. O
|
|
resultado continua simulado, sem submissão, pagamento ou
|
|
reembolso automático.
|
|
</p>
|
|
<p>{annualSource(b)}</p>
|
|
{error && <p role="alert">{error}</p>}
|
|
{field('subjectNuit', 'NUIT do sujeito do fecho L8')}
|
|
{field('year', 'Exercício do fecho L8')}
|
|
{field(
|
|
'reference',
|
|
'Suporte do fecho da entidade e exercício L8',
|
|
)}
|
|
<p>
|
|
Colecta disponível:
|
|
{a.colecta
|
|
? a.colecta.numerator +
|
|
'/' +
|
|
a.colecta.denominator +
|
|
' MZN'
|
|
: 'Indeterminada'}
|
|
;origem:
|
|
{a.kind === 'special' ? 'L7 qualificado' : 'L6 normal'}
|
|
;NUIT:{a.subjectNuit || 'Por identificar'};exercício:
|
|
{a.year || 'Por identificar'}.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
try {
|
|
save(reuseAnnualAssessment(draft, b));
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Reutilizar colecta corrente L6/L7
|
|
</button>
|
|
{field(
|
|
'assessmentReference',
|
|
'Suporte da colecta reutilizada L8',
|
|
)}
|
|
<fieldset className="space-y-2 rounded border p-3">
|
|
<legend>Retenções efectivamente sofridas por conta</legend>
|
|
{group('withholdings', 'Retenções sofridas')}
|
|
{options.length === 0 ? (
|
|
<p>
|
|
Não há afectações de comprovativosL5 neste contexto.
|
|
Reutilize e reconcilie a retenção recebida emL5 ou
|
|
declare ausência com suporte.
|
|
</p>
|
|
) : (
|
|
options.map((o) => (
|
|
<div
|
|
key={o.rowId + '|' + o.paymentId}
|
|
className="rounded border p-2"
|
|
>
|
|
<p>
|
|
{o.document || 'Prova por identificar'}
|
|
;emitente:{o.issuer};data:{o.date}
|
|
;parcelaL5:{o.amount}MZN;
|
|
{o.reason ||
|
|
'Retenção recebida/efectiva por conta reconciliada emL5.'}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
disabled={
|
|
!o.ready ||
|
|
b.withholdings.rows.some(
|
|
(r) =>
|
|
r.rowId === o.rowId &&
|
|
r.paymentId === o.paymentId,
|
|
)
|
|
}
|
|
onClick={() => {
|
|
try {
|
|
save(
|
|
reuseAnnualWithholding(
|
|
draft,
|
|
b,
|
|
o.rowId,
|
|
o.paymentId,
|
|
crypto.randomUUID(),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Seleccionar prova L5{' '}
|
|
{o.document || o.paymentId}
|
|
</button>
|
|
</div>
|
|
))
|
|
)}
|
|
{b.withholdings.rows.map((r, index) => {
|
|
const change = (p: Partial<typeof r>) =>
|
|
patch({
|
|
withholdings: {
|
|
...b.withholdings,
|
|
rows: b.withholdings.rows.map((x) =>
|
|
x.id === r.id ? { ...x, ...p } : x,
|
|
),
|
|
},
|
|
});
|
|
return (
|
|
<fieldset key={r.id} className="rounded border p-2">
|
|
<legend>
|
|
Retenção seleccionada {index + 1}
|
|
</legend>
|
|
<p>
|
|
Rendimento:{r.rowId};prova:{r.paymentId}
|
|
;NUIT:{r.subjectNuit};exercício:{r.year}.
|
|
Fonte conservada. Se mudou, retire esta
|
|
selecção e seleccione a prova actual. O
|
|
histórico mantém-se.
|
|
</p>
|
|
<label className="block">
|
|
Parcela a abater
|
|
<input
|
|
aria-label={
|
|
'Parcela da retenção L8 ' +
|
|
(index + 1)
|
|
}
|
|
value={r.amount}
|
|
onChange={(e) =>
|
|
change({ amount: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
O próprio sujeito é beneficiário e o
|
|
rendimento está incluído na base deste
|
|
exercício
|
|
<select
|
|
aria-label={
|
|
'Rendimento incluído L8 ' +
|
|
(index + 1)
|
|
}
|
|
value={r.incomeIncluded}
|
|
onChange={(e) =>
|
|
change({
|
|
incomeIncluded: e.target
|
|
.value as '' | 'yes' | 'no',
|
|
})
|
|
}
|
|
>
|
|
<option value="">
|
|
Informação em falta
|
|
</option>
|
|
<option value="yes">
|
|
Sim — documentado
|
|
</option>
|
|
<option value="no">
|
|
Não — crédito não aplicável neste
|
|
fecho
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label className="block">
|
|
Suporte da titularidade e inclusão
|
|
<input
|
|
aria-label={
|
|
'Suporte da retenção L8 ' +
|
|
(index + 1)
|
|
}
|
|
value={r.reference}
|
|
onChange={(e) =>
|
|
change({
|
|
reference: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
<FiscalProofParcelFields
|
|
value={r}
|
|
change={change}
|
|
label={'Retenção L8 ' + (index + 1)}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
patch({
|
|
withholdings: {
|
|
...b.withholdings,
|
|
rows: b.withholdings.rows.filter(
|
|
(x) => x.id !== r.id,
|
|
),
|
|
},
|
|
})
|
|
}
|
|
>
|
|
Retirar selecção L8 da retenção {index + 1}
|
|
</button>
|
|
</fieldset>
|
|
);
|
|
})}
|
|
</fieldset>
|
|
<fieldset className="space-y-2 rounded border p-3">
|
|
<legend>PPC efectivamente pagos</legend>
|
|
{group('ppc', 'PPC efectuados')}
|
|
<p>
|
|
Regista comprovativos de pagamentos por conta; não
|
|
calcula automaticamente a obrigação PPC nem usa
|
|
pagamentos previstos no plano.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
patch({
|
|
ppc: {
|
|
...b.ppc,
|
|
rows: [
|
|
...b.ppc.rows,
|
|
{
|
|
...newAnnualPpc(
|
|
crypto.randomUUID(),
|
|
),
|
|
subjectNuit: b.subjectNuit,
|
|
year: b.year,
|
|
},
|
|
],
|
|
},
|
|
})
|
|
}
|
|
>
|
|
Registar comprovativo PPC
|
|
</button>
|
|
{b.ppc.rows.map((r, index) => {
|
|
const change = (p: Partial<AnnualPpc>) =>
|
|
patch({
|
|
ppc: {
|
|
...b.ppc,
|
|
rows: b.ppc.rows.map((x) =>
|
|
x.id === r.id ? { ...x, ...p } : x,
|
|
),
|
|
},
|
|
});
|
|
return (
|
|
<fieldset key={r.id} className="rounded border p-2">
|
|
<legend>PPC {index + 1}</legend>
|
|
{(
|
|
[
|
|
['document', 'Comprovativo'],
|
|
['issuer', 'Emitente'],
|
|
['date', 'Data (AAAA-MM-DD)'],
|
|
['subjectNuit', 'NUIT beneficiário'],
|
|
['year', 'Exercício do comprovativo'],
|
|
['currency', 'Moeda do comprovativo'],
|
|
['proofAmount', 'Montante comprovado'],
|
|
['amount', 'Parcela a abater'],
|
|
[
|
|
'reference',
|
|
'Suporte do pagamento e afectação',
|
|
],
|
|
] as const
|
|
).map(([key, label]) => (
|
|
<label key={key} className="block">
|
|
{label}
|
|
<input
|
|
aria-label={
|
|
'PPC L8 ' +
|
|
(index + 1) +
|
|
' — ' +
|
|
label
|
|
}
|
|
value={r[key]}
|
|
onChange={(e) =>
|
|
change({
|
|
[key]: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
))}
|
|
<label className="block">
|
|
Pagamento efectivamente comprovado
|
|
<select
|
|
aria-label={
|
|
'PPC L8 ' +
|
|
(index + 1) +
|
|
' — Efectivo'
|
|
}
|
|
value={r.effective}
|
|
onChange={(e) =>
|
|
change({
|
|
effective: e.target.value as
|
|
| ''
|
|
| 'yes'
|
|
| 'no',
|
|
})
|
|
}
|
|
>
|
|
<option value="">
|
|
Informação em falta
|
|
</option>
|
|
<option value="yes">
|
|
Sim — comprovativo reconciliado
|
|
</option>
|
|
<option value="no">
|
|
Não — apenas previsão/outro estado
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<FiscalProofParcelFields
|
|
value={r}
|
|
change={change}
|
|
label={'PPC L8 ' + (index + 1)}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
patch({
|
|
ppc: {
|
|
...b.ppc,
|
|
rows: b.ppc.rows.filter(
|
|
(x) => x.id !== r.id,
|
|
),
|
|
},
|
|
})
|
|
}
|
|
>
|
|
Retirar PPC {index + 1}
|
|
</button>
|
|
</fieldset>
|
|
);
|
|
})}
|
|
</fieldset>
|
|
<SimulationIrpcEconomic
|
|
book={b}
|
|
save={save}
|
|
residentConfirmed={
|
|
a.current &&
|
|
a.book.scope === 'resident_general' &&
|
|
b.subjectNuit === a.subjectNuit &&
|
|
b.year === a.year &&
|
|
b.assessmentKey === a.key
|
|
}
|
|
/>
|
|
<SimulationIrpcInternational book={b} save={save} />
|
|
<SimulationIrpcInvestment draft={draft} book={b} save={save} />
|
|
<SimulationIrpcPec book={b} save={save} />
|
|
<SimulationIrpcCarry book={b} save={save} />
|
|
{[
|
|
...(b.pecPayments ? [] : [['pec', 'PEC'] as const]),
|
|
...(b.internationalCredits
|
|
? []
|
|
: [
|
|
[
|
|
'credits',
|
|
'Créditos com regras próprias',
|
|
] as const,
|
|
]),
|
|
].map(([key, label]) => (
|
|
<fieldset key={key} className="rounded border p-3">
|
|
<legend>{label} — tratamento separado</legend>
|
|
<label className="block">
|
|
Estado — {label}
|
|
<select
|
|
aria-label={'Estado L8 — ' + label}
|
|
value={b[key].state}
|
|
onChange={(e) =>
|
|
patch({
|
|
[key]: {
|
|
...b[key],
|
|
state: e.target.value as
|
|
| ''
|
|
| 'known'
|
|
| 'absent',
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<option value="">Informação em falta</option>
|
|
<option value="absent">
|
|
Ausência declarada com suporte
|
|
</option>
|
|
<option value="known">
|
|
Valor conhecido — positivo exige regra
|
|
própria
|
|
</option>
|
|
</select>
|
|
</label>
|
|
{(b[key].state === 'known' || b[key].amount) && (
|
|
<label className="block">
|
|
Montante declarado
|
|
<input
|
|
aria-label={'Montante L8 — ' + label}
|
|
value={b[key].amount}
|
|
onChange={(e) =>
|
|
patch({
|
|
[key]: {
|
|
...b[key],
|
|
amount: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
<label className="block">
|
|
Suporte
|
|
<input
|
|
aria-label={'Suporte L8 — ' + label}
|
|
value={b[key].reference}
|
|
onChange={(e) =>
|
|
patch({
|
|
[key]: {
|
|
...b[key],
|
|
reference: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
</fieldset>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
try {
|
|
save(recordAnnual(draft, b));
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Calcular e guardar saldo anual L8
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
try {
|
|
const html = annualHtml(draft),
|
|
url = URL.createObjectURL(
|
|
new Blob([html], {
|
|
type: 'text/html;charset=utf-8',
|
|
}),
|
|
),
|
|
link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = 'IRPC-L8-saldo-simulado.html';
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
setError('');
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Exportar saldo anual L8
|
|
</button>
|
|
{annualReportRows(draft).map((r, n) => (
|
|
<p key={n}>
|
|
{r[1]} — {r[2]} — {r[3]}
|
|
</p>
|
|
))}
|
|
</section>
|
|
);
|
|
} catch (e) {
|
|
return (
|
|
<p role="alert">
|
|
PreparaçãoL8 não recuperável:{String(e)}. Originais e históricos
|
|
conservados.
|
|
</p>
|
|
);
|
|
}
|
|
}
|