610 lines
26 KiB
TypeScript
610 lines
26 KiB
TypeScript
import { inputText } from '../lib/input-text.ts';
|
|
import {
|
|
newIrpcInterestRule,
|
|
type IrpcInterestRule,
|
|
} from '@/lib/irpc-interest';
|
|
import { mrftaCases, mrftaCaseHtml } from '@/lib/irpc-mrfta-cases';
|
|
import { useState } from 'react';
|
|
import { irpcCurrent, type IrpcBook } from '@/lib/irpc-withholdings';
|
|
import {
|
|
newMrftaInput,
|
|
recordMrfta,
|
|
mrftaHtml,
|
|
mrftaReportRows,
|
|
mrftaCurrent,
|
|
type MrftaInput,
|
|
} from '@/lib/irpc-mrfta';
|
|
|
|
export function SimulationIrpcMrfta({
|
|
draft,
|
|
book,
|
|
save,
|
|
}: {
|
|
draft: Record<string, unknown>;
|
|
book: IrpcBook;
|
|
save: (book: IrpcBook) => void;
|
|
}) {
|
|
const [error, setError] = useState(''),
|
|
i = book.mrfta ?? newMrftaInput();
|
|
const change = (patch: Partial<MrftaInput>) =>
|
|
save({ ...book, mrfta: { ...i, ...patch } });
|
|
const interestMode =
|
|
i.interest.mode ||
|
|
(i.interest.calculation === 'automatic' ? 'automatic' : '');
|
|
const interest = (patch: Partial<MrftaInput['interest']>) =>
|
|
change({ interest: { ...i.interest, ...patch } });
|
|
const field = (
|
|
key:
|
|
| 'period'
|
|
| 'nuit'
|
|
| 'name'
|
|
| 'commercialName'
|
|
| 'taxArea'
|
|
| 'address'
|
|
| 'contact'
|
|
| 'completenessRef'
|
|
| 'otherRubricsRef',
|
|
label: string,
|
|
type = 'text',
|
|
) => (
|
|
<label className="block">
|
|
{label}
|
|
<input
|
|
className="block rounded border p-2"
|
|
type={type}
|
|
value={i[key]}
|
|
onChange={(e) => change({ [key]: e.target.value })}
|
|
/>
|
|
</label>
|
|
);
|
|
const select = (
|
|
key: 'declaration' | 'timing' | 'completeness' | 'otherRubrics',
|
|
label: string,
|
|
options: Array<[string, string]>,
|
|
) => (
|
|
<label className="block">
|
|
{label}
|
|
<select
|
|
value={i[key]}
|
|
onChange={(e) => change({ [key]: e.target.value })}
|
|
>
|
|
{options.map(([v, l]) => (
|
|
<option key={v} value={v}>
|
|
{l}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
);
|
|
const periods = [
|
|
...new Set(
|
|
book.result?.rows.map((r) => r.period).filter(Boolean) ?? [],
|
|
),
|
|
];
|
|
const identities = Object.entries(
|
|
(JSON.parse(inputText(draft.modelo_a_snapshot || '{}'))
|
|
.irpcWithholdings ?? {}) as Record<string, IrpcBook>,
|
|
).filter(
|
|
([, b]) =>
|
|
b.mrfta?.nuit &&
|
|
b.mrfta?.name &&
|
|
(!i.nuit.trim() || i.nuit === b.mrfta.nuit) &&
|
|
(!i.name.trim() || i.name === b.mrfta.name) &&
|
|
(
|
|
[
|
|
'nuit',
|
|
'name',
|
|
'commercialName',
|
|
'taxArea',
|
|
'address',
|
|
'contact',
|
|
] as const
|
|
).some((key) => !i[key].trim() && b.mrfta![key].trim()),
|
|
);
|
|
return (
|
|
<fieldset
|
|
aria-label="Preparação MRFTA e juros"
|
|
className="space-y-3 rounded border p-4"
|
|
>
|
|
<legend>MRFTA — preenchimento e juros compensatórios</legend>
|
|
<p>
|
|
Reutiliza o cálculo guardado, documentos e entregas IRPC. Um
|
|
mês/ano por declaração. A retenção sofrida e as dispensas ficam
|
|
separadas. Quadros 8/9 reservados à assinatura e aos serviços;
|
|
não há submissão ou aceitação AT.
|
|
</p>
|
|
{identities.map(([scope, b]) => (
|
|
<button
|
|
key={scope}
|
|
type="button"
|
|
onClick={() => {
|
|
const p: Partial<MrftaInput> = {};
|
|
for (const key of [
|
|
'nuit',
|
|
'name',
|
|
'commercialName',
|
|
'taxArea',
|
|
'address',
|
|
'contact',
|
|
] as const)
|
|
if (!i[key].trim()) p[key] = b.mrfta![key];
|
|
change(p);
|
|
}}
|
|
>
|
|
Reutilizar identidade fiscal {b.mrfta!.name} ({scope}) nos
|
|
campos vazios
|
|
</button>
|
|
))}
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
{field('period', 'Quadro 2 — mês/ano', 'month')}
|
|
<label>
|
|
Período existente
|
|
<select
|
|
value=""
|
|
onChange={(e) => {
|
|
if (e.target.value)
|
|
change({ period: e.target.value });
|
|
}}
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{periods.map((p) => (
|
|
<option key={p} value={p}>
|
|
{p}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
{select('declaration', 'Quadro 1 — retenção na fonte', [
|
|
['', 'Por identificar'],
|
|
['initial', 'Inicial'],
|
|
['replacement', 'Substituição'],
|
|
])}
|
|
{select('timing', 'Quadro 2 — situação do prazo', [
|
|
['', 'Por esclarecer'],
|
|
['within', 'Dentro do prazo'],
|
|
['late', 'Fora do prazo'],
|
|
])}
|
|
{field('nuit', 'Quadro 3 — NUIT')}
|
|
{field('taxArea', 'Quadro 3 — área fiscal')}
|
|
{field('name', 'Quadro 4 — nome do sujeito passivo')}
|
|
{field('commercialName', 'Quadro 5 — nome comercial')}
|
|
{field('address', 'Quadro 6 — domicílio fiscal')}
|
|
{field('contact', 'Quadro 6 — contacto')}
|
|
{select(
|
|
'completeness',
|
|
'Retenções do sujeito passivo neste período conferidas, incluindo demais livros',
|
|
[
|
|
['', 'Por esclarecer'],
|
|
['yes', 'Sim'],
|
|
['no', 'Ainda não'],
|
|
],
|
|
)}
|
|
{field(
|
|
'completenessRef',
|
|
'Referência da reconciliação do período',
|
|
)}
|
|
{select(
|
|
'otherRubrics',
|
|
'IRPS, tributação autónoma e outras rubricas fora deste recorte',
|
|
[
|
|
['', 'Por esclarecer'],
|
|
[
|
|
'none',
|
|
'Ausência de outros valores devidos documentada',
|
|
],
|
|
['pending', 'Existem ou estão por apurar'],
|
|
],
|
|
)}
|
|
{field(
|
|
'otherRubricsRef',
|
|
'Documento da situação das outras rubricas',
|
|
)}
|
|
</div>
|
|
<h5>Quadro 7 — CER das rubricas correspondidas</h5>
|
|
<p>
|
|
Base, taxa e imposto vêm do cálculo guardado. O CER é transcrito
|
|
da referência aplicável; os números de campos da guia antiga não
|
|
são automaticamente códigos de receita. Códigos introduzidos não
|
|
são validados por catálogo oficial; códigos provisórios ou de
|
|
simulação não são correspondências oficiais.
|
|
</p>
|
|
{book.result?.inputs
|
|
.filter(
|
|
(r) =>
|
|
r.direction === 'paid' &&
|
|
book.result?.rows.some(
|
|
(v) =>
|
|
v.id === r.id &&
|
|
v.period === i.period &&
|
|
v.status === 'calculated' &&
|
|
v.treatment !== 'dispensed',
|
|
),
|
|
)
|
|
.map((r) => {
|
|
const code = i.codes.find((c) => c.rowId === r.id) ?? {
|
|
rowId: r.id,
|
|
cer: '',
|
|
reference: '',
|
|
};
|
|
const update = (p: Partial<typeof code>) =>
|
|
change({
|
|
codes: [
|
|
...i.codes.filter((c) => c.rowId !== r.id),
|
|
{ ...code, ...p },
|
|
],
|
|
});
|
|
return (
|
|
<div key={r.id} className="grid gap-2 md:grid-cols-3">
|
|
<span>
|
|
{r.documentRef} — {r.counterparty}
|
|
</span>
|
|
<label>
|
|
CER
|
|
<input
|
|
value={code.cer}
|
|
onChange={(e) =>
|
|
update({ cer: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Documento / referência do CER
|
|
<input
|
|
value={code.reference}
|
|
onChange={(e) =>
|
|
update({ reference: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
</div>
|
|
);
|
|
})}
|
|
<h5>Juros compensatórios — separados do principal</h5>
|
|
<p>
|
|
Regulamento IRPC, artigos 25/35: a taxa refere MAIBOR de 12
|
|
meses + 2 pontos. O Banco de Moçambique documenta a
|
|
descontinuidade da MAIBOR. O portal não adopta MIMO ou Prime
|
|
como substituto nem calcula juros de mora.
|
|
</p>
|
|
<label>
|
|
Tratamento dos juros
|
|
<select
|
|
value={interestMode}
|
|
onChange={(e) =>
|
|
interest(
|
|
e.target.value === 'automatic'
|
|
? { mode: '', calculation: 'automatic' }
|
|
: {
|
|
mode: e.target
|
|
.value as MrftaInput['interest']['mode'],
|
|
calculation: undefined,
|
|
},
|
|
)
|
|
}
|
|
>
|
|
<option value="">
|
|
Informação insuficiente / cálculo automático por
|
|
resolver
|
|
</option>
|
|
<option value="assessment">
|
|
Transcrever liquidação documentada de juros
|
|
compensatórios
|
|
</option>
|
|
<option value="automatic">
|
|
Cálculo com taxa e regra documentadas — sem parâmetro
|
|
pré-definido
|
|
</option>
|
|
<option value="timely">
|
|
Conferir entrega integral dentro do prazo documentado
|
|
</option>
|
|
</select>
|
|
</label>
|
|
{interestMode && (
|
|
<>
|
|
<label>
|
|
Período documentado dos juros / entregas
|
|
<input
|
|
type="month"
|
|
value={i.interest.period}
|
|
onChange={(e) =>
|
|
interest({ period: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Documento da liquidação / reconciliação
|
|
<input
|
|
value={i.interest.reference}
|
|
onChange={(e) =>
|
|
interest({ reference: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
</>
|
|
)}
|
|
{['assessment', 'automatic'].includes(interestMode) && (
|
|
<>
|
|
{i.interest.mode === 'assessment' && (
|
|
<label>
|
|
Montante dos juros em MZN
|
|
<input
|
|
value={i.interest.amount}
|
|
onChange={(e) =>
|
|
interest({ amount: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
)}
|
|
<label>
|
|
Data da liquidação
|
|
<input
|
|
type="date"
|
|
value={i.interest.date}
|
|
onChange={(e) => interest({ date: e.target.value })}
|
|
/>
|
|
</label>
|
|
</>
|
|
)}
|
|
{['timely', 'automatic'].includes(interestMode) && (
|
|
<>
|
|
<label>
|
|
Prazo aplicável documentado
|
|
<input
|
|
type="date"
|
|
value={i.interest.dueDate}
|
|
onChange={(e) =>
|
|
interest({ dueDate: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Fonte do prazo, incluindo eventual ajuste de calendário
|
|
<input
|
|
value={i.interest.dueReference}
|
|
onChange={(e) =>
|
|
interest({ dueReference: e.target.value })
|
|
}
|
|
/>
|
|
</label>
|
|
</>
|
|
)}
|
|
{interestMode === 'automatic' && (
|
|
<fieldset className="space-y-2 rounded border p-2">
|
|
<legend>
|
|
Parâmetros documentados do cálculo — sem taxa fiscal
|
|
de2026 pré-definida
|
|
</legend>
|
|
<p>
|
|
Simulação pelos pressupostos introduzidos, sem
|
|
confirmação fiscal dos juros. Preencha apenas a regra
|
|
aplicável ao caso. Sem parâmetro completo, os juros
|
|
continuam indeterminados. A taxa bancária actual não é
|
|
adoptada por substituição. Imposto por conta não retido
|
|
e juros de mora mantêm regra própria.
|
|
</p>
|
|
{(
|
|
[
|
|
'annualRate',
|
|
'rateSource',
|
|
'fiscalBasis',
|
|
'validFrom',
|
|
'validTo',
|
|
'dayBasisRef',
|
|
'roundingRef',
|
|
'occurrenceRef',
|
|
] as const
|
|
).map((k) => (
|
|
<label key={k} className="block">
|
|
{
|
|
{
|
|
annualRate:
|
|
'Taxa anual final (%) documentada, incluindo acréscimos aplicáveis',
|
|
rateSource: 'Documento e citação da taxa',
|
|
fiscalBasis:
|
|
'Fundamento da aplicação fiscal ao caso',
|
|
validFrom: 'Parâmetro aplicável desde',
|
|
validTo: 'Parâmetro aplicável até',
|
|
dayBasisRef:
|
|
'Documento da base anual e contagem diária',
|
|
roundingRef:
|
|
'Documento do arredondamento do total',
|
|
occurrenceRef:
|
|
'Documento da retenção e ocorrência',
|
|
}[k]
|
|
}
|
|
<input
|
|
type={
|
|
k === 'validFrom' || k === 'validTo'
|
|
? 'date'
|
|
: 'text'
|
|
}
|
|
value={i.interest.automatic?.[k] ?? ''}
|
|
onChange={(e) =>
|
|
interest({
|
|
automatic: {
|
|
...(i.interest.automatic ??
|
|
newIrpcInterestRule()),
|
|
[k]: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
))}
|
|
{(['dayBasis', 'rounding', 'retention'] as const).map(
|
|
(k) => (
|
|
<label key={k} className="block">
|
|
{
|
|
{
|
|
dayBasis:
|
|
'Base anual conforme documento',
|
|
rounding:
|
|
'Arredondamento do total conforme documento',
|
|
retention: 'Retenção efectuada',
|
|
}[k]
|
|
}
|
|
<select
|
|
value={i.interest.automatic?.[k] ?? ''}
|
|
onChange={(e) =>
|
|
interest({
|
|
automatic: {
|
|
...(i.interest.automatic ??
|
|
newIrpcInterestRule()),
|
|
[k]: e.target.value,
|
|
} as IrpcInterestRule,
|
|
})
|
|
}
|
|
>
|
|
{(k === 'dayBasis'
|
|
? [
|
|
['', 'Por identificar'],
|
|
['365', '365 dias'],
|
|
['366', '366 dias'],
|
|
[
|
|
'actual_year',
|
|
'Dias efectivos de cada ano',
|
|
],
|
|
]
|
|
: k === 'rounding'
|
|
? [
|
|
['', 'Por identificar'],
|
|
[
|
|
'total_cent_half_up',
|
|
'Total aos cêntimos — metade por excesso',
|
|
],
|
|
[
|
|
'total_cent_down',
|
|
'Total aos cêntimos — por defeito',
|
|
],
|
|
]
|
|
: [
|
|
['', 'Por identificar'],
|
|
[
|
|
'withheld',
|
|
'Sim, documentada',
|
|
],
|
|
[
|
|
'not_withheld',
|
|
'Não efectuada, documentado',
|
|
],
|
|
]
|
|
).map(([v, l]) => (
|
|
<option key={v} value={v}>
|
|
{l}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
),
|
|
)}
|
|
</fieldset>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="rounded border p-2"
|
|
onClick={() => {
|
|
try {
|
|
save(recordMrfta(book, irpcCurrent(draft, book)));
|
|
setError('');
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Preparar e guardar MRFTA
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="rounded border p-2"
|
|
onClick={() => {
|
|
try {
|
|
const html = mrftaHtml(book, irpcCurrent(draft, book)),
|
|
u = URL.createObjectURL(
|
|
new Blob([html], {
|
|
type: 'text/html;charset=utf-8',
|
|
}),
|
|
),
|
|
a = document.createElement('a');
|
|
a.href = u;
|
|
a.download =
|
|
'MRFTA-' +
|
|
(i.period || 'periodo') +
|
|
'-preparacao.html';
|
|
a.click();
|
|
URL.revokeObjectURL(u);
|
|
setError('');
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Exportar preenchimento MRFTA
|
|
</button>
|
|
{book.mrftaResult && (
|
|
<fieldset className="space-y-2 rounded border p-2">
|
|
<legend>
|
|
Casos de trabalho separados por período, rubrica, taxa e
|
|
CER
|
|
</legend>
|
|
<p>
|
|
Estas fichas conservam as pendências do período. Não são
|
|
declarações autónomas, anexos oficiais ou uma
|
|
autorização para fundir taxas.
|
|
</p>
|
|
{mrftaCases(book.mrftaResult.model).map((c) => (
|
|
<div key={c.key}>
|
|
<span>
|
|
{c.period}; {c.rubric}; {c.rate}%; CER:
|
|
{c.cer || 'por corresponder'}; documentos:
|
|
{c.documents.join('; ')}.
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
try {
|
|
if (
|
|
!irpcCurrent(draft, book) ||
|
|
!mrftaCurrent(book, true)
|
|
)
|
|
throw Error(
|
|
'Reprepare as entradas actuais antes de exportar o caso.',
|
|
);
|
|
const html = mrftaCaseHtml(
|
|
book.mrftaResult!.model,
|
|
c.key,
|
|
),
|
|
u = URL.createObjectURL(
|
|
new Blob([html], {
|
|
type: 'text/html;charset=utf-8',
|
|
}),
|
|
),
|
|
a = document.createElement('a');
|
|
a.href = u;
|
|
a.download =
|
|
'MRFTA-caso-' + c.key + '.html';
|
|
a.click();
|
|
URL.revokeObjectURL(u);
|
|
setError('');
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Exportar ficha de trabalho {c.rate}% /{' '}
|
|
{c.cer || 'CER pendente'}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</fieldset>
|
|
)}
|
|
{error && <p role="alert">{error}</p>}
|
|
{mrftaReportRows(book, irpcCurrent(draft, book)).map((r, n) => (
|
|
<p key={n}>
|
|
<strong>{r[1]}</strong>: {r[2]}
|
|
<small>{r[3]}</small>
|
|
</p>
|
|
))}
|
|
</fieldset>
|
|
);
|
|
}
|