423 lines
16 KiB
TypeScript
423 lines
16 KiB
TypeScript
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
|
export const goodsPresumptionVersion = 'goods-presumptions/1';
|
|
export const goodsPresumptionSource =
|
|
'CIVA Lei13/2016,3(3)d PDF6/p1426(264):mercadorias enviadas à consignação não devolvidas em180dias da entrega;3(4) PDF7/p1426(265):salvo prova em contrário,presunção de transmissão dos bens adquiridos/importados/produzidos não encontrados nos estabelecimentos e dos consumidos em quantidades excessivas face ao volume de produção;presunção de aquisição dos bens encontrados nesses locais.Lei22/2022 não altera3;Lei10/2025 PDF12 mantém3(3)d/3(4).Não são embalagensP102 nem aplicação automática da margemP092/P093/P125.';
|
|
export const goodsPresumptionTreatments = [
|
|
{
|
|
id: 'consignment_180',
|
|
branch: 'P123',
|
|
label: 'Consignação — não devolução em180dias',
|
|
},
|
|
{
|
|
id: 'missing_goods',
|
|
branch: 'P126',
|
|
label: 'Presunção de transmissão — bens não localizados',
|
|
},
|
|
{
|
|
id: 'excess_consumption',
|
|
branch: 'P126',
|
|
label: 'Presunção de transmissão — consumo excessivo',
|
|
},
|
|
{
|
|
id: 'found_goods',
|
|
branch: 'P126',
|
|
label: 'Presunção de aquisição — bens encontrados',
|
|
},
|
|
] as const;
|
|
export const goodsPresumptionConditions = {
|
|
consignment_180: [
|
|
[
|
|
'consignment',
|
|
'Mercadorias enviadas à consignação,com entrega ao destinatário identificado',
|
|
],
|
|
[
|
|
'notReturned',
|
|
'O conjunto identificado não foi devolvido até à data de conferência',
|
|
],
|
|
],
|
|
missing_goods: [
|
|
[
|
|
'acquiredImportedProduced',
|
|
'Bens adquiridos,importados ou produzidos pelo sujeito passivo',
|
|
],
|
|
[
|
|
'notLocated',
|
|
'Não se encontram em nenhum dos estabelecimentos do sujeito passivo,segundo o levantamento identificado',
|
|
],
|
|
],
|
|
excess_consumption: [
|
|
[
|
|
'acquiredImportedProduced',
|
|
'Bens adquiridos,importados ou produzidos pelo sujeito passivo',
|
|
],
|
|
[
|
|
'excessive',
|
|
'Consumo em quantidades excessivas,tendo em conta o volume de produção identificado',
|
|
],
|
|
],
|
|
found_goods: [
|
|
[
|
|
'found',
|
|
'Bens encontrados em estabelecimento do sujeito passivo identificado',
|
|
],
|
|
],
|
|
} as const;
|
|
export function presumptionConditions(
|
|
t: string,
|
|
): ReadonlyArray<readonly [string, string]> {
|
|
return (
|
|
goodsPresumptionConditions[
|
|
t as keyof typeof goodsPresumptionConditions
|
|
] ?? []
|
|
);
|
|
}
|
|
type Choice = '' | 'yes' | 'no';
|
|
export type GoodsPresumptionRow = {
|
|
id: string;
|
|
treatment: string;
|
|
description: string;
|
|
period: string;
|
|
origin: 'projected' | 'effective';
|
|
operationDate: string;
|
|
operationRef: string;
|
|
documentRef: string;
|
|
documentDate: string;
|
|
goodsRef: string;
|
|
scope: string;
|
|
evidence: string;
|
|
deliveryDate: string;
|
|
recipientRef: string;
|
|
inventoryRef: string;
|
|
productionRef: string;
|
|
quantity: string;
|
|
quantityUnit: string;
|
|
contraryProof: Choice;
|
|
contraryProofRef: string;
|
|
conditions: Partial<Record<string, Choice>>;
|
|
};
|
|
type LineResult = {
|
|
id: string;
|
|
status: 'determined' | 'not_applicable' | 'indeterminate';
|
|
classification:
|
|
| 'consignment_transmission'
|
|
| 'presumed_transmission'
|
|
| 'presumed_acquisition'
|
|
| 'rebutted'
|
|
| 'not_applicable'
|
|
| 'indeterminate';
|
|
deadline: string | null;
|
|
elapsedDays: number | null;
|
|
baseAmount: null;
|
|
vat: null;
|
|
rate: null;
|
|
reason: string;
|
|
};
|
|
export type GoodsPresumptionCalculation = {
|
|
version: string;
|
|
revision: number;
|
|
calculatedAt: string;
|
|
opportunity: string;
|
|
location: string;
|
|
sourceKey: string;
|
|
source: string;
|
|
inputs: GoodsPresumptionRow[];
|
|
rows: LineResult[];
|
|
};
|
|
export type GoodsPresumptionBook = {
|
|
rows: GoodsPresumptionRow[];
|
|
result?: GoodsPresumptionCalculation;
|
|
history?: GoodsPresumptionCalculation[];
|
|
};
|
|
type Store = ReturnType<typeof readAcquisitionStore> & {
|
|
goodsPresumptions?: Record<string, GoodsPresumptionBook>;
|
|
};
|
|
export function newGoodsPresumption(id: string): GoodsPresumptionRow {
|
|
return {
|
|
id,
|
|
treatment: '',
|
|
description: '',
|
|
period: '',
|
|
origin: 'projected',
|
|
operationDate: '',
|
|
operationRef: '',
|
|
documentRef: '',
|
|
documentDate: '',
|
|
goodsRef: '',
|
|
scope: '',
|
|
evidence: '',
|
|
deliveryDate: '',
|
|
recipientRef: '',
|
|
inventoryRef: '',
|
|
productionRef: '',
|
|
quantity: '',
|
|
quantityUnit: '',
|
|
contraryProof: '',
|
|
contraryProofRef: '',
|
|
conditions: {},
|
|
};
|
|
}
|
|
export function switchPresumptionTreatment(
|
|
row: GoodsPresumptionRow,
|
|
treatment: string,
|
|
): GoodsPresumptionRow {
|
|
return { ...row, treatment, conditions: {}, contraryProof: '' };
|
|
}
|
|
function readStore(draft: Record<string, unknown>): Store {
|
|
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
|
if (
|
|
store.goodsPresumptions !== undefined &&
|
|
(!store.goodsPresumptions ||
|
|
typeof store.goodsPresumptions !== 'object' ||
|
|
Array.isArray(store.goodsPresumptions))
|
|
)
|
|
throw new Error('Presunções não recuperáveis;originais preservados.');
|
|
const fields = Object.keys(newGoodsPresumption('')).filter(
|
|
(k) => k !== 'conditions',
|
|
);
|
|
for (const book of Object.values(store.goodsPresumptions ?? {}))
|
|
if (
|
|
!book ||
|
|
!Array.isArray(book.rows) ||
|
|
(book.history !== undefined && !Array.isArray(book.history)) ||
|
|
book.rows.some(
|
|
(row) =>
|
|
!row ||
|
|
fields.some(
|
|
(k) =>
|
|
typeof row[k as keyof GoodsPresumptionRow] !==
|
|
'string',
|
|
) ||
|
|
!row.conditions ||
|
|
typeof row.conditions !== 'object' ||
|
|
Array.isArray(row.conditions),
|
|
)
|
|
)
|
|
throw new Error(
|
|
'Linhas não recuperáveis;originais conservados,sem lista vazia por defeito.',
|
|
);
|
|
return store;
|
|
}
|
|
export function goodsPresumptionBook(
|
|
draft: Record<string, unknown>,
|
|
): GoodsPresumptionBook {
|
|
return (
|
|
readStore(draft).goodsPresumptions?.[acquisitionScope(draft)] ?? {
|
|
rows: [],
|
|
}
|
|
);
|
|
}
|
|
export function saveGoodsPresumptionBook(
|
|
draft: Record<string, unknown>,
|
|
book: GoodsPresumptionBook,
|
|
): string {
|
|
const store = readStore(draft),
|
|
encoded = JSON.stringify({
|
|
...store,
|
|
goodsPresumptions: {
|
|
...store.goodsPresumptions,
|
|
[acquisitionScope(draft)]: book,
|
|
},
|
|
});
|
|
readStore({ ...draft, modelo_a_snapshot: encoded });
|
|
return encoded;
|
|
}
|
|
function date(s: string): number | null {
|
|
const n = Date.parse(s + 'T00:00:00Z');
|
|
return /^\d{4}-\d{2}-\d{2}$/.test(s) &&
|
|
Number.isFinite(n) &&
|
|
new Date(n).toISOString().slice(0, 10) === s
|
|
? n
|
|
: null;
|
|
}
|
|
export function evaluateGoodsPresumption(row: GoodsPresumptionRow): LineResult {
|
|
const result: LineResult = {
|
|
id: row.id,
|
|
status: 'indeterminate',
|
|
classification: 'indeterminate',
|
|
deadline: null,
|
|
elapsedDays: null,
|
|
baseAmount: null,
|
|
vat: null,
|
|
rate: null,
|
|
reason: '',
|
|
};
|
|
const stop = (
|
|
reason: string,
|
|
status: LineResult['status'] = 'indeterminate',
|
|
classification: LineResult['classification'] = status ===
|
|
'not_applicable'
|
|
? 'not_applicable'
|
|
: result.classification,
|
|
) => ({ ...result, status, classification, reason });
|
|
const rule = goodsPresumptionTreatments.find((t) => t.id === row.treatment);
|
|
if (!rule) return stop('Identifique o percurso;sem presunção por defeito.');
|
|
const operation = date(row.operationDate);
|
|
if (
|
|
operation === null ||
|
|
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
|
|
row.period !== row.operationDate.slice(0, 7) ||
|
|
!['projected', 'effective'].includes(row.origin)
|
|
)
|
|
return stop(
|
|
'Identifique data de conferência/mês2026 e dados efectivos ou pressupostos expressos.',
|
|
);
|
|
if (
|
|
![
|
|
row.description,
|
|
row.operationRef,
|
|
row.documentRef,
|
|
row.goodsRef,
|
|
row.scope,
|
|
row.evidence,
|
|
].every((v) => !!v.trim()) ||
|
|
date(row.documentDate) === null
|
|
)
|
|
return stop(
|
|
'Identifique bens,conjunto/âmbito,operação,documento/data e factos ou pressupostos;reutilize dados disponíveis,sem certificado/aprovador novo.',
|
|
);
|
|
const conditions = presumptionConditions(row.treatment);
|
|
if (conditions.some(([k]) => row.conditions[k] === 'no'))
|
|
return stop(
|
|
'Os factos declarados afastam este percurso específico;isso não determina isenção,IVA zero ou a classificação de outra operação.',
|
|
'not_applicable',
|
|
);
|
|
if (conditions.some(([k]) => row.conditions[k] !== 'yes'))
|
|
return stop(
|
|
'Factos próprios por esclarecer;desconhecido não é não aplicável.',
|
|
);
|
|
if (row.treatment === 'consignment_180') {
|
|
const delivery = date(row.deliveryDate);
|
|
if (
|
|
delivery === null ||
|
|
delivery > operation ||
|
|
!row.recipientRef.trim()
|
|
)
|
|
return stop(
|
|
'Identifique entrega ao destinatário e data anterior ou igual à conferência;sem prazo presumido.',
|
|
);
|
|
result.elapsedDays = (operation - delivery) / 86400000;
|
|
result.deadline = new Date(delivery + 180 * 86400000)
|
|
.toISOString()
|
|
.slice(0, 10);
|
|
if (result.elapsedDays <= 180)
|
|
return stop(
|
|
`Prazo de180dias ainda não ultrapassado na conferência;limite ${result.deadline},decorridos ${result.elapsedDays}dias.Sem transmissão por não devolução nesta data. O dia180 conserva o prazo até ao fim do dia.`,
|
|
`not_applicable`,
|
|
);
|
|
return stop(
|
|
`P123:mercadorias identificadas enviadas à consignação,não devolvidas após180dias desde entrega${row.deliveryDate};limite${result.deadline};decorridos${result.elapsedDays}dias. Transmissão3(3)d identificada no recorte de conferência;não são embalagensP102. Data de exigibilidade/base/IVA não calculadas neste percurso;devolução posterior requer tratamento próprio,sem apagar histórico.`,
|
|
`determined`,
|
|
'consignment_transmission',
|
|
);
|
|
}
|
|
if (!row.inventoryRef.trim())
|
|
return stop(
|
|
'Identifique levantamento/inventário e os estabelecimentos abrangidos;ausência numa única localização não demonstra ausência em todos os estabelecimentos.',
|
|
);
|
|
if (
|
|
row.treatment === 'excess_consumption' &&
|
|
(!row.productionRef.trim() ||
|
|
!row.quantityUnit.trim() ||
|
|
!/^\d{1,15}(\.\d{1,6})?$/.test(row.quantity) ||
|
|
!/[1-9]/.test(row.quantity))
|
|
)
|
|
return stop(
|
|
'Identifique quantidade/unidade consumida e volume de produção/comparação que fundamenta o excesso;sem percentagem ou limiar inventado.',
|
|
);
|
|
if (row.contraryProof === 'yes')
|
|
return row.contraryProofRef.trim()
|
|
? stop(
|
|
'Prova em contrário identificada para o conjunto/âmbito declarado:presunção3(4) afastada,sem atribuir isenção ou IVA zero e sem apagar histórico.',
|
|
'not_applicable',
|
|
'rebutted',
|
|
)
|
|
: stop(
|
|
'Identifique o âmbito e referência da prova em contrário declarada;sem afastar presunção só por seleccionar Sim.',
|
|
);
|
|
if (row.contraryProof !== 'no')
|
|
return stop(
|
|
'Existência de prova em contrário por esclarecer;presunção ilidível,não conclusão definitiva nem infracção presumida.',
|
|
);
|
|
return stop(
|
|
`P126:presunção ilidível3(4) de ${row.treatment === 'found_goods' ? 'aquisição dos bens encontrados' : 'transmissão dos bens ' + (row.treatment === 'missing_goods' ? 'não localizados nos estabelecimentos abrangidos' : 'consumidos em quantidades excessivas face ao volume de produção')};segundo factos/pressupostos e âmbito identificados,sem prova em contrário declarada. Não comprova factura,aquisição efectiva,isenção ou dedução.Nenhuma base/taxa/IVA calculada;manter a possibilidade de prova em contrário.`,
|
|
`determined`,
|
|
row.treatment === 'found_goods'
|
|
? 'presumed_acquisition'
|
|
: 'presumed_transmission',
|
|
);
|
|
}
|
|
function key(
|
|
draft: Record<string, unknown>,
|
|
book: GoodsPresumptionBook,
|
|
): string {
|
|
return JSON.stringify({
|
|
version: goodsPresumptionVersion,
|
|
scope: acquisitionScope(draft),
|
|
location: draft.selection_labels ?? {},
|
|
rows: book.rows,
|
|
});
|
|
}
|
|
export function goodsPresumptionCurrent(
|
|
draft: Record<string, unknown>,
|
|
book: GoodsPresumptionBook,
|
|
): boolean {
|
|
return !!book.result && book.result.sourceKey === key(draft, book);
|
|
}
|
|
export function recordGoodsPresumptions(
|
|
draft: Record<string, unknown>,
|
|
book: GoodsPresumptionBook,
|
|
): GoodsPresumptionBook {
|
|
if (
|
|
!book.rows.length ||
|
|
new Set(book.rows.map((r) => r.id)).size !== book.rows.length
|
|
)
|
|
throw new Error(
|
|
'Indique operações com identificadores distintos;originais conservados.',
|
|
);
|
|
const result: GoodsPresumptionCalculation = {
|
|
version: goodsPresumptionVersion,
|
|
revision: (book.result?.revision ?? 0) + 1,
|
|
calculatedAt: new Date().toISOString(),
|
|
opportunity: acquisitionScope(draft),
|
|
location: JSON.stringify(draft.selection_labels ?? {}),
|
|
sourceKey: key(draft, book),
|
|
source: goodsPresumptionSource,
|
|
inputs: structuredClone(book.rows),
|
|
rows: book.rows.map(evaluateGoodsPresumption),
|
|
};
|
|
const next = {
|
|
...book,
|
|
result,
|
|
history: [
|
|
...(book.history ?? []),
|
|
...(book.result ? [book.result] : []),
|
|
],
|
|
};
|
|
saveGoodsPresumptionBook(draft, next);
|
|
return next;
|
|
}
|
|
export function goodsPresumptionReportRows(
|
|
draft: Record<string, unknown>,
|
|
): Array<[string, string, string, string]> {
|
|
try {
|
|
const book = goodsPresumptionBook(draft),
|
|
r = book.result;
|
|
if (!r) return [];
|
|
return r.inputs.map((row, i) => [
|
|
'IVA mensal',
|
|
`${goodsPresumptionTreatments.find((t) => t.id === row.treatment)?.branch ?? 'Presunção'} — ${row.period} — ${row.id}`,
|
|
`${row.description};${r.rows[i].reason};base/IVA:Indeterminados`,
|
|
`${r.opportunity};${r.location};${goodsPresumptionCurrent(draft, book) ? 'Resultado actual' : 'Entradas alteradas;resultado histórico conservado'};versão${r.revision};${r.calculatedAt};${r.source};${row.origin};operação ${row.operationRef},${row.operationDate};documento ${row.documentRef},${row.documentDate};bens ${row.goodsRef};âmbito ${row.scope};${row.treatment === 'consignment_180' ? `entrega${row.deliveryDate},destinatário${row.recipientRef},limite${r.rows[i].deadline ?? 'indeterminado'}` : `inventário ${row.inventoryRef};${row.treatment === 'excess_consumption' ? `consumo ${row.quantity} ${row.quantityUnit},produção/comparação ${row.productionRef};` : ''}provaemcontrário ${row.contraryProof},${row.contraryProofRef}`};condições${JSON.stringify(row.conditions)};fundamento ${row.evidence};sem nova dedução/campo11/IVA/margem automática.`,
|
|
]);
|
|
} catch {
|
|
return [
|
|
[
|
|
'IVA mensal',
|
|
'Consignação/presunções',
|
|
'Dados não recuperáveis',
|
|
'Originais conservados;sem classificação por defeito.',
|
|
],
|
|
];
|
|
}
|
|
}
|