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

927 lines
44 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 { FiscalCalculationGaps } from './fiscal-calculation-gaps';
import { FiscalL3Preparation } from './fiscal-l3-preparation';
import { useState } from 'react';
import { AcquisitionClosure } from './acquisition-closure';
import { AcquisitionSubstitution } from './acquisition-substitution';
import { AcquisitionRefund } from './acquisition-refund';
import { AcquisitionBeverageRestitution } from './acquisition-beverage-restitution';
import { AcquisitionRegularizations } from './acquisition-regularizations';
import { AcquisitionOperationLink } from './acquisition-operation-link';
import { AcquisitionImportsExclusions } from './acquisition-imports-exclusions';
import { AcquisitionAllocationEditor } from './acquisition-allocation';
import { acquisitionRightVersion } from '@/lib/iva-acquisition-right';
import {
acquisitionBook,
acquisitionCurrent,
acquisitionExcludedFields,
acquisitionPlanReferences,
acquisitionReportRows,
recordAcquisitions,
newAcquisition,
reuseAcquisitions,
saveAcquisitionBook,
type Acquisition,
type AcquisitionBook,
} from '@/lib/iva-acquisitions';
import {
acquisitionTreatmentIssue,
continueWithoutFormerExploration,
formerExplorationSource,
type AcquisitionTreatment,
} from '@/lib/iva-acquisition-continuity';
export function SimulationAcquisitions({
draft,
onChange,
}: {
draft: Record<string, unknown>;
onChange: (field: string, value: string) => void;
}) {
const [error, setError] = useState('');
const [manualPeriod, setManualPeriod] = useState('');
let book: AcquisitionBook;
try {
book = acquisitionBook(draft);
} catch (cause) {
return <p role="alert">{String(cause)}</p>;
}
const save = (next: AcquisitionBook) => {
try {
onChange('modelo_a_snapshot', saveAcquisitionBook(draft, next));
setError('');
} catch (cause) {
setError(String(cause));
}
};
const change = (id: string, patch: Partial<Acquisition>) =>
save({
...book,
...('deductionLink' in patch
? { rightVersion: acquisitionRightVersion }
: {}),
rows: book.rows.map((row) =>
row.id === id ? { ...row, ...patch } : row,
),
});
const run = (action: () => void) => {
try {
action();
setError('');
} catch (cause) {
setError(cause instanceof Error ? cause.message : String(cause));
}
};
let references: ReturnType<typeof acquisitionPlanReferences> = [];
try {
references = acquisitionPlanReferences(draft);
} catch {
/* The action reports the concrete currency/source error without replacing saved values. */
}
const labels: Record<string, string> = {
'03': 'Base à taxa reduzida',
'04': 'IVA à taxa reduzida',
'05': 'Isentas com direito à dedução',
'06': 'Operações art.18(1)(b)',
'07': 'Isentas sem direito',
'11': 'Importações',
'12': 'Regularizações a favor do sujeito passivo',
'13': 'Regularizações a favor do Estado',
};
const select = (
row: Acquisition,
key: keyof Acquisition,
label: string,
options: Array<[string, string]>,
) => (
<label className="block" key={key}>
{label}
<select
className="block rounded border p-2"
value={inputText(row[key] ?? '')}
onChange={(e) => change(row.id, { [key]: e.target.value })}
>
<option value="">Por indicar</option>
{options.map(([value, text]) => (
<option key={value} value={value}>
{text}
</option>
))}
</select>
</label>
);
const yesNo: Array<[string, string]> = [
['yes', 'Sim'],
['no', 'Não'],
];
return (
<section
aria-label="Aquisições e apuramento parcial Modelo A"
className="space-y-4 rounded border p-3"
>
<h4 className="font-semibold">
Compras e apuramento parcial do Modelo A
</h4>
<p>
Oportunidade em tratamento:{' '}
{inputText(draft.context_opportunity || 'entrada geral')}. Moeda
de origem: {book.currency || 'por indicar'}. A compra registada
não gera automaticamente IVA dedutível.
</p>
{book.currency && draft.currency !== book.currency && (
<p role="alert">
As compras conservam {book.currency}; o projecto está em{' '}
{inputText(draft.currency || 'moeda por indicar')}. Reveja a
moeda no projecto. Os montantes e o resultado anterior não
foram convertidos ou reinterpretados.
</p>
)}
{!book.currency && !!draft.currency && (
<button
type="button"
className="underline"
onClick={() =>
save({
...book,
currency: String(draft.currency),
rows: book.rows.map((row) =>
row.sourceCurrency
? row
: {
...row,
sourceCurrency: String(
draft.currency,
),
},
),
})
}
>
Confirmar explicitamente {inputText(draft.currency)} como
moeda de origem dos valores sem moeda, conservando os
montantes
</button>
)}
<p>
Aquisições em MZN/2026,ligadas à operação de destino:com
direito,sem direito ou indeterminadas. A/B/C/D distinguem
compras internas,importações de bens,exclusões/excepções e
afectação/proporções. Documento e dedução no mesmo mês;afectação
mista,proporções18(6),dedução posterior e regularizações
conservam percursos próprios. Nas projecções,cada confirmação é
um pressuposto. Compras internas:indique dois valores entre
líquido,IVA e bruto. Importações:IVA expressamente identificado
no bilhete;custos líquido/bruto são opcionais,sem calcular
direitos ou presumir taxa.
</p>
<label className="block">
Adquirente sujeito passivo de IVA no regime normal (efectivo ou
pressuposto da projecção)
<select
className="block rounded border p-2"
value={book.taxpayerRegistered ?? ''}
onChange={(e) =>
save({
...book,
taxpayerRegistered: e.target
.value as AcquisitionBook['taxpayerRegistered'],
})
}
>
<option value="">Por confirmar</option>
<option value="yes">Sim</option>
<option value="no">
Não / outro enquadramento — fora deste lote
</option>
</select>
</label>
{error && <p role="alert">{error}</p>}
<details className="rounded border p-2">
<summary>
Indicar mês da aquisição pelo documento ou pressuposto da
projecção
</summary>
<p>
Para compras introduzidas directamente, indique o mês
aplicável sem depender de ter vendas nesse mês. A
reutilização de valores do orçamento mantém a conferência da
origem.
</p>
<label>
Mês da aquisição
<input
type="month"
min="2026-01"
max="2026-12"
value={manualPeriod}
onChange={(e) => setManualPeriod(e.target.value)}
/>
</label>
<button
type="button"
onClick={() =>
run(() => {
if (!/^2026-(0[1-9]|1[0-2])$/.test(manualPeriod))
throw new Error(
'Indique um mês válido de2026.',
);
if (
book.periods.some(
(p) => p.period === manualPeriod,
)
)
throw new Error(
'Esse mês já consta da cobertura; valores conservados.',
);
save({
...book,
periods: [
...book.periods,
{
period: manualPeriod,
complete: false,
absent: [],
},
],
});
})
}
>
Acrescentar mês indicado
</button>
</details>
<button
type="button"
className="underline"
onClick={() => run(() => save(reuseAcquisitions(draft, book)))}
>
Reutilizar compras e períodos existentes
</button>
<button
type="button"
className="ml-3 underline"
disabled={!book.periods.length}
onClick={() =>
save({
...book,
rightVersion: acquisitionRightVersion,
rows: [
...book.rows,
newAcquisition(
crypto.randomUUID(),
book.periods[0]?.period ?? '',
book.currency,
),
],
})
}
>
Acrescentar aquisição em falta
</button>
{book.rows.map((row) => (
<details key={row.id} className="rounded border p-3">
<summary>
{row.description || 'Aquisição adicional'} —{' '}
{row.period} — campo{row.field}
</summary>
<AcquisitionAllocationEditor
draft={draft}
row={row}
onChange={(patch) => change(row.id, patch)}
/>
{!row.d?.method && (
<AcquisitionOperationLink
draft={draft}
row={row}
onChange={(patch) => change(row.id, patch)}
/>
)}
<AcquisitionImportsExclusions
draft={draft}
row={row}
onChange={(patch) => change(row.id, patch)}
/>
<details className="rounded border p-2">
<summary>
Enquadramento especial da aquisição / referência
anterior
</summary>
<label>
Tratamento da aquisição
<select
value={row.acquisitionTreatment ?? ''}
onChange={(e) =>
row.acquisitionTreatment ===
'former_exploration_exemption' &&
e.target.value !==
'former_exploration_exemption'
? run(() =>
save({
...book,
rows: book.rows.map((item) =>
item.id === row.id
? continueWithoutFormerExploration(
item,
)
: item,
),
}),
)
: change(row.id, {
acquisitionTreatment: e.target
.value as AcquisitionTreatment,
})
}
>
<option value="">
Enquadramento normal; sem benefício especial
indicado
</option>
<option value="former_exploration_exemption">
Antiga isenção de prospecção/pesquisa
—14(2)(c),eliminada
</option>
<option value="normal">
Continuidade pelo enquadramento normal
</option>
</select>
</label>
{acquisitionTreatmentIssue(
row.acquisitionTreatment,
) && (
<p role="status">
{acquisitionTreatmentIssue(
row.acquisitionTreatment,
)}
</p>
)}
{row.acquisitionTreatment ===
'former_exploration_exemption' && (
<>
<p>{formerExplorationSource}</p>
<p>
A continuidade conserva
montantes,moeda,mês,documento e respostas
existentes. Não introduz taxa nem altera o
IVA declarado; reveja as condições antes de
calcular.
</p>
<button
type="button"
onClick={() =>
run(() =>
save({
...book,
rows: book.rows.map((item) =>
item.id === row.id
? continueWithoutFormerExploration(
item,
)
: item,
),
}),
)
}
>
Continuar aquisição sem esta isenção e rever
condições
</button>
</>
)}
{row.acquisitionTreatmentOrigin && (
<p>
Origem conservada: antiga
referência14(2)(c),eliminada.{' '}
{row.acquisitionTreatment ===
'former_exploration_exemption'
? 'A continuidade normal foi escolhida numa versão anterior; a referência antiga está agora seleccionada para análise.'
: 'Continuidade normal escolhida expressamente; não é nova concessão de isenção.'}
</p>
)}
</details>
<p>
Origem:{' '}
{row.sourceMonth === null
? 'Introdução directa'
: `Plano, mês ${row.sourceMonth} (${row.sourcePeriod ?? 'por indicar'}): ${row.sourceAmount ?? 'sem informação'} ${row.sourceCurrency}`}
. O mês da encomenda/orçamento não determina a data do
documento ou a dedução. Alterações não substituem o
resultado anterior.
</p>
<div className="grid gap-3 md:grid-cols-2">
<label>
Descrição
<input
className="block rounded border p-2"
value={row.description}
onChange={(e) =>
change(row.id, {
description: e.target.value,
})
}
/>
</label>
{row.sourceMonth === null && (
<label>
Reutilizar valor existente, sem presumir que o
orçamento é uma factura
<select
className="block rounded border p-2"
value=""
onChange={(e) =>
run(() => {
const ref = references.find(
(r) => r.key === e.target.value,
);
if (ref)
change(row.id, {
sourceMonth: ref.month,
sourcePeriod: ref.period,
sourceField: ref.kind,
sourceAmount: ref.amount,
sourceCurrency:
ref.currency,
...(ref.kind === 'asset'
? {}
: {
period: ref.period,
}),
});
})
}
>
<option value="">
Escolher apenas se corresponde a esta
aquisição
</option>
{references
.filter(
(ref) =>
ref.kind ===
(row.field === '08'
? 'asset'
: row.field === '10'
? 'operating'
: 'inventory'),
)
.map((ref) => (
<option
key={ref.key}
value={ref.key}
>
{ref.label}:{' '}
{ref.amount ?? 'sem informação'}{' '}
{ref.currency}
</option>
))}
</select>
</label>
)}
{select(row, 'field', 'Categoria', [
['08', '08 — Imobilizado'],
['09', '09 — Existências'],
['10', '10 — Outros bens e serviços'],
...(row.bc
? [
['11', '11 — Importações de bens'] as [
string,
string,
],
]
: []),
])}
{row.field === '11' && (
<label>
<input
type="checkbox"
checked={row.investmentAsset ?? false}
onChange={(e) =>
change(row.id, {
investmentAsset: e.target.checked,
})
}
/>
Bem de investimento importado — incluir no
controlo de activos;conserva campo11 e condições
próprias da importação.
</label>
)}
{select(row, 'origin', 'Natureza dos dados', [
['projected', 'Projectados / pressupostos'],
['effective', 'Efectivos declarados'],
])}
<label>
Período de dedução
<input
className="block rounded border p-2"
type="month"
value={row.period}
onChange={(e) =>
change(row.id, { period: e.target.value })
}
/>
</label>
{row.sourceMonth !== null && (
<label>
O valor de origem corresponde a
<select
className="block rounded border p-2"
value={row.sourceBasis}
onChange={(e) => {
const basis = e.target
.value as Acquisition['sourceBasis'];
change(row.id, {
sourceBasis: basis,
...(basis === 'net' && !row.net
? {
net:
row.sourceAmount ??
'',
}
: basis === 'gross' &&
!row.gross
? {
gross:
row.sourceAmount ??
'',
}
: {}),
});
}}
>
<option value="">Por classificar</option>
<option value="net">Valor sem IVA</option>
<option value="gross">Valor com IVA</option>
</select>
</label>
)}
{(['net', 'vat', 'gross'] as const).map(
(field, index) => (
<label key={field}>
{
[
'Valor líquido',
'IVA indicado',
'Valor bruto',
][index]
}{' '}
({row.sourceCurrency})
<input
className="block rounded border p-2"
inputMode="decimal"
value={row[field]}
onChange={(e) =>
change(row.id, {
[field]: e.target.value,
})
}
/>
</label>
),
)}
{select(
row,
'documentType',
'Tipo de documento',
row.domestic === 'no' && row.bc
? [
[
'customs',
'Bilhete de despacho de importação',
],
]
: [
['invoice', 'Factura'],
['equivalent', 'Documento equivalente'],
],
)}
<label>
Referência do documento / previsão
<input
className="block rounded border p-2"
value={row.documentRef}
onChange={(e) =>
change(row.id, {
documentRef: e.target.value,
})
}
/>
</label>
<label>
Linha do documento, se discriminado em várias
aquisições
<input
className="block rounded border p-2"
value={row.documentLine}
onChange={(e) =>
change(row.id, {
documentLine: e.target.value,
})
}
/>
</label>
<label>
Data do documento / prevista
<input
className="block rounded border p-2"
type="date"
value={row.documentDate}
onChange={(e) =>
change(row.id, {
documentDate: e.target.value,
})
}
/>
</label>
{!(row.bc && row.domestic === 'no') && (
<label>
Fornecedor / previsto
<input
className="block rounded border p-2"
value={row.supplier}
onChange={(e) =>
change(row.id, {
supplier: e.target.value,
})
}
/>
</label>
)}
{select(row, 'domestic', 'Aquisição interna', yesNo)}
{row.acquisitionTreatment !==
'former_exploration_exemption' && (
<>
{!(row.bc && row.domestic === 'no') &&
select(
row,
'supplierRegistered',
'Fornecedor inscrito no IVA',
yesNo,
)}
{select(
row,
'legalDocument',
'Documento emitido em forma legal',
yesNo,
)}
{select(
row,
'held',
'Documento na posse do adquirente / previsto na projecção',
yesNo,
)}
{select(row, 'allocation', 'Afectação', [
[
'taxable_only',
row.d?.method === 'reduced'
? 'Exclusiva à origem18(6)'
: row.deductionLink
? 'Exclusiva à operação ligada'
: 'Exclusiva a operações tributadas',
],
[
'mixed',
row.d
? 'Mista / parcial — métodoD'
: 'Mista / parcial — preparar métodoD',
],
[
'no_right',
'Operações sem direito à dedução',
],
])}
{select(
row,
'exclusion',
'Exclusões de dedução',
[
[
'none',
'Sem exclusões ou casos especiais dos arts.18–20',
],
['excluded', 'Despesa excluída'],
[
'special',
'Veículos, combustível, viagens, alojamento, comunicações, luxo ou outra situação especial — por enquadrar',
],
],
)}
{select(
row,
'integrity',
'Operação real / pressuposto regular, sem simulação, margem de segunda mão ou impedimentos do art.18(3–5)',
yesNo,
)}
</>
)}
</div>
<p>
Fundamento:CIVA18–21,fontes e versão no resultado.{' '}
{row.d
? 'MétodoD aplicado apenas à parcela elegível pelas condiçõesB/C;fracções exactas,sem regularização anual automática ou restituiçãoL3.'
: row.bc
? 'Aplicam-se apenas as exclusões/excepções documentadas escolhidas;gasóleo50% conserva fracção exacta. Sem pro rata,proporções18(6) ou regularização neste incremento.'
: 'Afectação parcial,despesa especial e dedução posterior exigem percurso próprio;use a análiseC quando aplicável.'}
</p>
{row.sourceMonth === null && (
<button
type="button"
className="mr-3 underline"
onClick={() =>
save({
...book,
rows: book.rows.filter(
(r) => r.id !== row.id,
),
})
}
>
Remover esta aquisição introduzida directamente
</button>
)}
{row.sourceMonth !== null && (
<button
type="button"
className="underline"
onClick={() =>
run(() => {
const ref = acquisitionPlanReferences(
draft,
).find(
(r) =>
r.month === row.sourceMonth &&
r.kind ===
(row.sourceField ??
'inventory'),
);
if (!ref)
throw new Error(
'Origem não disponível; conserve o registo e reveja o plano.',
);
if (draft.currency !== row.sourceCurrency)
throw new Error(
'Moeda divergente; não adoptar montantes de outra moeda.',
);
change(row.id, {
sourceAmount: ref.amount,
sourcePeriod: ref.period,
...(row.sourceBasis === 'net'
? {
net: ref.amount ?? '',
gross: '',
}
: row.sourceBasis === 'gross'
? {
gross: ref.amount ?? '',
net: '',
}
: {}),
});
})
}
>
Adoptar explicitamente o valor actual do plano
(conserva histórico)
</button>
)}
</details>
))}
<AcquisitionRegularizations draft={draft} onChange={onChange} />
{book.periods.map((period) => (
<details key={period.period} className="rounded border p-3">
<summary>
{period.period} — cobertura das parcelas —{' '}
{period.complete &&
period.absent.length ===
acquisitionExcludedFields.length
? 'confirmada'
: 'por completar'}
</summary>
{book.rows.some((r) => r.bc) && (
<label className="block">
<input
type="checkbox"
checked={period.importsComplete ?? false}
onChange={(e) =>
save({
...book,
periods: book.periods.map((p) =>
p.period === period.period
? {
...p,
importsComplete:
e.target.checked,
}
: p,
),
})
}
/>{' '}
Incluí todas as importações de bens/campo11 deste
período,mesmo isentas ou sem direito;sem somar
novamente IVA de serviços incluídos na base.
</label>
)}
<label className="block">
<input
type="checkbox"
checked={period.complete}
onChange={(e) =>
save({
...book,
periods: book.periods.map((p) =>
p.period === period.period
? {
...p,
complete: e.target.checked,
}
: p,
),
})
}
/>{' '}
Incluí todas as aquisições08–10 deste período, incluindo
as sem direito à dedução; uma lista vazia significa que
não houve essas aquisições.
</label>
<p>
Campos fora deste lote: assinale apenas quando não
existiram as respectivas operações. Uma parcela não
assinalada permanece indeterminada.
</p>
{acquisitionExcludedFields.map((field) => (
<label className="block" key={field}>
<input
type="checkbox"
checked={period.absent.includes(field)}
onChange={(e) =>
save({
...book,
periods: book.periods.map((p) =>
p.period === period.period
? {
...p,
absent: e.target.checked
? [...p.absent, field]
: p.absent.filter(
(f) =>
f !== field,
),
}
: p,
),
})
}
/>{' '}
Sem operações no campo{field} — {labels[field]}
</label>
))}
</details>
))}
<button
type="button"
disabled={!book.periods.length}
className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50"
onClick={() => run(() => save(recordAcquisitions(draft, book)))}
>
Calcular e guardar versão do apuramento parcial
</button>
<p>
O apuramento01–18 conserva o seu resultado antes dos créditos. O
fechoA/B abaixo reutiliza a versão congelada,sem ordem de
pagamento,submissãoAT ou promessa de reembolso.
</p>
<AcquisitionClosure draft={draft} onChange={onChange} />
<AcquisitionSubstitution draft={draft} onChange={onChange} />
<AcquisitionRefund draft={draft} onChange={onChange} />
<AcquisitionBeverageRestitution draft={draft} onChange={onChange} />
<FiscalCalculationGaps draft={draft} />
<FiscalL3Preparation draft={draft} />
{book.result && (
<>
<p role="status">
{acquisitionCurrent(draft, book)
? 'Resultado correspondente às entradas guardadas.'
: 'Entradas ou origem alteradas: resultado histórico preservado; actualizar o cálculo.'}
</p>
<dl>
{acquisitionReportRows(draft).map(
([, label, value, origin], i) => (
<div key={i}>
<dt className="font-semibold">{label}</dt>
<dd>
{value}
<small className="block">
{origin}
</small>
</dd>
</div>
),
)}
</dl>
</>
)}
</section>
);
}