684 lines
33 KiB
TypeScript
684 lines
33 KiB
TypeScript
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>
|
||
);
|
||
}
|