524 lines
25 KiB
TypeScript
524 lines
25 KiB
TypeScript
import { inputText } from '../lib/input-text.ts';
|
|
import { useState } from 'react';
|
|
import {
|
|
travelEventBook,
|
|
travelEventCurrent,
|
|
travelEventReportRows,
|
|
travelEventSource,
|
|
travelEventSources,
|
|
newTravelEvent,
|
|
recordTravelEvents,
|
|
saveTravelEventBook,
|
|
switchTravelEvent,
|
|
type TravelEventBook,
|
|
type TravelEventRow,
|
|
} from '@/lib/iva-travel-events';
|
|
export function SimulationTravelEvents({
|
|
draft,
|
|
onChange,
|
|
}: {
|
|
draft: Record<string, unknown>;
|
|
onChange: (field: string, value: string) => void;
|
|
}) {
|
|
const [error, setError] = useState('');
|
|
let book: TravelEventBook;
|
|
try {
|
|
book = travelEventBook(draft);
|
|
} catch (e) {
|
|
return <p role="alert">{String(e)}</p>;
|
|
}
|
|
const save = (next: TravelEventBook) => {
|
|
try {
|
|
onChange('modelo_a_snapshot', saveTravelEventBook(draft, next));
|
|
setError('');
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
};
|
|
const change = (id: string, patch: Partial<TravelEventRow>) =>
|
|
save({
|
|
...book,
|
|
rows: book.rows.map((r) => (r.id === id ? { ...r, ...patch } : r)),
|
|
});
|
|
const fields = (
|
|
row: TravelEventRow,
|
|
list: ReadonlyArray<
|
|
readonly [Exclude<keyof TravelEventRow, 'fxSelection'>, string]
|
|
>,
|
|
) =>
|
|
list.map(([key, label]) => (
|
|
<label className="block" key={key}>
|
|
{label}
|
|
<input
|
|
type={
|
|
key === 'period'
|
|
? 'month'
|
|
: String(key).endsWith('Date') ||
|
|
key === 'legalDeadline'
|
|
? 'date'
|
|
: 'text'
|
|
}
|
|
value={row[key]}
|
|
onChange={(e) => change(row.id, { [key]: e.target.value })}
|
|
/>
|
|
</label>
|
|
));
|
|
const select = (
|
|
row: TravelEventRow,
|
|
key: Exclude<keyof TravelEventRow, 'fxSelection'>,
|
|
label: string,
|
|
options: ReadonlyArray<readonly [string, string]>,
|
|
) => (
|
|
<label>
|
|
{label}
|
|
<select
|
|
value={row[key]}
|
|
onChange={(e) => change(row.id, { [key]: e.target.value })}
|
|
>
|
|
<option value="">Por esclarecer</option>
|
|
{options.map(([value, text]) => (
|
|
<option key={value} value={value}>
|
|
{text}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
);
|
|
const yn = [
|
|
['yes', 'Sim'],
|
|
['no', 'Não'],
|
|
] as const;
|
|
let sources: ReturnType<typeof travelEventSources> = [];
|
|
try {
|
|
sources = travelEventSources(draft);
|
|
} catch {
|
|
/* Origin reader preserves originals. */
|
|
}
|
|
return (
|
|
<details className="rounded border p-3">
|
|
<summary>
|
|
Eventos de viagens e cessação de regimes suspensivos
|
|
</summary>
|
|
<p>
|
|
Conserve separados facto gerador e exigibilidade. Não inferir
|
|
pagamento integral por um sinal, início pela emissão do bilhete
|
|
ou cessação do regime por mudança de armazém. Estes eventos não
|
|
calculam imposto nem concedem benefício.
|
|
</p>
|
|
<p>{travelEventSource}</p>
|
|
{error && <p role="alert">{error}</p>}
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
save({
|
|
...book,
|
|
rows: [
|
|
...book.rows,
|
|
newTravelEvent(
|
|
crypto.randomUUID(),
|
|
inputText(draft.currency ?? ''),
|
|
),
|
|
],
|
|
})
|
|
}
|
|
>
|
|
Acrescentar evento especial
|
|
</button>
|
|
{book.rows.map((row) => {
|
|
const source = sources.find((s) => s.id === row.travelRowId);
|
|
return (
|
|
<details className="rounded border p-3" key={row.id}>
|
|
<summary>
|
|
{row.description || 'Evento por identificar'} —{' '}
|
|
{row.period || 'período por indicar'}
|
|
</summary>
|
|
<label>
|
|
Percurso do evento especial
|
|
<select
|
|
value={row.treatment}
|
|
onChange={(e) =>
|
|
change(
|
|
row.id,
|
|
switchTravelEvent(row, e.target.value),
|
|
)
|
|
}
|
|
>
|
|
<option value="">Por esclarecer</option>
|
|
<option value="travel_special">
|
|
Viagens — pagamento integral/início7(9)
|
|
</option>
|
|
<option value="suspensive_exit">
|
|
Cessação de regime/procedimento7(8)
|
|
</option>
|
|
</select>
|
|
</label>
|
|
{fields(row, [
|
|
['description', 'Evento ou operação identificada'],
|
|
[
|
|
'operationRef',
|
|
'Referência da operação existente',
|
|
],
|
|
['period', 'Mês de contexto do evento'],
|
|
[
|
|
'documentRef',
|
|
'Documento disponível / referência da previsão',
|
|
],
|
|
['documentDate', 'Data do documento / prevista'],
|
|
[
|
|
'reviewDate',
|
|
'Data até à qual os eventos são conferidos',
|
|
],
|
|
[
|
|
'scope',
|
|
'Âmbito e parcela efectivamente abrangidos',
|
|
],
|
|
['evidence', 'Factos ou pressupostos expressos'],
|
|
])}
|
|
{select(row, 'origin', 'Natureza dos dados', [
|
|
['projected', 'Projectados / pressupostos'],
|
|
['effective', 'Efectivos declarados'],
|
|
])}
|
|
{select(
|
|
row,
|
|
'factsConfirmed',
|
|
'Factos correspondem ao percurso e âmbito identificados',
|
|
yn,
|
|
)}
|
|
{row.treatment === 'suspensive_exit' && (
|
|
<>
|
|
{select(
|
|
row,
|
|
'regimeClause',
|
|
'Regime/procedimento identificado no artigo14(1)',
|
|
[
|
|
[
|
|
'a',
|
|
'a — Importação/colocação em regimes ou depósitos indicados',
|
|
],
|
|
[
|
|
'b',
|
|
'b — Expedição/transporte para as zonas ou depósitos e conexos',
|
|
],
|
|
[
|
|
'c',
|
|
'c — Transmissão e serviços conexos sob o regime',
|
|
],
|
|
[
|
|
'd',
|
|
'd — Trânsito, transbordo, draubaque ou importação temporária e conexos',
|
|
],
|
|
],
|
|
)}
|
|
{fields(row, [
|
|
[
|
|
'entryDate',
|
|
'Data de colocação sob o regime/procedimento',
|
|
],
|
|
[
|
|
'entryRef',
|
|
'Referência do regime, bens e operação de entrada',
|
|
],
|
|
])}
|
|
{select(
|
|
row,
|
|
'regimeStatus',
|
|
'Situação do regime na conferência',
|
|
[
|
|
[
|
|
'still',
|
|
'Continua abrangido pelo regime',
|
|
],
|
|
[
|
|
'ceased',
|
|
'Cessou efectivamente o regime',
|
|
],
|
|
],
|
|
)}
|
|
{row.regimeStatus === 'ceased' && (
|
|
<>
|
|
{fields(row, [
|
|
[
|
|
'exitDate',
|
|
'Data da cessação efectiva',
|
|
],
|
|
[
|
|
'exitRef',
|
|
'Referência e âmbito da cessação',
|
|
],
|
|
])}
|
|
{select(
|
|
row,
|
|
'cessationConfirmed',
|
|
'Confirmada cessação fiscal, distinguida de transferência entre regimes ou mudança física',
|
|
yn,
|
|
)}
|
|
</>
|
|
)}
|
|
<p>
|
|
A referência demonstra o facto e o âmbito.
|
|
Não se exige certificado ou autorização
|
|
inventados; não há desembaraço, valor
|
|
aduaneiro, direitos ou ICE automáticos.
|
|
</p>
|
|
</>
|
|
)}
|
|
{row.treatment === 'travel_special' && (
|
|
<>
|
|
<label>
|
|
Operação de viagens já classificada nesta
|
|
oportunidade
|
|
<select
|
|
value={row.travelRowId}
|
|
onChange={(e) =>
|
|
change(row.id, {
|
|
travelRowId: e.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="">
|
|
Escolher operação
|
|
</option>
|
|
{sources.map((s) => (
|
|
<option key={s.id} value={s.id}>
|
|
{s.description} — {s.period} —{' '}
|
|
{s.classification.regime}
|
|
{s.current
|
|
? ''
|
|
: ' — histórico'}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
{source && (
|
|
<>
|
|
<p>
|
|
Origem: {source.operationRef};{' '}
|
|
{source.scope}. Período do registo:{' '}
|
|
{source.period}.{' '}
|
|
{source.classification.reason}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
change(row.id, {
|
|
operationRef:
|
|
row.operationRef ||
|
|
source.operationRef,
|
|
description:
|
|
row.description ||
|
|
source.description,
|
|
documentRef:
|
|
row.documentRef ||
|
|
source.input
|
|
.documentRef,
|
|
documentDate:
|
|
row.documentDate ||
|
|
source.input
|
|
.documentDate,
|
|
currency:
|
|
row.currency ||
|
|
source.input.currency,
|
|
scope:
|
|
row.scope ||
|
|
source.scope,
|
|
})
|
|
}
|
|
>
|
|
Preencher apenas referências em
|
|
falta a partir da origem
|
|
</button>
|
|
<p>
|
|
Referências já preenchidas são
|
|
conservadas; divergências precisam
|
|
de conferência. O período do evento
|
|
pode diferir do período da margem.
|
|
</p>
|
|
</>
|
|
)}
|
|
{select(
|
|
row,
|
|
'fullPaymentState',
|
|
'Estado do pagamento da contraprestação',
|
|
[
|
|
[
|
|
'full',
|
|
'Pagamento integral confirmado',
|
|
],
|
|
['partial', 'Apenas pagamento parcial'],
|
|
['none', 'Sem pagamentos'],
|
|
],
|
|
)}
|
|
{row.fullPaymentState === 'full' &&
|
|
fields(row, [
|
|
[
|
|
'fullPaymentDate',
|
|
'Data do pagamento integral',
|
|
],
|
|
[
|
|
'fullPaymentRef',
|
|
'Referência da liquidação integral / conjunto de recebimentos',
|
|
],
|
|
])}
|
|
{select(
|
|
row,
|
|
'startState',
|
|
'Início da viagem/alojamento e primeiro serviço',
|
|
[
|
|
[
|
|
'started',
|
|
'Primeiro serviço/início ocorrido no cenário conferido',
|
|
],
|
|
['not_started', 'Ainda não iniciado'],
|
|
],
|
|
)}
|
|
{row.startState === 'started' &&
|
|
fields(row, [
|
|
[
|
|
'startDate',
|
|
'Data do primeiro serviço ao cliente',
|
|
],
|
|
[
|
|
'startRef',
|
|
'Referência do primeiro serviço e âmbito',
|
|
],
|
|
])}
|
|
<p>
|
|
«Imediatamente antes» identifica o momento;
|
|
não se subtrai um dia à data. Pagamento e
|
|
início na mesma data não demonstram ordem
|
|
intradiária.
|
|
</p>
|
|
{fields(row, [
|
|
[
|
|
'amount',
|
|
'Contraprestação total da parcela analisada',
|
|
],
|
|
['currency', 'Moeda da contraprestação'],
|
|
])}
|
|
{select(
|
|
row,
|
|
'invoiceRequired',
|
|
'Operação com obrigação de emitir factura25',
|
|
yn,
|
|
)}
|
|
{row.invoiceRequired === 'yes' && (
|
|
<>
|
|
{fields(row, [
|
|
[
|
|
'legalDeadline',
|
|
'Termo do prazo legal de emissão aplicável',
|
|
],
|
|
[
|
|
'deadlineRef',
|
|
'Disposição, calendário e âmbito que fundamentam o prazo',
|
|
],
|
|
])}
|
|
{select(
|
|
row,
|
|
'invoiceState',
|
|
'Factura até à conferência',
|
|
[
|
|
['issued', 'Emitida'],
|
|
['not_issued', 'Não emitida'],
|
|
],
|
|
)}
|
|
{row.invoiceState === 'issued' &&
|
|
fields(row, [
|
|
[
|
|
'invoiceDate',
|
|
'Data de emissão da factura',
|
|
],
|
|
[
|
|
'invoiceRef',
|
|
'Referência da factura',
|
|
],
|
|
])}
|
|
</>
|
|
)}
|
|
{select(
|
|
row,
|
|
'paymentState',
|
|
'Recebimento ou parcela anterior à liquidação integral',
|
|
[
|
|
['none', 'Sem recebimento'],
|
|
[
|
|
'received',
|
|
'Recebimento identificado',
|
|
],
|
|
],
|
|
)}
|
|
{row.paymentState === 'received' &&
|
|
fields(row, [
|
|
[
|
|
'paymentDate',
|
|
'Data do recebimento identificado',
|
|
],
|
|
[
|
|
'paymentAmount',
|
|
'Montante recebido nessa parcela',
|
|
],
|
|
[
|
|
'paymentRef',
|
|
'Referência e âmbito da parcela recebida',
|
|
],
|
|
])}
|
|
<p>
|
|
Se existe sinal anterior e posterior
|
|
liquidação integral, identifique ambos: a
|
|
parcela e o saldo não são somados duas
|
|
vezes. A menção «IVA incluído» não é factura
|
|
emitida por este simulador.
|
|
</p>
|
|
</>
|
|
)}
|
|
<p>
|
|
Mudar percurso conserva campos ocultos e
|
|
referências; reveja as confirmações próprias.
|
|
</p>
|
|
</details>
|
|
);
|
|
})}
|
|
{!!book.rows.length && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
try {
|
|
save(recordTravelEvents(draft, book));
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
}}
|
|
>
|
|
Conferir eventos especiais e guardar versão
|
|
</button>
|
|
)}
|
|
{book.result && (
|
|
<>
|
|
<p role="status">
|
|
{travelEventCurrent(draft, book)
|
|
? 'Resultados correspondem às entradas e origens guardadas.'
|
|
: 'Entradas/origens alteradas; resultado histórico conservado. Reveja o delta.'}{' '}
|
|
{book.history?.length ?? 0} versões anteriores. Sem
|
|
base/IVA/dedução calculados.
|
|
</p>
|
|
<dl>
|
|
{travelEventReportRows(draft).map(
|
|
([, label, value, origin], i) => (
|
|
<div key={i}>
|
|
<dt>{label}</dt>
|
|
<dd>
|
|
{value}
|
|
<small className="block">
|
|
{origin}
|
|
</small>
|
|
</dd>
|
|
</div>
|
|
),
|
|
)}
|
|
</dl>
|
|
</>
|
|
)}
|
|
</details>
|
|
);
|
|
}
|