Files
Investidor-V2/resources/js/components/simulation-ispc-residuals.tsx
T

568 lines
25 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 { 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>;
}
}