568 lines
25 KiB
TypeScript
568 lines
25 KiB
TypeScript
import { useState } from 'react';
|
||
import { ispcEligibilityBook } from '@/lib/ispc-eligibility';
|
||
import { ispcWithholdingBook } from '@/lib/ispc-withholding';
|
||
import { decimalFraction, addFractions } from '@/lib/fiscal-rational';
|
||
import {
|
||
ispcResidualBook,
|
||
saveIspcResidualBook,
|
||
recordIspcResidual,
|
||
priorOccasionalSettlement,
|
||
ispcResidualReportRows,
|
||
ispcFormalPreparationHtml,
|
||
type ResidualKind,
|
||
} from '@/lib/ispc-residuals';
|
||
export function SimulationIspcResiduals({
|
||
draft,
|
||
onChange,
|
||
}: {
|
||
draft: Record<string, unknown>;
|
||
onChange: (field: string, value: string) => void;
|
||
}) {
|
||
const [error, setError] = useState('');
|
||
if (draft.fiscal_component !== 'ispc') return null;
|
||
try {
|
||
const own = ispcResidualBook(draft, 'occasional'),
|
||
acq = ispcResidualBook(draft, 'acquisitions'),
|
||
formal = ispcResidualBook(draft, 'formalities'),
|
||
e = ispcEligibilityBook(draft),
|
||
w = ispcWithholdingBook(draft);
|
||
let above = false;
|
||
try {
|
||
const sum = addFractions([
|
||
decimalFraction(w.input.base),
|
||
decimalFraction(w.input.priorAnnualPurchases),
|
||
]);
|
||
above = BigInt(sum.numerator) > 2500000n * BigInt(sum.denominator);
|
||
} catch {
|
||
/* Unknown is still handled in existing acquisition panel. */
|
||
}
|
||
const book = (k: ResidualKind) =>
|
||
k === 'occasional' ? own : k === 'acquisitions' ? acq : formal;
|
||
const change = (k: ResidualKind, p: Record<string, string>) => {
|
||
const b = book(k);
|
||
onChange(
|
||
'modelo_a_snapshot',
|
||
saveIspcResidualBook(draft, {
|
||
...b,
|
||
input: { ...b.input, ...p },
|
||
}),
|
||
);
|
||
};
|
||
const field = (
|
||
k: ResidualKind,
|
||
n: string,
|
||
label: string,
|
||
type = 'text',
|
||
) => (
|
||
<label key={n} className="block">
|
||
{label}
|
||
<input
|
||
className="block rounded border p-2"
|
||
type={type}
|
||
value={book(k).input[n] ?? ''}
|
||
onChange={(ev) => change(k, { [n]: ev.target.value })}
|
||
/>
|
||
</label>
|
||
);
|
||
const select = (
|
||
k: ResidualKind,
|
||
n: string,
|
||
label: string,
|
||
options: Array<[string, string]>,
|
||
) => (
|
||
<label key={n} className="block">
|
||
{label}
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={book(k).input[n] ?? ''}
|
||
onChange={(ev) => change(k, { [n]: ev.target.value })}
|
||
>
|
||
{options.map(([v, l]) => (
|
||
<option key={v} value={v}>
|
||
{l}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
);
|
||
const yesno = (k: ResidualKind, n: string, label: string) =>
|
||
select(k, n, label, [
|
||
['', 'Por esclarecer'],
|
||
['yes', 'Sim'],
|
||
['no', 'Não'],
|
||
]);
|
||
const save = (k: ResidualKind) => {
|
||
try {
|
||
onChange(
|
||
'modelo_a_snapshot',
|
||
saveIspcResidualBook(
|
||
draft,
|
||
recordIspcResidual(draft, book(k)),
|
||
),
|
||
);
|
||
setError('');
|
||
} catch (err) {
|
||
setError(String(err));
|
||
}
|
||
};
|
||
const activities: Array<[string, string]> = [
|
||
['', 'Por identificar'],
|
||
['forestry', 'Silvícola'],
|
||
['fishery', 'Pesqueira'],
|
||
['livestock', 'Pecuária'],
|
||
['agriculture', 'Agrícola'],
|
||
['poultry', 'Avícola'],
|
||
['beekeeping', 'Apícola'],
|
||
['other', 'Outra—fora do recorte ocasional'],
|
||
];
|
||
return (
|
||
<section
|
||
aria-label="ISPC — operações ocasionais, limite e formalidades"
|
||
className="space-y-4"
|
||
>
|
||
{error && <p role="alert">{error}</p>}
|
||
{e.input.mode === 'occasional' && (
|
||
<section className="space-y-3 rounded border p-4">
|
||
<h4>Operações ocasionais próprias</h4>
|
||
<p>
|
||
Percurso do vendedor ocasional, distinto da retenção
|
||
feita pelo comprador. Uma retenção liberatória
|
||
comprovada não gera segunda liquidação própria.
|
||
</p>
|
||
{select(
|
||
'occasional',
|
||
'personType',
|
||
'Tipo de pessoa destas operações',
|
||
[
|
||
['', 'Por identificar'],
|
||
['singular', 'Pessoa singular'],
|
||
['collective', 'Pessoa colectiva'],
|
||
],
|
||
)}
|
||
{select('occasional', 'origin', 'Natureza dos dados', [
|
||
['projected', 'Pressupostos de simulação'],
|
||
['effective', 'Factos efectivos declarados'],
|
||
])}
|
||
{field('occasional', 'currency', 'Moeda—MZN')}
|
||
{field(
|
||
'occasional',
|
||
'operationDate',
|
||
'Data das operações',
|
||
'date',
|
||
)}
|
||
{select('occasional', 'quarter', 'Trimestre2026', [
|
||
['', 'Por identificar'],
|
||
['1', '1'],
|
||
['2', '2'],
|
||
['3', '3'],
|
||
['4', '4'],
|
||
])}
|
||
{yesno(
|
||
'occasional',
|
||
'territory',
|
||
'Operação realizada em território nacional',
|
||
)}
|
||
{yesno(
|
||
'occasional',
|
||
'noHabitual',
|
||
'Não exerce actividade habitual',
|
||
)}
|
||
{yesno(
|
||
'occasional',
|
||
'sporadic',
|
||
'Acto não previsível nem reiterado',
|
||
)}
|
||
{select(
|
||
'occasional',
|
||
'activity',
|
||
'Natureza da operação',
|
||
activities,
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'reference',
|
||
'Documentos ou pressupostos da operação',
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'amount',
|
||
'Volume de todas estas operações do trimestre em MZN',
|
||
)}
|
||
{select('occasional', 'settlement', 'Via aplicável', [
|
||
['', 'Por identificar'],
|
||
['self', 'Liquidação própria por opção ISPC'],
|
||
[
|
||
'withheld',
|
||
'Retenção liberatória documentada pelo adquirente',
|
||
],
|
||
])}
|
||
{own.input.settlement === 'self' && (
|
||
<>
|
||
{yesno(
|
||
'occasional',
|
||
'allQuarterOperations',
|
||
'Inclui todas as operações ocasionais do sujeito passivo no trimestre',
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'priorYearTurnover',
|
||
'Volume de referência2025 para acesso, incluindo zero expresso',
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'priorYearRef',
|
||
'Suporte da referência2025',
|
||
)}
|
||
{yesno(
|
||
'occasional',
|
||
'organizedRequired',
|
||
'Obrigado a contabilidade organizada para impostos sobre rendimento',
|
||
)}
|
||
{select(
|
||
'occasional',
|
||
'option',
|
||
'Opção para estas operações',
|
||
[
|
||
[
|
||
'preparation',
|
||
'Em preparação—apenas cenário',
|
||
],
|
||
['opted', 'Opção documentada'],
|
||
['not_opted', 'Não optou'],
|
||
],
|
||
)}
|
||
{own.input.option === 'opted' && (
|
||
<>
|
||
{field(
|
||
'occasional',
|
||
'optionRef',
|
||
'Referência da opção',
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'optionDate',
|
||
'Data da opção',
|
||
'date',
|
||
)}
|
||
</>
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'annual2026',
|
||
'Volume anual2026 para escalão',
|
||
)}
|
||
{select(
|
||
'occasional',
|
||
'annualOrigin',
|
||
'Origem do volume anual2026',
|
||
[
|
||
['', 'Por identificar'],
|
||
['forecast', 'Previsão—cenário'],
|
||
[
|
||
'closed_year',
|
||
'Volume anual encerrado documentado',
|
||
],
|
||
],
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'annualRef',
|
||
'Suporte ou pressuposto do volume anual2026',
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'priorInYear',
|
||
'Volume de operações anteriores no ano2026',
|
||
)}
|
||
{priorOccasionalSettlement(own) && (
|
||
<>
|
||
<p>
|
||
Existe retenção liberatória guardada
|
||
para esta operação. A mudança de via
|
||
não a apaga nem gera uma segunda
|
||
prestação.
|
||
</p>
|
||
{yesno(
|
||
'occasional',
|
||
'withholdingCorrected',
|
||
'Retenção anterior corrigida/anulada segundo documento externo',
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'withholdingCorrectionRef',
|
||
'Referência externa da correcção da retenção anterior',
|
||
)}
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
{own.input.settlement === 'withheld' && (
|
||
<>
|
||
{yesno(
|
||
'occasional',
|
||
'buyerIspc',
|
||
'Adquirente sujeito passivo ISPC',
|
||
)}
|
||
{yesno(
|
||
'occasional',
|
||
'unregistered',
|
||
'Vendedor não inscrito para tributação',
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'buyerAnnualPurchases',
|
||
'Aquisições globais anuais do adquirente, incluindo estas operações',
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'withheldActual',
|
||
'Montante efectivamente retido em MZN',
|
||
)}
|
||
{field(
|
||
'occasional',
|
||
'withheldRef',
|
||
'Documento da retenção liberatória',
|
||
)}
|
||
</>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className="rounded border p-2"
|
||
onClick={() => save('occasional')}
|
||
>
|
||
Calcular e guardar operações ocasionais
|
||
</button>
|
||
</section>
|
||
)}
|
||
{(above || acq.result) && (
|
||
<section className="space-y-3 rounded border p-4">
|
||
<h4>Aquisições acima do limite global</h4>
|
||
<p>
|
||
Reutiliza a aquisição, os seus documentos e o
|
||
acumulado global guardados acima. A parcela
|
||
excedente não recebe automaticamente a taxa de 5%
|
||
nem a taxa de 20% do excesso de vendas ISPC.
|
||
</p>
|
||
{select(
|
||
'acquisitions',
|
||
'route',
|
||
'Tratamento próprio da parcela excedente',
|
||
[
|
||
[
|
||
'unknown',
|
||
'Por identificar—mostrar parcelas sem total presumido',
|
||
],
|
||
[
|
||
'ispc_supplier',
|
||
'Fornecedor com opção ISPC aplicável',
|
||
],
|
||
[
|
||
'irps_isolated',
|
||
'Acto isolado sujeito a IRPS57(2)k',
|
||
],
|
||
],
|
||
)}
|
||
{acq.input.route === 'ispc_supplier' &&
|
||
field(
|
||
'acquisitions',
|
||
'supplierOptionRef',
|
||
'Referência da opção/registo do fornecedor e operações abrangidas',
|
||
)}
|
||
{acq.input.route === 'irps_isolated' && (
|
||
<>
|
||
{yesno(
|
||
'acquisitions',
|
||
'noIspcOnExcess',
|
||
'Sem opção ISPC aplicável a esta parcela, segundo documento indicado',
|
||
)}
|
||
{yesno(
|
||
'acquisitions',
|
||
'isolatedAct',
|
||
'Acto isolado qualificado pelo artigo8(3h,4)doIRPS',
|
||
)}
|
||
{yesno(
|
||
'acquisitions',
|
||
'incomeInMoz',
|
||
'Rendimento obtido em Moçambique',
|
||
)}
|
||
{field(
|
||
'acquisitions',
|
||
'grossExcess',
|
||
'Rendimento ilíquido da parcela excedente em MZN—sem dedução de custos ou IVA',
|
||
)}
|
||
{field(
|
||
'acquisitions',
|
||
'grossRef',
|
||
'Suporte da base ilíquida',
|
||
)}
|
||
{field(
|
||
'acquisitions',
|
||
'reference',
|
||
'Suporte da classificação IRPS desta parcela',
|
||
)}
|
||
</>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className="rounded border p-2"
|
||
onClick={() => save('acquisitions')}
|
||
>
|
||
Calcular e guardar parcela excedente
|
||
</button>
|
||
</section>
|
||
)}
|
||
<section className="space-y-3 rounded border p-4">
|
||
<h4>Formalidades de2026—preparação do B–Geral</h4>
|
||
<p>
|
||
O Comunicado AT9/2026indica o B–Geral durante a
|
||
adaptação do sistema, com pagamentos em Janeiro, Abril,
|
||
Julho e Outubro. O Modelo30disponível conserva campos
|
||
antigos; não os aplicamos a2026. Esta preparação não
|
||
submete nem paga à AT.
|
||
</p>
|
||
{select('formalities', 'sourceKind', 'Cálculo a preparar', [
|
||
['quarter', 'Trimestral habitual'],
|
||
['occasional', 'Operações ocasionais próprias'],
|
||
[
|
||
'acquisitions',
|
||
'Retenção da parcela excedente qualificada',
|
||
],
|
||
])}
|
||
{formal.input.sourceKind === 'acquisitions' &&
|
||
field(
|
||
'formalities',
|
||
'retentionFormRef',
|
||
'Modelo/código e instrução para entrega das retenções ISPC/IRPS, separados do imposto próprio',
|
||
)}
|
||
{field(
|
||
'formalities',
|
||
'name',
|
||
'Nome/designação social—quadro5',
|
||
)}
|
||
{field('formalities', 'nuit', 'NUIT—quadro3')}
|
||
{field(
|
||
'formalities',
|
||
'fiscalArea',
|
||
'Área Fiscal/UGC—quadro3',
|
||
)}
|
||
{field(
|
||
'formalities',
|
||
'address',
|
||
'Domicílio fiscal—quadro6',
|
||
)}
|
||
{select(
|
||
'formalities',
|
||
'declaration',
|
||
'Tipo de declaração—quadro1',
|
||
[
|
||
['initial', 'Inicial'],
|
||
['substitution', 'Substituição'],
|
||
],
|
||
)}
|
||
{field(
|
||
'formalities',
|
||
'formMonth',
|
||
'Mês a inscrever no quadro2—não presumido pelo mês de pagamento',
|
||
)}
|
||
{field(
|
||
'formalities',
|
||
'formYear',
|
||
'Ano a inscrever no quadro2',
|
||
)}
|
||
{field(
|
||
'formalities',
|
||
'formPeriodRef',
|
||
'Instrução/correspondência usada para o período do quadro2',
|
||
)}
|
||
{select(
|
||
'formalities',
|
||
'interestSituation',
|
||
'Juros compensatórios—quadro10',
|
||
[
|
||
['', 'Por esclarecer'],
|
||
['none', 'Sem juros, segundo suporte indicado'],
|
||
['amount', 'Montante documentado'],
|
||
],
|
||
)}
|
||
{formal.input.interestSituation === 'amount' &&
|
||
field(
|
||
'formalities',
|
||
'interestAmount',
|
||
'Montante de juros documentado',
|
||
)}
|
||
{field(
|
||
'formalities',
|
||
'interestRef',
|
||
'Suporte da situação dos juros',
|
||
)}
|
||
{select(
|
||
'formalities',
|
||
'externalStatus',
|
||
'Histórico externo declarado',
|
||
[
|
||
['preparation', 'Em preparação'],
|
||
['submitted', 'Submetido'],
|
||
['received', 'Recebido pela AT'],
|
||
['paid', 'Pagamento efectuado'],
|
||
],
|
||
)}
|
||
{formal.input.externalStatus !== 'preparation' && (
|
||
<>
|
||
{field(
|
||
'formalities',
|
||
'externalRef',
|
||
'Referência externa',
|
||
)}
|
||
{field(
|
||
'formalities',
|
||
'externalDate',
|
||
'Data do acto externo',
|
||
'date',
|
||
)}
|
||
</>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className="rounded border p-2"
|
||
onClick={() => save('formalities')}
|
||
>
|
||
Preparar e guardar campos B–Geral
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="rounded border p-2"
|
||
onClick={() => {
|
||
try {
|
||
const html = ispcFormalPreparationHtml(draft),
|
||
url = URL.createObjectURL(
|
||
new Blob([html], {
|
||
type: 'text/html;charset=utf-8',
|
||
}),
|
||
),
|
||
a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'ISPC-preparacao-B-Geral.html';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
setError('');
|
||
} catch (err) {
|
||
setError(String(err));
|
||
}
|
||
}}
|
||
>
|
||
Descarregar preparação B–Geral
|
||
</button>
|
||
</section>
|
||
{ispcResidualReportRows(draft).map((r, n) => (
|
||
<p key={n}>
|
||
<strong>{r[1]}:</strong> {r[2]}
|
||
<small className="block">{r[3]}</small>
|
||
</p>
|
||
))}
|
||
</section>
|
||
);
|
||
} catch (err) {
|
||
return <p role="alert">{String(err)}</p>;
|
||
}
|
||
}
|