Files
Investidor-V2/resources/js/components/simulation-irpc-withholdings.tsx
T

1073 lines
54 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 { inputText } from '../lib/input-text.ts';
import { SimulationIrpcMrfta } from '@/components/simulation-irpc-mrfta';
import {
irpcReliefRules,
newIrpcReliefPreparation,
prepareIrpcRelief,
} from '@/lib/irpc-relief-preparation';
import { SimulationFxSelection } from '@/components/simulation-fx-selection';
import {
irpcFxSources,
irpcFxRow,
updateIrpcFx,
type IrpcFxAmount,
} from '@/lib/irpc-fx';
import { useState } from 'react';
import {
irpcBook,
saveIrpcBook,
reuseIrpcSources,
irpcSources,
newIrpcRow,
recordIrpc,
irpcKinds,
emptyIrpcOrdinaryFacts,
irpcReportRows,
irpcPreparationHtml,
type IrpcBook,
type IrpcRow,
type IrpcEvent,
type IrpcPayment,
type IrpcOrdinaryFacts,
} from '@/lib/irpc-withholdings';
export function SimulationIrpcWithholdings({
draft,
onChange,
}: {
draft: Record<string, unknown>;
onChange: (field: string, value: string) => void;
}) {
const [error, setError] = useState('');
if (
draft.fiscal_component !== 'irpc' &&
draft.fiscal_component !== 'withholding'
)
return null;
try {
const book = irpcBook(draft),
sources = irpcSources(draft),
save = (b: IrpcBook) => {
try {
onChange('modelo_a_snapshot', saveIrpcBook(draft, b));
setError('');
} catch (e) {
setError(String(e));
}
},
change = (id: string, p: Partial<IrpcRow>) =>
save({
...book,
rows: book.rows.map((r) =>
r.id === id ? { ...r, ...p } : r,
),
}),
uid = () => crypto.randomUUID();
const field = (
r: IrpcRow,
k: keyof IrpcRow,
label: string,
type = 'text',
) => (
<label className="block">
{label}
<input
className="block rounded border p-2"
type={type}
value={inputText(r[k] ?? '')}
onChange={(e) => change(r.id, { [k]: e.target.value })}
/>
</label>
);
const select = (
r: IrpcRow,
k: keyof IrpcRow,
label: string,
options: Array<[string, string]>,
) => (
<label className="block">
{label}
<select
className="block rounded border p-2"
value={inputText(r[k] ?? '')}
onChange={(e) => change(r.id, { [k]: e.target.value })}
>
{options.map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</select>
</label>
);
const yn: Array<[string, string]> = [
['', 'Por esclarecer'],
['yes', 'Sim'],
['no', 'Não'],
];
const eventLabels: Array<[IrpcEvent['kind'], string]> = [
['cost', 'Reconhecimento do custo'],
['payment', 'Pagamento do rendimento'],
['maturity', 'Vencimento, ainda que presumido'],
['availability', 'Colocação à disposição'],
['liquidation', 'Liquidação'],
['quantification', 'Apuramento do quantitativo'],
];
const fxSources = irpcFxSources(draft);
const renderFx = (
id: string,
input: IrpcFxAmount,
apply: (patch: Partial<IrpcFxAmount>) => void,
) => (
<div className="space-y-2">
<p>
A base/comprovativo conserva a moeda e o montante originais.
A conversão reutiliza o selector e a aritmética entregues; a
data cambial fica distinta do evento da retenção.
</p>
{fxSources.map((src) => (
<div key={src.id}>
<span>
{src.label};data:
{src.selection.applied?.date || 'por confirmar'}
;selecção actual:
{input.fxSelection?.applied?.rate || 'nenhuma'}.
</span>
<button
type="button"
className="rounded border p-2"
onClick={() => {
if (
window.confirm(
`Substituir a selecção cambial desta linha (${input.fxSelection?.applied?.rate || 'vazia'}) pela origem ${src.label},data ${src.selection.applied?.date || 'por confirmar'}? O montante e a moeda originais são conservados;resultado anterior permanece no histórico até novo cálculo.`,
)
)
apply({
fxSelection: structuredClone(
src.selection,
),
fxLink: {
id: src.id,
fingerprint: src.fingerprint,
},
});
}}
>
Reutilizar selecção cambial {src.id}
</button>
</div>
))}
<SimulationFxSelection
row={irpcFxRow(id, input)}
onChange={(patch) => {
if (patch.fxSelection)
apply(updateIrpcFx(input, patch.fxSelection));
}}
/>
</div>
);
const ordinarySelect = (
r: IrpcRow,
k: keyof IrpcOrdinaryFacts,
label: string,
options: Array<[string, string]>,
) => (
<label className="block">
{label}
<select
value={String(
(r.ordinary ?? emptyIrpcOrdinaryFacts())[k] ?? '',
)}
onChange={(e) =>
change(r.id, {
ordinary: {
...(r.ordinary ?? emptyIrpcOrdinaryFacts()),
[k]: e.target.value,
},
})
}
>
{options.map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</select>
</label>
);
const paymentChange = (id: string, p: Partial<IrpcPayment>) =>
save({
...book,
payments: book.payments.map((v) =>
v.id === id ? { ...v, ...p } : v,
),
});
return (
<section
aria-label="Retenções de IRPC"
className="space-y-4 rounded border p-4"
>
<h4>IRPC — retenções e preparação documental</h4>
<p>
Reutilize documentos e contrapartes existentes. Classifique
o rendimento e o evento aplicável:facturar,pagar e
reconhecer o custo são factos distintos. O conjunto calcula
os casos digitais de 10%, as taxas normais de 20% e as taxas
especiais do artigo 62(4) nos âmbitos identificados.
Dispensas dos artigos 68(d/f) e limite convencional de
royalties Portugal–Moçambique são calculados nos casos
documentados. Informação insuficiente condiciona apenas o
respectivo rendimento.
</p>
{error && <p role="alert">{error}</p>}
<button
type="button"
className="rounded border p-2"
onClick={() => {
try {
save(reuseIrpcSources(draft, book));
} catch (e) {
setError(String(e));
}
}}
>
Reutilizar contrapartes e documentos existentes
</button>
<button
type="button"
className="rounded border p-2"
onClick={() =>
save({
...book,
rows: [...book.rows, newIrpcRow(uid())],
})
}
>
Acrescentar rendimento IRPC
</button>
{book.rows.map((r) => (
<fieldset
key={r.id}
className="space-y-3 rounded border p-3"
>
<legend>
{r.description ||
r.documentRef ||
'Rendimento por identificar'}
</legend>
{r.source && (
<p>
Origem:{r.source.id}. Entradas deliberadamente
editadas são conservadas.{' '}
{sources.find((s) => s.id === r.source!.id)
?.key !== r.source.key && (
<>
<strong>
Origem alterada ou indisponível.
</strong>
{sources.find(
(s) => s.id === r.source!.id,
) && (
<>
<span>
{' '}
Base actual:{r.base};base na
origem:
{sources.find(
(s) =>
s.id ===
r.source!.id,
)?.amount || 'desconhecida'}
;documento actual:
{r.documentRef};origem:
{sources.find(
(s) =>
s.id ===
r.source!.id,
)?.documentRef ||
'desconhecido'}
.
</span>
<button
type="button"
className="rounded border p-2"
onClick={() => {
const s = sources.find(
(s) =>
s.id ===
r.source!.id,
)!;
if (
window.confirm(
'Confirma a reconciliação da origem indicada, conservando as entradas deliberadas e os resultados anteriores no histórico?',
)
)
change(r.id, {
source: {
...r.source!,
key: s.key,
},
});
}}
>
Conservar edições e
reconciliar origem
</button>
</>
)}
</>
)}
</p>
)}
<div className="grid gap-3 md:grid-cols-2">
{select(r, 'direction', 'Perspectiva', [
[
'paid',
'Rendimento pago — retenção a entregar',
],
[
'received',
'Rendimento recebido — retenção sofrida',
],
])}
{field(r, 'counterparty', 'Contraparte')}
{field(
r,
'documentRef',
'Documento / operação / linha distinta',
)}
{field(r, 'description', 'Descrição')}
{select(r, 'origin', 'Origem', [
['projected', 'Projecção com pressupostos'],
['effective', 'Factos documentados'],
])}
{select(
r,
'kind',
'Natureza do rendimento',
irpcKinds,
)}
{field(
r,
'base',
'Base do rendimento, separada do IVA',
)}
{field(r, 'currency', 'Moeda da base')}
{select(
r,
'recipientIrpc',
'Titular sujeito a IRPC neste rendimento',
yn,
)}
{select(
r,
'payerIrpc',
'Devedor sujeito a IRPC',
yn,
)}
{select(r, 'residence', 'Residência do titular', [
['', 'Por esclarecer'],
['resident', 'Residente'],
['nonresident', 'Não residente'],
])}
{r.residence === 'nonresident' && (
<>
{select(
r,
'permanentEstablishment',
'Existe estabelecimento estável em Moçambique',
yn,
)}
{r.permanentEstablishment === 'yes' &&
select(
r,
'attributable',
'Rendimento imputável ao estabelecimento',
yn,
)}
</>
)}
{select(
r,
'mozambique',
'Rendimento obtido em Moçambique, conforme qualificação legal',
yn,
)}
{field(
r,
'qualificationRef',
'Suporte / pressuposto da qualificação, residência e incidência',
)}
{select(
r,
'specialRelief',
'Convenção, isenção ou dispensa aplicável',
[
['', 'Por esclarecer'],
[
'none',
'Não invocada neste caso qualificado',
],
[
'claimed',
'Invocada — identificar regra e condições',
],
],
)}
{r.specialRelief === 'claimed' &&
field(
r,
'reliefRef',
'Artigo, condições, período e prova invocados',
)}
</div>
{r.specialRelief === 'claimed' && (
<fieldset className="space-y-2 rounded border p-2">
<legend>
Preparação de dispensa ou convenção
</legend>
<p>
Reutiliza contraparte, residência,
estabelecimento estável e documento deste
rendimento.
</p>
<label>
Enquadramento
<select
value={
r.reliefPreparation?.kind ??
'dispensation'
}
onChange={(e) =>
change(r.id, {
reliefPreparation: {
...(r.reliefPreparation ??
newIrpcReliefPreparation()),
kind: e.target.value as
| 'dispensation'
| 'exemption'
| 'treaty',
},
})
}
>
<option value="dispensation">
Dispensa de retenção
</option>
<option value="exemption">
Isenção
</option>
<option value="treaty">
Convenção
</option>
</select>
</label>
{(
[
'instrument',
'article',
'country',
'residenceDocument',
'validFrom',
'validTo',
'conditions',
'evidence',
] as const
)
.filter(
(k) =>
![
'country',
'residenceDocument',
].includes(k) ||
r.reliefPreparation?.kind ===
'treaty',
)
.map((k) => (
<label key={k} className="block">
{
{
instrument:
'Diploma / convenção',
article: 'Artigo',
country:
'Estado contratante',
residenceDocument:
'Referência do documento de residência fiscal',
validFrom:
'Aplicável desde',
validTo: 'Aplicável até',
conditions:
'Condições do benefício',
evidence:
'Referências dos documentos existentes',
}[k]
}
<input
type={
k === 'validFrom' ||
k === 'validTo'
? 'date'
: 'text'
}
value={
r.reliefPreparation?.[k] ??
''
}
onChange={(e) =>
change(r.id, {
reliefPreparation: {
...(r.reliefPreparation ??
newIrpcReliefPreparation()),
[k]: e.target.value,
},
})
}
/>
</label>
))}
<label>
Regra aplicável
<select
value={r.reliefPreparation?.rule ?? ''}
onChange={(e) =>
change(r.id, {
reliefPreparation: {
...(r.reliefPreparation ??
newIrpcReliefPreparation()),
rule: e.target
.value as NonNullable<
IrpcRow['reliefPreparation']
>['rule'],
},
})
}
>
{irpcReliefRules.map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</select>
</label>
{(r.reliefPreparation?.rule === 'ptMz12'
? ([
'treatyResident',
'beneficialOwner',
'payerResidentMz',
'armLength',
'royaltyDefinition',
] as const)
: r.reliefPreparation?.rule === 'cirpc68f'
? ([
'subjectToIrpc',
'notTransparent',
] as const)
: r.reliefPreparation?.rule === 'cirpc68d'
? (['subjectToIrpc'] as const)
: []
).map((k) => (
<label key={k} className="block">
{
{
subjectToIrpc:
'Titular sujeito a IRPC relativamente a este rendimento (ainda que isento)',
notTransparent:
'Sociedade fora da transparência fiscal — 6(1)c',
treatyResident:
'Residência em Portugal nos termos do artigo 4 documentada',
beneficialOwner:
'Titular é beneficiário efectivo das royalties',
payerResidentMz:
'Devedor residente em Moçambique; origem do rendimento conforme 12(5)',
armLength:
'Montante sem excesso por relações especiais — 12(6)',
royaltyDefinition:
'Rendimento corresponde à definição de royalties — 12(3), versão 2009',
}[k]
}
<select
value={
r.reliefPreparation?.facts?.[
k
] ?? ''
}
onChange={(e) =>
change(r.id, {
reliefPreparation: {
...(r.reliefPreparation ??
newIrpcReliefPreparation()),
facts: {
...r
.reliefPreparation
?.facts,
[k]: e.target
.value as
| ''
| 'yes'
| 'no',
},
},
})
}
>
{yn.map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</select>
</label>
))}
<p>
{prepareIrpcRelief(
r,
r.reliefPreparation ??
newIrpcReliefPreparation(),
).missing.length
? 'Faltam elementos documentais; confira artigo, período e provas.'
: 'Documentos registados; calcule para conferir as condições da regra seleccionada.'}
</p>
</fieldset>
)}
{r.currency !== 'MZN' &&
renderFx(r.id, r, (patch) => change(r.id, patch))}
{r.kind &&
![
'digital_goods',
'digital_services',
'emoney_commission',
'other',
].includes(r.kind) && (
<fieldset className="space-y-2 rounded border p-2">
<legend>
Factos específicos do rendimento
</legend>
{ordinarySelect(
r,
'nonDigital',
'Rendimento fora do âmbito digital/moeda electrónica do artigo62(5)',
yn,
)}
{r.kind === 'capital' &&
ordinarySelect(
r,
'capitalType',
'Categoria dos capitais',
[
['', 'Por esclarecer'],
[
'unlisted_interest',
'Juros não cotados, fora das permutas de liquidez',
],
[
'unlisted_dividend',
'Dividendos não cotados',
],
[
'listed_debt_mz',
'Juros de bilhetes do Tesouro/títulos de dívida cotados em bolsa —62(3)a',
],
[
'listed_other_mz',
'Outros títulos cotados na Bolsa de Valores de Moçambique —62(4)d',
],
[
'liquidity_repo',
'Juros de permutas de liquidez em bancos,com ou sem garantia —62(3)b',
],
['other', 'Outra categoria'],
],
)}
{r.kind === 'capital' &&
r.residence === 'resident' &&
[
'listed_debt_mz',
'liquidity_repo',
].includes(
r.ordinary?.capitalType ?? '',
) &&
ordinarySelect(
r,
'capitalSeatAndManagement',
'Titular com sede e direcção efectiva nacionais —62(3)',
yn,
)}
{r.kind === 'rent' &&
ordinarySelect(
r,
'rentOwnPropertyManagement',
'Titular é sociedade com objecto de gestão de imóveis próprios',
yn,
)}
{([
'telecom',
'international_transport',
'connected_installation',
'aircraft_maintenance_charter',
'rural_public_electrification',
'maritime_fishing_cabotage',
].includes(r.kind) ||
(r.kind === 'capital' &&
r.ordinary?.capitalType ===
'listed_other_mz')) &&
ordinarySelect(
r,
'specialFacts',
'Factos exactos da categoria62(4) identificados no suporte — não apenas actividade semelhante',
yn,
)}
<label className="block">
Suporte ou pressuposto específico da
classificação
<input
value={r.ordinary?.reference ?? ''}
onChange={(e) =>
change(r.id, {
ordinary: {
...(r.ordinary ??
emptyIrpcOrdinaryFacts()),
reference:
e.target.value,
},
})
}
/>
</label>
</fieldset>
)}
<h5>Eventos aplicáveis — artigo67(5), versão2026</h5>
{r.events.map((e, n) => (
<div key={n} className="grid gap-2 md:grid-cols-3">
<label>
Evento
<select
value={e.kind}
onChange={(ev) =>
change(r.id, {
events: r.events.map((v, j) =>
j === n
? {
...v,
kind: ev.target
.value as IrpcEvent['kind'],
}
: v,
),
})
}
>
{eventLabels.map(([k, l]) => (
<option key={k} value={k}>
{l}
</option>
))}
</select>
</label>
<label>
Data
<input
type="date"
value={e.date}
onChange={(ev) =>
change(r.id, {
events: r.events.map((v, j) =>
j === n
? {
...v,
date: ev.target
.value,
}
: v,
),
})
}
/>
</label>
<label>
Suporte / pressuposto
<input
value={e.reference}
onChange={(ev) =>
change(r.id, {
events: r.events.map((v, j) =>
j === n
? {
...v,
reference:
ev.target
.value,
}
: v,
),
})
}
/>
</label>
</div>
))}
<button
type="button"
onClick={() =>
change(r.id, {
events: [
...r.events,
{
kind: 'cost',
date: '',
reference: '',
},
],
})
}
>
Acrescentar evento aplicável
</button>
{select(
r,
'eventsComplete',
'Todos os eventos aplicáveis identificados, incluindo custo quando aplicável',
yn,
)}
{select(
r,
'settlement',
'Reconciliação da entrega / prova de retenção sofrida',
[
['', 'Por esclarecer'],
['none', 'Ainda sem entrega / prova'],
[
'complete',
'Pagamentos / provas associados completos',
],
],
)}
{field(
r,
'settlementRef',
'Referência da situação declarada',
)}
{r.allocations.map((a, n) => (
<div key={n}>
<label>
Pagamento / prova existente
<select
value={a.paymentId}
onChange={(e) =>
change(r.id, {
allocations: r.allocations.map(
(v, j) =>
j === n
? {
...v,
paymentId:
e.target
.value,
}
: v,
),
})
}
>
<option value="">Seleccione</option>
{book.payments.map((p) => (
<option key={p.id} value={p.id}>
{p.reference} — {p.amount}{' '}
{p.currency} — {p.kind}
</option>
))}
</select>
</label>
<label>
Parcela usada em MZN
<input
value={a.amount}
onChange={(e) =>
change(r.id, {
allocations: r.allocations.map(
(v, j) =>
j === n
? {
...v,
amount: e
.target
.value,
}
: v,
),
})
}
/>
</label>
</div>
))}
{r.settlement === 'complete' && (
<button
type="button"
onClick={() =>
change(r.id, {
allocations: [
...r.allocations,
{ paymentId: '', amount: '' },
],
})
}
>
Associar pagamento/prova existente
</button>
)}
</fieldset>
))}
<h5>Pagamentos e comprovativos reutilizáveis neste livro</h5>
<p>
Uma referência é registada uma vez e repartida entre
rendimentos sem exceder o montante. Pagamento ao fornecedor
não é entrega à AT; pagamentos de IVA conservam o seu
vínculo anterior e não são automaticamente usados em IRPC.
</p>
{book.payments.map((p) => (
<fieldset
key={p.id}
className="grid gap-2 rounded border p-2 md:grid-cols-3"
>
<label>
Natureza
<select
value={p.kind}
onChange={(e) =>
paymentChange(p.id, {
kind: e.target
.value as IrpcPayment['kind'],
})
}
>
<option value="remittance">
Entrega IRPC à AT
</option>
<option value="withholding_proof">
Comprovativo de retenção sofrida
</option>
<option value="beneficiary">
Pagamento ao beneficiário
</option>
</select>
</label>
{(
['reference', 'date', 'amount', 'currency'] as const
).map((k) => (
<label key={k}>
{
{
reference: 'Referência',
date: 'Data',
amount: 'Montante',
currency: 'Moeda',
}[k]
}
<input
type={k === 'date' ? 'date' : 'text'}
value={p[k]}
onChange={(e) =>
paymentChange(p.id, {
[k]: e.target.value,
})
}
/>
</label>
))}
{p.currency !== 'MZN' &&
(p.kind === 'withholding_proof' ? (
renderFx(p.id, p, (patch) =>
paymentChange(p.id, patch),
)
) : (
<p>
A entrega IRPC à AT requer o montante MZN do
comprovativo; converter o pagamento ao
beneficiário não prova entrega do imposto.
</p>
))}
</fieldset>
))}
<button
type="button"
onClick={() =>
save({
...book,
payments: [
...book.payments,
{
id: uid(),
kind: 'remittance',
reference: '',
date: '',
amount: '',
currency: 'MZN',
},
],
})
}
>
Registar pagamento/prova para reutilização
</button>
<div className="space-x-3">
<button
type="button"
className="rounded border p-2"
onClick={() => {
try {
save(recordIrpc(draft, book));
} catch (e) {
setError(String(e));
}
}}
>
Calcular e guardar retenções IRPC
</button>
<button
type="button"
className="rounded border p-2"
onClick={() => {
try {
const html = irpcPreparationHtml(draft),
u = URL.createObjectURL(
new Blob([html], {
type: 'text/html;charset=utf-8',
}),
),
a = document.createElement('a');
a.href = u;
a.download = 'IRPC-L5-preparacao.html';
a.click();
URL.revokeObjectURL(u);
setError('');
} catch (e) {
setError(String(e));
}
}}
>
Exportar preparação IRPC
</button>
</div>
<SimulationIrpcMrfta draft={draft} book={book} save={save} />
{irpcReportRows(draft)
.slice(0, book.result?.rows.length ?? 0)
.map((r, n) => (
<p key={n}>
<strong>{r[1]}</strong>: {r[2]}
<small className="block">{r[3]}</small>
</p>
))}
<p>
Formalidades externas:MRFTA19/39 e guia M/39 preservados.
CER/rubricas digitais e remissão67(7)→62(6) por
corresponder. Exportar não submete nem obtém aceitação AT.
L5 continua aberto;ISPC e IRPS conservam os seus limites
anteriores.
</p>
</section>
);
} catch (e) {
return <p role="alert">{String(e)}</p>;
}
}