1339 lines
74 KiB
TypeScript
1339 lines
74 KiB
TypeScript
import { inputText } from '../lib/input-text.ts';
|
||
import { useState } from 'react';
|
||
import { fundraisingActivities } from '@/lib/iva-fundraising';
|
||
import {
|
||
pharmaAnnexEntries,
|
||
pharmaAnnexEntry,
|
||
pharmaAnnexReference,
|
||
} from '@/lib/iva-pharma-annex';
|
||
import { annexIEntries, annexIEntry, annexIReference } from '@/lib/iva-annex-i';
|
||
import {
|
||
temporaryBenefit,
|
||
temporaryBenefitNotice,
|
||
} from '@/lib/iva-temporary-benefits';
|
||
import { isExportService } from '@/lib/iva-export-services';
|
||
import {
|
||
isSpecialRegime,
|
||
specialRegimesFor,
|
||
specialRegimeIssue,
|
||
type SpecialRegime,
|
||
} from '@/lib/iva-special-regimes';
|
||
import {
|
||
isOnboardSupply,
|
||
supplyCategories,
|
||
onboardSupplyIssue,
|
||
type SupplyCategory,
|
||
} from '@/lib/iva-onboard-supplies';
|
||
import {
|
||
emptyProductionTaxOption,
|
||
type ProductionTaxOption,
|
||
} from '@/lib/iva-production-option';
|
||
import {
|
||
referenceExportOperation,
|
||
continueTemporaryOperation,
|
||
continueAnnexIOperation,
|
||
modeloA06Correspondence,
|
||
newSalesOperation,
|
||
operationTreatments,
|
||
recordSales,
|
||
reuseSales,
|
||
salesBook,
|
||
salesCurrent,
|
||
salesReportRows,
|
||
saveSalesBook,
|
||
splitSales,
|
||
type SalesBook,
|
||
type SalesOperation,
|
||
} from '@/lib/iva-operations';
|
||
|
||
export function SimulationOperations({
|
||
draft,
|
||
onChange,
|
||
}: {
|
||
draft: Record<string, unknown>;
|
||
onChange: (field: string, value: string) => void;
|
||
}) {
|
||
const [error, setError] = useState('');
|
||
const [reuseOption, setReuseOption] = useState<{
|
||
rowId: string;
|
||
sourceId: string;
|
||
} | null>(null);
|
||
const [annexSearch, setAnnexSearch] = useState<Record<string, string>>({});
|
||
let book: SalesBook;
|
||
try {
|
||
book = salesBook(draft);
|
||
} catch (cause) {
|
||
return <p role="alert">{String(cause)}</p>;
|
||
}
|
||
const run = (action: () => void) => {
|
||
try {
|
||
action();
|
||
setError('');
|
||
} catch (cause) {
|
||
setError(cause instanceof Error ? cause.message : String(cause));
|
||
}
|
||
};
|
||
const save = (next: SalesBook) =>
|
||
onChange('modelo_a_snapshot', saveSalesBook(draft, next));
|
||
const change = (id: string, patch: Partial<SalesOperation>) =>
|
||
run(() =>
|
||
save({
|
||
...book,
|
||
rows: book.rows.map((r) =>
|
||
r.id === id ? { ...r, ...patch } : r,
|
||
),
|
||
}),
|
||
);
|
||
const continueTemporary = (id: string, target: string) =>
|
||
run(() =>
|
||
save({
|
||
...book,
|
||
rows: book.rows.map((row) =>
|
||
row.id === id
|
||
? continueTemporaryOperation(row, target)
|
||
: row,
|
||
),
|
||
}),
|
||
);
|
||
return (
|
||
<section
|
||
aria-label="Operações IVA por oportunidade"
|
||
className="space-y-3 rounded border p-3"
|
||
>
|
||
<h4 className="font-semibold">
|
||
Operações de IVA — enquadramento das vendas
|
||
</h4>
|
||
<p>
|
||
Oportunidade{' '}
|
||
{inputText(draft.context_opportunity || 'entrada geral')}; moeda
|
||
original {book.currency || 'por indicar'}. Cobertura2026/MZN.
|
||
Reutilize as vendas e complete apenas o enquadramento. Este
|
||
bloco classifica as operações; não substitui nem soma novamente
|
||
a execução mensal geral acima.
|
||
</p>
|
||
<p>
|
||
Uma empresa pode ter operações com tratamentos diferentes. Os
|
||
valores são projecções sob pressupostos declarados, sem
|
||
certificar facturas, reconhecer créditos ou submeter uma
|
||
declaração à AT.
|
||
</p>
|
||
<details>
|
||
<summary>
|
||
Correspondência dos campos05/06/07 e contagem única
|
||
</summary>
|
||
<p>{modeloA06Correspondence}</p>
|
||
</details>
|
||
{error && <p role="alert">{error}</p>}
|
||
<button
|
||
type="button"
|
||
className="underline"
|
||
onClick={() => run(() => save(reuseSales(draft, book)))}
|
||
>
|
||
Reutilizar vendas e períodos existentes
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="ml-3 underline"
|
||
disabled={!book.periods.length}
|
||
onClick={() =>
|
||
run(() =>
|
||
save({
|
||
...book,
|
||
rows: [
|
||
...book.rows,
|
||
newSalesOperation(
|
||
crypto.randomUUID(),
|
||
book.periods[0].period,
|
||
book.currency,
|
||
),
|
||
],
|
||
}),
|
||
)
|
||
}
|
||
>
|
||
Acrescentar operação em falta
|
||
</button>
|
||
{book.rows.map((row) => {
|
||
const rule = operationTreatments.find(
|
||
(r) => r.id === row.treatment,
|
||
),
|
||
annexI = annexIEntry(row.annexIItem);
|
||
return (
|
||
<details className="rounded border p-3" key={row.id}>
|
||
<summary>
|
||
{row.description || 'Operação adicional'} —{' '}
|
||
{row.period}
|
||
</summary>
|
||
<p>
|
||
Origem: {row.source};{' '}
|
||
{row.sourceAmount ?? 'sem valor de origem'}{' '}
|
||
{row.sourceCurrency}; {row.sourcePeriod}. O
|
||
estabelecimento não é inferido da localização do
|
||
projecto.
|
||
</p>
|
||
<div className="grid gap-3 md:grid-cols-2">
|
||
<label>
|
||
Descrição da operação
|
||
<input
|
||
className="block rounded border p-2"
|
||
value={row.description}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
description: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
<label>
|
||
Estabelecimento
|
||
<input
|
||
className="block rounded border p-2"
|
||
value={row.establishment}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
establishment: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
<label>
|
||
Período
|
||
<input
|
||
type="month"
|
||
className="block rounded border p-2"
|
||
min="2026-01"
|
||
max="2026-12"
|
||
value={row.period}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
period: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
<label>
|
||
Valor declarado
|
||
<input
|
||
inputMode="decimal"
|
||
className="block rounded border p-2"
|
||
value={row.amount}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
amount: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
<label>
|
||
Representação do preço
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.basis}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
basis: e.target
|
||
.value as SalesOperation['basis'],
|
||
})
|
||
}
|
||
>
|
||
<option value="">Ainda desconhecida</option>
|
||
<option value="net">Sem IVA</option>
|
||
<option value="included">
|
||
IVA incluído
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
Tratamento da operação
|
||
<select
|
||
className="block rounded border p-2"
|
||
value={row.treatment}
|
||
onChange={(e) =>
|
||
temporaryBenefit(row.treatment) &&
|
||
!temporaryBenefit(e.target.value)
|
||
? continueTemporary(
|
||
row.id,
|
||
e.target.value,
|
||
)
|
||
: change(row.id, {
|
||
treatment: e.target.value,
|
||
confirmed: [],
|
||
})
|
||
}
|
||
>
|
||
{operationTreatments.map((t) => (
|
||
<option value={t.id} key={t.id}>
|
||
{t.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
</div>
|
||
{isSpecialRegime(row.treatment) && (
|
||
<fieldset className="space-y-2 rounded border p-2">
|
||
<legend>
|
||
Regime aduaneiro ou fiscal da operação
|
||
</legend>
|
||
<label>
|
||
Regime do artigo14(1)
|
||
<select
|
||
value={
|
||
specialRegimesFor(
|
||
row.treatment,
|
||
).some(
|
||
([value]) =>
|
||
value === row.specialRegime,
|
||
)
|
||
? row.specialRegime
|
||
: ''
|
||
}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
specialRegime: e.target
|
||
.value as SpecialRegime,
|
||
confirmed: [],
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Por identificar
|
||
</option>
|
||
{specialRegimesFor(row.treatment).map(
|
||
([value, label]) => (
|
||
<option
|
||
value={value}
|
||
key={value}
|
||
>
|
||
{label}
|
||
</option>
|
||
),
|
||
)}
|
||
</select>
|
||
</label>
|
||
<p>
|
||
Identifique no fundamento os bens, a
|
||
operação e o documento ou pressuposto
|
||
expresso do regime. A escolha conserva
|
||
preço, moeda e período e pede a revisão das
|
||
condições; não comprova o enquadramento por
|
||
estar numa zona. Bens e serviço de
|
||
transporte ou serviço conexo têm preços
|
||
próprios, sem duplicar o valor.
|
||
</p>
|
||
{specialRegimeIssue(
|
||
row.treatment,
|
||
row.specialRegime,
|
||
) && (
|
||
<p role="status">
|
||
{specialRegimeIssue(
|
||
row.treatment,
|
||
row.specialRegime,
|
||
)}
|
||
</p>
|
||
)}
|
||
</fieldset>
|
||
)}
|
||
{isOnboardSupply(row.treatment) && (
|
||
<fieldset className="space-y-2 rounded border p-2">
|
||
<legend>
|
||
Natureza do abastecimento a bordo
|
||
</legend>
|
||
<label>
|
||
Categoria do artigo13(3)
|
||
<select
|
||
value={row.supplyCategory ?? ''}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
supplyCategory: e.target
|
||
.value as SupplyCategory,
|
||
confirmed: [],
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Por identificar
|
||
</option>
|
||
{supplyCategories.map(
|
||
([value, label]) => (
|
||
<option
|
||
key={value}
|
||
value={value}
|
||
>
|
||
{label}
|
||
</option>
|
||
),
|
||
)}
|
||
</select>
|
||
</label>
|
||
<p>
|
||
A escolha conserva o preço, a moeda e o
|
||
período; reveja as condições. Para
|
||
fornecimento misto, discrimine as parcelas
|
||
efectivas sem repetir o total em cada linha.
|
||
Desconhecido não equivale a «sem bebidas».
|
||
</p>
|
||
{onboardSupplyIssue(
|
||
row.treatment,
|
||
row.supplyCategory,
|
||
) && (
|
||
<p role="status">
|
||
{onboardSupplyIssue(
|
||
row.treatment,
|
||
row.supplyCategory,
|
||
)}
|
||
</p>
|
||
)}
|
||
</fieldset>
|
||
)}
|
||
{isExportService(row.treatment) && (
|
||
<fieldset className="space-y-2 rounded border p-2">
|
||
<legend>
|
||
Referência à operação relacionada
|
||
</legend>
|
||
<p>
|
||
Se a operação já consta desta
|
||
oportunidade,pode referenciá-la sem
|
||
preencher de novo a descrição. A escolha
|
||
conserva o preço,moeda,período e fundamento
|
||
deste serviço;limpa as confirmações para
|
||
revisão. Não copia o valor dos bens para o
|
||
preço do serviço nem comprova
|
||
automaticamente a isenção. Para operação
|
||
documentada fora deste livro,use o
|
||
fundamento abaixo.
|
||
</p>
|
||
<label>
|
||
Operação já preenchida
|
||
<select
|
||
value={row.relatedExport?.id ?? ''}
|
||
onChange={(e) =>
|
||
run(() => {
|
||
if (!e.target.value) {
|
||
save({
|
||
...book,
|
||
rows: book.rows.map(
|
||
(item) =>
|
||
item.id ===
|
||
row.id
|
||
? {
|
||
...item,
|
||
relatedExport:
|
||
undefined,
|
||
confirmed:
|
||
[],
|
||
}
|
||
: item,
|
||
),
|
||
});
|
||
return;
|
||
}
|
||
const related = book.rows.find(
|
||
(item) =>
|
||
item.id ===
|
||
e.target.value,
|
||
);
|
||
if (!related)
|
||
throw new Error(
|
||
'Operação relacionada já não disponível; dados conservados.',
|
||
);
|
||
save({
|
||
...book,
|
||
rows: book.rows.map(
|
||
(item) =>
|
||
item.id === row.id
|
||
? referenceExportOperation(
|
||
item,
|
||
related,
|
||
)
|
||
: item,
|
||
),
|
||
});
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Sem referência interna — identificar
|
||
no fundamento
|
||
</option>
|
||
{book.rows
|
||
.filter(
|
||
(item) => item.id !== row.id,
|
||
)
|
||
.map((item) => (
|
||
<option
|
||
key={item.id}
|
||
value={item.id}
|
||
>
|
||
{item.description ||
|
||
item.id}{' '}
|
||
— {item.period}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
{row.relatedExport && (
|
||
<>
|
||
<p>
|
||
Referência guardada:{' '}
|
||
{row.relatedExport.description}; ID{' '}
|
||
{row.relatedExport.id};{' '}
|
||
{row.relatedExport.treatment}.
|
||
</p>
|
||
{book.rows.some(
|
||
(item) =>
|
||
item.id ===
|
||
row.relatedExport!.id,
|
||
) && (
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
run(() => {
|
||
const related =
|
||
book.rows.find(
|
||
(item) =>
|
||
item.id ===
|
||
row
|
||
.relatedExport!
|
||
.id,
|
||
)!;
|
||
save({
|
||
...book,
|
||
rows: book.rows.map(
|
||
(item) =>
|
||
item.id ===
|
||
row.id
|
||
? referenceExportOperation(
|
||
item,
|
||
related,
|
||
)
|
||
: item,
|
||
),
|
||
});
|
||
})
|
||
}
|
||
>
|
||
Actualizar referência escolhida
|
||
e rever condições
|
||
</button>
|
||
)}
|
||
</>
|
||
)}
|
||
</fieldset>
|
||
)}
|
||
{temporaryBenefit(row.treatment) && (
|
||
<fieldset className="space-y-2 rounded border p-2">
|
||
<legend>
|
||
Benefício temporário — prazo e continuidade
|
||
</legend>
|
||
<p>
|
||
{temporaryBenefitNotice(
|
||
row.treatment,
|
||
row.period,
|
||
)}
|
||
</p>
|
||
<p>
|
||
{
|
||
temporaryBenefit(row.treatment)!
|
||
.historicalScope
|
||
}
|
||
</p>
|
||
<p>
|
||
Não precisa de preencher documentos de uma
|
||
isenção já terminada para prosseguir em2026.
|
||
Antes de escolher outro tratamento, confirme
|
||
a natureza da operação: a escolha conserva
|
||
valor,moeda,período,estabelecimento eorigem;
|
||
limpa as confirmações para revisão e mantém
|
||
o resultado anterior no histórico.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
continueTemporary(row.id, 'general')
|
||
}
|
||
>
|
||
Escolher operação interna geral e rever
|
||
condições
|
||
</button>
|
||
{row.treatment === 'temporary_sugar_cane' && (
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
continueTemporary(
|
||
row.id,
|
||
'production_agriculture',
|
||
)
|
||
}
|
||
>
|
||
Analisar produção agrícola própria —
|
||
artigo9(7)(a)
|
||
</button>
|
||
)}
|
||
<p>
|
||
Para outro enquadramento disponível,use a
|
||
escolha «Tratamento da operação». Nenhum
|
||
resultado é recalculado automaticamente.
|
||
</p>
|
||
</fieldset>
|
||
)}
|
||
{temporaryBenefit(row.temporaryBenefitOrigin) &&
|
||
!temporaryBenefit(row.treatment) && (
|
||
<p>
|
||
Origem conservada:{' '}
|
||
{
|
||
temporaryBenefit(
|
||
row.temporaryBenefitOrigin,
|
||
)!.label
|
||
}
|
||
,benefício com termo31/12/2025.
|
||
Enquadramento actual escolhido
|
||
expressamente;confirme as respectivas
|
||
condições antes de calcular.
|
||
</p>
|
||
)}
|
||
{row.treatment === 'annexI_goods' && (
|
||
<fieldset className="space-y-2 rounded border p-2">
|
||
<legend>
|
||
Bem do Anexo I — edição Lei22/2022
|
||
</legend>
|
||
<p>
|
||
Escolha código e designação e confira apenas
|
||
os qualificadores dessa linha. A pesquisa
|
||
conserva a escolha actual; mudar de posição
|
||
exige rever as condições específicas. O
|
||
anexo não dispensa o enquadramento de outras
|
||
isenções aplicáveis ao mesmo bem.
|
||
</p>
|
||
<label className="block">
|
||
Pesquisar código ou designação no Anexo I
|
||
<input
|
||
value={annexSearch[`I:${row.id}`] ?? ''}
|
||
onChange={(e) =>
|
||
setAnnexSearch({
|
||
...annexSearch,
|
||
[`I:${row.id}`]: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
<label className="block">
|
||
Posição do Anexo I
|
||
<select
|
||
className="block max-w-full rounded border p-2"
|
||
value={row.annexIItem ?? ''}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
annexIItem: e.target.value,
|
||
annexIOnly: undefined,
|
||
confirmed: row.confirmed.filter(
|
||
(n) => n === 0,
|
||
),
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Ainda por determinar
|
||
</option>
|
||
{annexIEntries
|
||
.filter(
|
||
(entry) =>
|
||
entry.id ===
|
||
row.annexIItem ||
|
||
`${entry.code} ${entry.context ?? ''} ${entry.label}`
|
||
.toLocaleLowerCase('pt')
|
||
.includes(
|
||
(
|
||
annexSearch[
|
||
`I:${row.id}`
|
||
] ?? ''
|
||
)
|
||
.trim()
|
||
.toLocaleLowerCase(
|
||
'pt',
|
||
),
|
||
),
|
||
)
|
||
.map((entry) => (
|
||
<option
|
||
key={entry.id}
|
||
value={entry.id}
|
||
>
|
||
{entry.code} —{' '}
|
||
{entry.context
|
||
? entry.context + ' — '
|
||
: ''}
|
||
{entry.label} (PDF
|
||
{entry.pdfPage})
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
{row.annexIItem && (
|
||
<p>
|
||
{annexI
|
||
? annexIReference(annexI)
|
||
: `Posição guardada não encontrada nesta edição: ${row.annexIItem}. Original conservado; escolha explicitamente a correspondência.`}
|
||
</p>
|
||
)}
|
||
{!!annexI?.relatedTreatments?.length && (
|
||
<div>
|
||
<p>
|
||
Confira a natureza e o destino: há
|
||
um tratamento existente que pode
|
||
abranger este bem. Se for aplicável,
|
||
use-o para conservar também o
|
||
direito à dedução nas condições
|
||
legais. Ao confirmar a passagem,
|
||
conserva valores e origem e terá de
|
||
rever as condições do tratamento
|
||
escolhido; nenhum resultado anterior
|
||
é substituído automaticamente.
|
||
</p>
|
||
{annexI.relatedTreatments.map((id) => {
|
||
const target =
|
||
operationTreatments.find(
|
||
(t) => t.id === id,
|
||
);
|
||
return (
|
||
target && (
|
||
<button
|
||
className="block underline"
|
||
type="button"
|
||
key={id}
|
||
onClick={() =>
|
||
run(() =>
|
||
save({
|
||
...book,
|
||
rows: book.rows.map(
|
||
(r) =>
|
||
r.id ===
|
||
row.id
|
||
? continueAnnexIOperation(
|
||
r,
|
||
id,
|
||
)
|
||
: r,
|
||
),
|
||
}),
|
||
)
|
||
}
|
||
>
|
||
Usar {target.label} e
|
||
rever condições
|
||
</button>
|
||
)
|
||
);
|
||
})}
|
||
{!annexI.relatedRequired && (
|
||
<label className="block">
|
||
<input
|
||
type="checkbox"
|
||
checked={
|
||
row.annexIOnly === true
|
||
}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
annexIOnly:
|
||
e.target
|
||
.checked,
|
||
})
|
||
}
|
||
/>
|
||
Conferi a designação e o
|
||
destino: esta operação não
|
||
corresponde ao tratamento
|
||
indicado acima; o fundamento
|
||
explica a aplicação apenas do
|
||
artigo9(12)(h).
|
||
</label>
|
||
)}
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
)}
|
||
{row.treatment === 'pharma_manufacturing_inputs' && (
|
||
<fieldset className="space-y-2 rounded border p-2">
|
||
<legend>
|
||
Bem do Anexo III — edição Lei22/2022
|
||
</legend>
|
||
<p>
|
||
Escolha a linha pelo código e pela
|
||
designação. O estabelecimento que transmite
|
||
deve estar integrado no SNS; o destino
|
||
hospitalar não substitui essa condição. A
|
||
selecção não classifica automaticamente uma
|
||
mercadoria nem confirma a sua utilização.
|
||
</p>
|
||
<label className="block">
|
||
Pesquisar código ou designação
|
||
<input
|
||
value={annexSearch[row.id] ?? ''}
|
||
onChange={(e) =>
|
||
setAnnexSearch({
|
||
...annexSearch,
|
||
[row.id]: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
<label className="block">
|
||
Linha do Anexo III
|
||
<select
|
||
className="block max-w-full rounded border p-2"
|
||
value={row.pharmaAnnexItem ?? ''}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
pharmaAnnexItem: e.target.value,
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Ainda por determinar
|
||
</option>
|
||
{pharmaAnnexEntries
|
||
.filter(
|
||
(entry) =>
|
||
entry.id ===
|
||
row.pharmaAnnexItem ||
|
||
`${entry.code} ${entry.label}`
|
||
.toLocaleLowerCase('pt')
|
||
.includes(
|
||
(
|
||
annexSearch[
|
||
row.id
|
||
] ?? ''
|
||
)
|
||
.trim()
|
||
.toLocaleLowerCase(
|
||
'pt',
|
||
),
|
||
),
|
||
)
|
||
.map((entry) => (
|
||
<option
|
||
key={entry.id}
|
||
value={entry.id}
|
||
>
|
||
{entry.code} — {entry.label}{' '}
|
||
(PDF{entry.pdfPage})
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
{row.pharmaAnnexItem && (
|
||
<p>
|
||
{pharmaAnnexEntry(row.pharmaAnnexItem)
|
||
? pharmaAnnexReference(
|
||
pharmaAnnexEntry(
|
||
row.pharmaAnnexItem,
|
||
)!,
|
||
)
|
||
: `Escolha guardada não encontrada nesta edição: ${row.pharmaAnnexItem}. Original conservado; seleccione explicitamente a correspondência.`}
|
||
</p>
|
||
)}
|
||
<p>
|
||
O exemplar repete2912.19.00 em duas linhas
|
||
com designações diferentes e imprime2935.
|
||
sem completar a posição. As linhas mantêm
|
||
identidades próprias; não se inventam
|
||
dígitos nem se estende a isenção apenas pelo
|
||
código.
|
||
</p>
|
||
</fieldset>
|
||
)}
|
||
{row.treatment.startsWith('fundraising_') && (
|
||
<fieldset className="space-y-2 rounded border p-2">
|
||
<legend>
|
||
Manifestação ocasional — limite anual da
|
||
entidade
|
||
</legend>
|
||
<p>
|
||
O total é declarado para a entidade em todo
|
||
o ano, incluindo manifestações fora deste
|
||
dossier. Várias vendas da mesma manifestação
|
||
não são novas manifestações.
|
||
</p>
|
||
{(
|
||
[
|
||
[
|
||
'entityReference',
|
||
'Referência da entidade organizadora',
|
||
],
|
||
[
|
||
'annualEvents',
|
||
'Total anual de manifestações da entidade, incluindo esta',
|
||
],
|
||
[
|
||
'eventNumber',
|
||
'Número desta manifestação no ano',
|
||
],
|
||
] as const
|
||
).map(([field, label]) => (
|
||
<label className="block" key={field}>
|
||
{label}
|
||
<input
|
||
value={
|
||
row.fundraiser?.[field] ?? ''
|
||
}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
fundraiser: {
|
||
entityReference: '',
|
||
annualEvents: '',
|
||
eventNumber: '',
|
||
activity: '',
|
||
...row.fundraiser,
|
||
[field]: e.target.value,
|
||
},
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
))}
|
||
<label className="block">
|
||
Actividade habitual isenta
|
||
<select
|
||
value={row.fundraiser?.activity ?? ''}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
fundraiser: {
|
||
entityReference: '',
|
||
annualEvents: '',
|
||
eventNumber: '',
|
||
...row.fundraiser,
|
||
activity: e.target.value,
|
||
},
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Por identificar
|
||
</option>
|
||
{fundraisingActivities
|
||
.filter(([id]) =>
|
||
row.treatment ===
|
||
'fundraising_nonprofit'
|
||
? !id.endsWith('_public')
|
||
: id !==
|
||
'statutory_membership',
|
||
)
|
||
.map(([id, label]) => (
|
||
<option key={id} value={id}>
|
||
{label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
</fieldset>
|
||
)}
|
||
{row.treatment.startsWith('production_') &&
|
||
row.treatment.endsWith('_opted') && (
|
||
<fieldset className="space-y-2 rounded border p-2">
|
||
<legend>
|
||
Opção de tributação da produção —
|
||
artigo11
|
||
</legend>
|
||
<p>
|
||
Complete apenas as datas em falta. A
|
||
opção não cessa automaticamente ao fim
|
||
de cinco anos. O prazo termina às 24
|
||
horas do dia correspondente; se faltar
|
||
esse dia, no último dia do mesmo mês. O
|
||
artigo11(3) exige declaração antes de
|
||
expirar o prazo e fixa os efeitos em 1
|
||
de Janeiro seguinte, respeitando a
|
||
permanência mínima.
|
||
</p>
|
||
{book.rows.some(
|
||
(source) =>
|
||
source.id !== row.id &&
|
||
source.taxOption,
|
||
) && (
|
||
<label className="block">
|
||
Reutilizar opção já preenchida neste
|
||
dossier
|
||
<select
|
||
value={
|
||
reuseOption?.rowId ===
|
||
row.id
|
||
? reuseOption.sourceId
|
||
: ''
|
||
}
|
||
onChange={(e) =>
|
||
setReuseOption(
|
||
e.target.value
|
||
? {
|
||
rowId: row.id,
|
||
sourceId:
|
||
e.target
|
||
.value,
|
||
}
|
||
: null,
|
||
)
|
||
}
|
||
>
|
||
<option value="">
|
||
Escolha a origem, se
|
||
aplicável ao mesmo sujeito
|
||
passivo
|
||
</option>
|
||
{book.rows
|
||
.filter(
|
||
(source) =>
|
||
source.id !==
|
||
row.id &&
|
||
source.taxOption,
|
||
)
|
||
.map((source) => (
|
||
<option
|
||
key={source.id}
|
||
value={source.id}
|
||
>
|
||
{source.description}{' '}
|
||
—{' '}
|
||
{
|
||
source.establishment
|
||
}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
)}
|
||
{reuseOption?.rowId === row.id && (
|
||
<div>
|
||
<p>
|
||
Ao confirmar, substituirá o
|
||
enquadramento e as datas da
|
||
opção desta linha pelos da
|
||
origem escolhida, conservando a
|
||
data desta operação. Confirme
|
||
que se aplicam ao mesmo sujeito
|
||
passivo; o resultado anterior
|
||
permanece no histórico.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
run(() => {
|
||
const source =
|
||
book.rows.find(
|
||
(r) =>
|
||
r.id ===
|
||
reuseOption.sourceId,
|
||
);
|
||
if (!source?.taxOption)
|
||
throw new Error(
|
||
'Origem da opção já não disponível; dados conservados.',
|
||
);
|
||
change(row.id, {
|
||
taxOption: {
|
||
...structuredClone(
|
||
source.taxOption,
|
||
),
|
||
operationDate:
|
||
row
|
||
.taxOption
|
||
?.operationDate ??
|
||
'',
|
||
origin: {
|
||
rowId: source.id,
|
||
description:
|
||
source.description,
|
||
establishment:
|
||
source.establishment,
|
||
},
|
||
},
|
||
});
|
||
setReuseOption(null);
|
||
})
|
||
}
|
||
>
|
||
Confirmar reutilização da opção
|
||
escolhida
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
setReuseOption(null)
|
||
}
|
||
>
|
||
Conservar os dados desta linha
|
||
</button>
|
||
</div>
|
||
)}
|
||
<label className="block">
|
||
Tipo de declaração de opção
|
||
<select
|
||
value={row.taxOption?.mode ?? ''}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
taxOption: {
|
||
...emptyProductionTaxOption(),
|
||
...row.taxOption,
|
||
mode: e.target
|
||
.value as ProductionTaxOption['mode'],
|
||
},
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Por indicar
|
||
</option>
|
||
<option value="annual">
|
||
Opção com efeitos no ano
|
||
seguinte
|
||
</option>
|
||
<option value="startup">
|
||
Opção na declaração de início da
|
||
actividade
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label className="block">
|
||
Declaração de regresso à isenção
|
||
<select
|
||
value={
|
||
row.taxOption?.returnStatus ??
|
||
''
|
||
}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
taxOption: {
|
||
...emptyProductionTaxOption(),
|
||
...row.taxOption,
|
||
returnStatus: e.target
|
||
.value as ProductionTaxOption['returnStatus'],
|
||
},
|
||
})
|
||
}
|
||
>
|
||
<option value="">
|
||
Ainda desconhecido
|
||
</option>
|
||
<option value="none">
|
||
Não existe declaração de
|
||
regresso
|
||
</option>
|
||
<option value="declared">
|
||
Existe declaração de regresso
|
||
</option>
|
||
</select>
|
||
</label>
|
||
{(
|
||
[
|
||
[
|
||
'declarationDate',
|
||
'Data da declaração de opção',
|
||
],
|
||
...(row.taxOption?.mode ===
|
||
'startup'
|
||
? [
|
||
[
|
||
'startupDate',
|
||
'Data de início da actividade',
|
||
],
|
||
]
|
||
: []),
|
||
[
|
||
'operationDate',
|
||
'Data da operação nesta linha',
|
||
],
|
||
...(row.taxOption?.returnStatus ===
|
||
'declared' ||
|
||
row.taxOption?.returnDeclarationDate
|
||
? [
|
||
[
|
||
'returnDeclarationDate',
|
||
'Data da declaração de regresso à isenção',
|
||
],
|
||
]
|
||
: []),
|
||
] as Array<
|
||
[
|
||
(
|
||
| 'declarationDate'
|
||
| 'startupDate'
|
||
| 'operationDate'
|
||
| 'returnDeclarationDate'
|
||
),
|
||
string,
|
||
]
|
||
>
|
||
).map(([field, label]) => (
|
||
<label className="block" key={field}>
|
||
{label}
|
||
<input
|
||
type="date"
|
||
value={
|
||
row.taxOption?.[field] ?? ''
|
||
}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
taxOption: {
|
||
...emptyProductionTaxOption(),
|
||
...row.taxOption,
|
||
[field]:
|
||
e.target.value,
|
||
},
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
))}
|
||
</fieldset>
|
||
)}
|
||
<p>
|
||
{rule?.article}; {rule?.source}. Aplicação nesta
|
||
versão: Janeiro–Dezembro2026, sujeita às condições
|
||
seguintes.
|
||
</p>
|
||
{rule?.conditions.map((condition, index) => (
|
||
<label className="block" key={index}>
|
||
<input
|
||
type="checkbox"
|
||
checked={row.confirmed.includes(index)}
|
||
onChange={(e) =>
|
||
change(row.id, {
|
||
confirmed: e.target.checked
|
||
? [...row.confirmed, index]
|
||
: row.confirmed.filter(
|
||
(i) => i !== index,
|
||
),
|
||
})
|
||
}
|
||
/>{' '}
|
||
{condition}
|
||
</label>
|
||
))}
|
||
<label className="block">
|
||
Documento ou pressuposto que sustenta estas
|
||
condições
|
||
<textarea
|
||
className="block w-full rounded border p-2"
|
||
value={row.evidence}
|
||
onChange={(e) =>
|
||
change(row.id, { evidence: e.target.value })
|
||
}
|
||
/>
|
||
</label>
|
||
<p>
|
||
Se precisar de repartir uma venda agregada, conserve
|
||
o valor de origem e prepare linhas discriminadas.
|
||
Não altere silenciosamente a sua representação
|
||
monetária.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
className="mr-3 underline"
|
||
onClick={() =>
|
||
run(() =>
|
||
save(
|
||
splitSales(
|
||
book,
|
||
row.id,
|
||
crypto.randomUUID(),
|
||
),
|
||
),
|
||
)
|
||
}
|
||
>
|
||
Repartir explicitamente esta venda, conservando o
|
||
total de origem
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="underline"
|
||
onClick={() =>
|
||
run(() =>
|
||
save({
|
||
...book,
|
||
rows: book.rows.filter(
|
||
(r) => r.id !== row.id,
|
||
),
|
||
periods: book.periods.map((p) =>
|
||
p.period === row.period
|
||
? { ...p, complete: false }
|
||
: p,
|
||
),
|
||
}),
|
||
)
|
||
}
|
||
>
|
||
Remover esta linha do preenchimento (resultado
|
||
anterior conservado)
|
||
</button>
|
||
</details>
|
||
);
|
||
})}
|
||
{book.periods.map((p) => (
|
||
<label className="block" key={p.period}>
|
||
<input
|
||
type="checkbox"
|
||
checked={p.complete}
|
||
onChange={(e) =>
|
||
run(() =>
|
||
save({
|
||
...book,
|
||
periods: book.periods.map((v) =>
|
||
v.period === p.period
|
||
? {
|
||
...v,
|
||
complete: e.target.checked,
|
||
}
|
||
: v,
|
||
),
|
||
}),
|
||
)
|
||
}
|
||
/>{' '}
|
||
{p.period}: incluí todas as operações deste período; lista
|
||
vazia significa ausência expressamente confirmada.
|
||
</label>
|
||
))}
|
||
<p>
|
||
A correspondência05/06/07 está identificada no exemplar.
|
||
Tratamentos ainda não implementados conservam o seu fundamento
|
||
por determinar;localização estrangeira não determina06. IVA
|
||
incluído a5% conserva a fracção exacta e apresenta um decimal
|
||
a6casas; a aproximação de apresentação não constitui regra
|
||
declarativa. O desconhecido nunca é assumido como zero.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
disabled={!book.periods.length}
|
||
className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50"
|
||
onClick={() => run(() => save(recordSales(draft, book)))}
|
||
>
|
||
Calcular e guardar operações IVA
|
||
</button>
|
||
{book.result && (
|
||
<>
|
||
<p role="status">
|
||
{salesCurrent(draft, book)
|
||
? 'Resultado correspondente às entradas.'
|
||
: 'Resultado histórico conservado; reveja as alterações antes de recalcular.'}
|
||
</p>
|
||
<dl>
|
||
{salesReportRows(draft).map(
|
||
([, label, value, origin], index) => (
|
||
<div
|
||
className="break-inside-avoid border-t py-2"
|
||
key={index}
|
||
>
|
||
<dt className="font-semibold">{label}</dt>
|
||
<dd>
|
||
{value}
|
||
<small className="block">
|
||
{origin}
|
||
</small>
|
||
</dd>
|
||
</div>
|
||
),
|
||
)}
|
||
</dl>
|
||
</>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|