Files
Investidor-V2/resources/js/components/simulation-irpc-international.tsx
T
2026-11-10 13:38:56 +02:00

684 lines
33 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 { SimulationIrpcPtCdt } from './simulation-irpc-pt-cdt';
import { FiscalProofParcelFields } from './fiscal-proof-parcel';
import CountrySelect, { type Country } from '@/components/country-select';
import { countryIsoCodes } from '@/lib/country-iso-codes';
import {
newInternationalProof,
prepareInternationalBook,
type AnnualBook,
} from '@/lib/irpc-annual-settlement';
import {
newInternationalPosition,
internationalCreditSource,
type CreditEvidence,
type InternationalCredits,
type InternationalPosition,
type InternationalTaxProof,
} from '@/lib/irpc-international-credit';
const pt = new Intl.DisplayNames(['pt'], { type: 'region' }),
en = new Intl.DisplayNames(['en'], { type: 'region' });
const countries: Country[] = countryIsoCodes
.filter((code) => code !== 'MZ')
.map((code) => ({
code,
name: pt.of(code) ?? code,
name_en: en.of(code) ?? code,
}))
.sort((a, b) => a.name.localeCompare(b.name, 'pt'));
function Evidence({
value,
label,
change,
}: {
value: CreditEvidence;
label: string;
change: (value: CreditEvidence) => void;
}) {
return (
<fieldset className="rounded border p-2">
<legend>{label}</legend>
<label className="block">
Estado
<select
aria-label={label + ' — Estado'}
value={value.state}
onChange={(e) =>
change({
...value,
state: e.target.value as CreditEvidence['state'],
})
}
>
<option value="">Informação em falta</option>
<option value="known">Valor conhecido</option>
<option value="absent">Ausência declarada</option>
</select>
</label>
{(value.state === 'known' || value.amount) && (
<label className="block">
Montante MZN
<input
aria-label={label + ' — Montante'}
value={value.amount}
onChange={(e) =>
change({ ...value, amount: e.target.value })
}
/>
</label>
)}
<label className="block">
Suporte
<input
aria-label={label + ' — Suporte'}
value={value.reference}
onChange={(e) =>
change({ ...value, reference: e.target.value })
}
/>
</label>
</fieldset>
);
}
export function SimulationIrpcInternational({
book,
save,
}: {
book: AnnualBook;
save: (book: AnnualBook) => void;
}) {
const c = book.internationalCredits;
if (!c)
return (
<fieldset className="rounded border p-3">
<legend>Crédito internacional — próprio exercício</legend>
<p>
Prepare a inclusão do imposto estrangeiro comprovado e dos
limites documentados, antes do PEC. O resumo anterior de
créditos e os históricos ficam conservados. A escolha do
país não confirma uma convenção ou benefício.
</p>
<button
type="button"
onClick={() => save(prepareInternationalBook(book))}
>
Preparar crédito internacional
</button>
</fieldset>
);
const update = (next: InternationalCredits) =>
save({ ...book, internationalCredits: next });
const positionChange = (p: InternationalPosition) =>
update({
...c,
positions: c.positions.map((x) => (x.id === p.id ? p : x)),
});
return (
<fieldset className="space-y-3 rounded border p-3">
<legend>Crédito por dupla tributação internacional</legend>
<p>{internationalCreditSource}</p>
{c.positions.some((p) => p.nationalMethod === 'proportional') && (
<p>
No modo de derivação ordinária abaixo, o portal calcula a
fracção nacional a partir da colecta e matéria colectável L6
correntes e da parcela líquida correspondida. Mantêm-se as
restantes condições e limites do modo documentado; não
determina a convenção aplicável.
</p>
)}
<p>
Reúna os comprovativos de cada país numa posição por exercício.
Indique a fracção nacional e a convenção conforme o suporte do
caso. Limites admitem decimal ou fracção exacta n/d; a
apresentação não constitui arredondamento fiscal. Valores MZN
exigem correspondência documental; uma previsão de câmbio ou de
pagamento não comprova o crédito.
</p>
<label className="block">
Estado internacional
<select
aria-label="Estado do crédito internacional L8"
value={c.state}
onChange={(e) =>
update({
...c,
state: e.target
.value as InternationalCredits['state'],
})
}
>
<option value="">Informação em falta</option>
<option value="known">Posições conhecidas</option>
<option value="absent">Ausência declarada</option>
</select>
</label>
<label className="block">
Suporte do inventário
<input
aria-label="Suporte do crédito internacional L8"
value={c.reference}
onChange={(e) =>
update({ ...c, reference: e.target.value })
}
/>
</label>
{book.economicCredits ? (
<p>
Resumo económico anterior conservado:{' '}
{c.economic.state || 'Informação em falta'};{' '}
{c.economic.amount || 'Montante não preenchido'}. Nesta
execução é usado o detalhe económico acima.
</p>
) : (
<Evidence
label="Crédito económico — regra própria"
value={c.economic}
change={(economic) => update({ ...c, economic })}
/>
)}
<Evidence
label="Benefícios/créditos próprios — regra própria"
value={c.benefits}
change={(benefits) => update({ ...c, benefits })}
/>
<p>
{book.economicCredits
? 'Benefícios positivos exigem regras próprias; o crédito económico tem o seu detalhe separado. Informação em falta mantém o saldo dependente indeterminado.'
: 'Créditos económicos e benefícios positivos exigem regras próprias; informação em falta mantém o saldo dependente indeterminado.'}
</p>
<button
type="button"
onClick={() =>
update({
...c,
state: 'known',
positions: [
...c.positions,
{
...newInternationalPosition(
crypto.randomUUID(),
),
subjectNuit: book.subjectNuit,
year: book.year,
assessmentKey: book.assessmentKey,
},
],
})
}
>
Registar posição internacional
</button>
{c.positions.map((p, index) => {
const patch = (v: Partial<InternationalPosition>) =>
positionChange({ ...p, ...v });
const paymentChange = (r: InternationalTaxProof) =>
patch({
payments: p.payments.map((x) =>
x.id === r.id ? r : x,
),
});
return (
<fieldset
key={p.id}
className="space-y-2 rounded border p-3"
>
<legend>Posição internacional {index + 1}</legend>
<CountrySelect
countries={countries}
label={'País do imposto estrangeiro ' + (index + 1)}
value={p.country}
onChange={(country) => patch({ country })}
allowLegacyValue
/>
<label className="block">
Fracção nacional
<select
aria-label={
'Internacional ' +
(index + 1) +
' — Método da fracção nacional'
}
value={p.nationalMethod ?? 'documented'}
onChange={(e) =>
patch({
nationalMethod: e.target
.value as InternationalPosition['nationalMethod'],
})
}
>
<option value="documented">
Limite documentado
</option>
<option value="proportional">
Derivar da colecta L6 ordinária
</option>
</select>
</label>
{p.nationalMethod === 'proportional' && (
<label className="block">
Correspondência da parcela líquida estrangeira
<textarea
aria-label={
'Internacional ' +
(index + 1) +
' — Correspondência da parcela líquida'
}
value={p.nationalBasisReference ?? ''}
onChange={(e) =>
patch({
nationalBasisReference:
e.target.value,
})
}
/>
<span className="block">
Indique abaixo a parcela líquida já incluída
na matéria colectável, após os custos,
perdas e deduções correspondentes. A fracção
será colecta L6 × parcela líquida / matéria
colectável L6, antes de créditos. O portal
não reparte custos ou perdas nem determina
convenções. O limite manual anterior é
conservado, mas não usado neste modo.
</span>
</label>
)}
{(
[
['subjectNuit', 'NUIT titular'],
['year', 'Exercício'],
['incomeAmount', 'Rendimento incluído — MZN'],
[
'incomeReference',
'Suporte da inclusão na matéria colectável',
],
[
'nationalLimit',
'Fracção nacional correspondente — MZN',
],
[
'nationalLimitReference',
'Suporte da fracção nacional e exercício',
],
] as const
)
.filter(
([key]) =>
p.nationalMethod !== 'proportional' ||
(key !== 'nationalLimit' &&
key !== 'nationalLimitReference'),
)
.map(([key, label]) => (
<label key={key} className="block">
{key === 'incomeAmount' &&
p.nationalMethod === 'proportional'
? 'Parcela líquida estrangeira incluída — MZN'
: label}
<input
aria-label={
'Internacional ' +
(index + 1) +
' — ' +
label
}
value={p[key]}
onChange={(e) =>
patch({ [key]: e.target.value })
}
/>
</label>
))}
<label className="block">
Rendimento incluído na matéria colectável
<select
aria-label={
'Internacional ' +
(index + 1) +
' — Inclusão'
}
value={p.incomeIncluded}
onChange={(e) =>
patch({
incomeIncluded: e.target
.value as InternationalPosition['incomeIncluded'],
})
}
>
<option value="">Informação em falta</option>
<option value="yes">Sim — documentado</option>
<option value="no">
Não — crédito não aplicável nesta base
</option>
</select>
</label>
<button
type="button"
onClick={() =>
patch({
assessmentKey: book.assessmentKey,
qualificationCountry: p.country,
})
}
>
Vincular inclusão e limites ao país e à colecta
seleccionados
</button>
<label className="block">
Convenção aplicável
<select
aria-label={
'Internacional ' +
(index + 1) +
' — Convenção'
}
value={p.treaty}
onChange={(e) =>
patch({
treaty: e.target
.value as InternationalPosition['treaty'],
})
}
>
<option value="">Informação em falta</option>
<option value="yes">
Sim — artigo, período e limite documentados
</option>
<option value="no">
Não — ausência de aplicação documentada
</option>
</select>
</label>
{p.treaty === 'yes' && (
<label className="block">
Determinar limite da convenção
<select
aria-label={
'Internacional ' +
(index + 1) +
' — Método CDT'
}
value={p.treatyMethod ?? 'documented'}
onChange={(e) =>
patch({
treatyMethod: e.target
.value as InternationalPosition['treatyMethod'],
})
}
>
<option value="documented">
Limite documentado
</option>
<option value="ptMzRoyalty">
Portugal — royalties ordinárias,
art.12/23(3)a
</option>
<option value="ptMzDividend">
Portugal — dividendos, art.10/23(3)a
</option>
<option value="ptMzInterest">
Portugal — juros ordinários,
art.11/23(3)a
</option>
</select>
</label>
)}
<SimulationIrpcPtCdt
position={p}
patch={patch}
prefix={'Internacional ' + (index + 1) + ' — '}
/>
{(p.treatyMethod ?? 'documented') === 'documented' &&
(p.treaty === 'yes' || p.treatyLimit) && (
<label className="block">
Imposto pago elegível nos termos da
convenção — limite MZN
<input
aria-label={
'Internacional ' +
(index + 1) +
' — Limite CDT'
}
value={p.treatyLimit}
onChange={(e) =>
patch({
treatyLimit: e.target.value,
})
}
/>
</label>
)}
<label className="block">
Suporte da convenção ou da não aplicação
<input
aria-label={
'Internacional ' +
(index + 1) +
' — Suporte CDT'
}
value={p.treatyReference}
onChange={(e) =>
patch({ treatyReference: e.target.value })
}
/>
</label>
<Evidence
label={
'Utilização anterior do crédito — país ' +
(index + 1)
}
value={p.previousUse}
change={(previousUse) => patch({ previousUse })}
/>
<p>
Utilização anterior é crédito já deduzido e
documentado. Não é a soma de simulações históricas
nem o imposto pago acima dos limites.
</p>
<button
type="button"
onClick={() =>
patch({
payments: [
...p.payments,
{
...newInternationalProof(
crypto.randomUUID(),
),
subjectNuit: p.subjectNuit,
year: p.year,
},
],
})
}
>
Registar imposto estrangeiro na posição {index + 1}
</button>
{p.payments.map((r, ri) => {
const change = (
v: Partial<InternationalTaxProof>,
) => paymentChange({ ...r, ...v });
return (
<fieldset
key={r.id}
className="rounded border p-2"
>
<legend>
Imposto estrangeiro {index + 1}.{ri + 1}
</legend>
{(
[
['document', 'Comprovativo'],
['issuer', 'Emitente'],
['date', 'Data (AAAA-MM-DD)'],
['subjectNuit', 'NUIT titular'],
['year', 'Exercício do pagamento'],
['currency', 'Moeda'],
[
'proofAmount',
'Total comprovado — MZN',
],
[
'amount',
'Parcela de imposto seleccionado — MZN',
],
[
'reference',
'Suporte do pagamento e afectação',
],
[
'taxReference',
'Suporte da natureza de imposto sobre o rendimento',
],
] as const
).map(([key, label]) => (
<label key={key} className="block">
{label}
<input
aria-label={
'Imposto estrangeiro ' +
(index + 1) +
'.' +
(ri + 1) +
' — ' +
label
}
value={r[key]}
onChange={(e) =>
change({
[key]: e.target.value,
})
}
/>
</label>
))}
{(r.exerciseReference ||
(r.date &&
!r.date.startsWith(
p.year + '-',
))) && (
<label className="block">
Suporte da correspondência da data
ao exercício
<input
aria-label={
'Imposto estrangeiro ' +
(index + 1) +
'.' +
(ri + 1) +
' — Correspondência temporal'
}
value={r.exerciseReference}
onChange={(e) =>
change({
exerciseReference:
e.target.value,
})
}
/>
</label>
)}
<label className="block">
Pagamento efectivo
<select
aria-label={
'Imposto estrangeiro ' +
(index + 1) +
'.' +
(ri + 1) +
' — Efectivo'
}
value={r.effective}
onChange={(e) =>
change({
effective: e.target
.value as InternationalTaxProof['effective'],
})
}
>
<option value="">
Informação em falta
</option>
<option value="yes">
Sim — comprovado
</option>
<option value="no">
Não — previsão/outra situação
</option>
</select>
</label>
<label className="block">
Imposto sobre o rendimento elegível
<select
aria-label={
'Imposto estrangeiro ' +
(index + 1) +
'.' +
(ri + 1) +
' — Natureza'
}
value={r.incomeTax}
onChange={(e) =>
change({
incomeTax: e.target
.value as InternationalTaxProof['incomeTax'],
})
}
>
<option value="">
Informação em falta
</option>
<option value="yes">
Sim — natureza documentada
</option>
<option value="no">
Não — outra natureza, sem
crédito presumido
</option>
</select>
</label>
<FiscalProofParcelFields
value={r}
change={change}
label={
'Imposto estrangeiro ' +
(index + 1) +
'.' +
(ri + 1)
}
/>
<button
type="button"
onClick={() =>
patch({
payments: p.payments.filter(
(x) => x.id !== r.id,
),
})
}
>
Retirar imposto estrangeiro {index + 1}.
{ri + 1}
</button>
</fieldset>
);
})}
<button
type="button"
onClick={() =>
update({
...c,
positions: c.positions.filter(
(x) => x.id !== p.id,
),
})
}
>
Retirar posição internacional {index + 1}
</button>
</fieldset>
);
})}
</fieldset>
);
}