894 lines
37 KiB
TypeScript
894 lines
37 KiB
TypeScript
import {
|
||
addFractions,
|
||
fraction,
|
||
subtractFractions,
|
||
fractionPresentation,
|
||
type FiscalFraction,
|
||
} from './fiscal-rational.ts';
|
||
import { fiscalMoney } from './irpc-normal-schedules.ts';
|
||
export const normalCostsVersion = 'IRPC-normal-adjustments/6';
|
||
export const costRules = {
|
||
allowance: [
|
||
'Ajudas de custo/deslocação em viatura própria',
|
||
'expenseCorrections',
|
||
'36(1)e',
|
||
],
|
||
representation: [
|
||
'Despesas de representação',
|
||
'expenseCorrections',
|
||
'36(1)f/36(3)',
|
||
],
|
||
advertising: [
|
||
'Publicidade ordinária — limite anual partilhado',
|
||
'expenseCorrections',
|
||
'36(1)n,Lei20/2009',
|
||
],
|
||
vehicle: [
|
||
'Outros encargos com viatura ligeira de passageiros',
|
||
'expenseCorrections',
|
||
'36(4)',
|
||
],
|
||
fuel: [
|
||
'Combustível — prova do activo/locação e consumo normal',
|
||
'expenseCorrections',
|
||
'36(1)i/36(4)',
|
||
],
|
||
rental: [
|
||
'Aluguer sem condutor — parcela de quota não aceite documentada',
|
||
'expenseCorrections',
|
||
'36(1)h/36(4)',
|
||
],
|
||
credit: [
|
||
'Provisão/perda por imparidade de créditos ordinários',
|
||
'expenseCorrections',
|
||
'28(1)a/29',
|
||
],
|
||
stock: [
|
||
'Provisão/perda por imparidade de existências',
|
||
'expenseCorrections',
|
||
'28(1)b',
|
||
],
|
||
litigation: [
|
||
'Provisão para processo judicial em curso',
|
||
'expenseCorrections',
|
||
'28(1)c',
|
||
],
|
||
provision_reversal: [
|
||
'Reversão de provisão antes deduzida',
|
||
'incomeCorrections',
|
||
'28(2)',
|
||
],
|
||
taxed_reversal: [
|
||
'Reversão contabilística de provisão antes tributada',
|
||
'incomeCorrections',
|
||
'17/28;sem dupla tributação da parcela comprovada',
|
||
],
|
||
prior_cost: [
|
||
'Custo de exercício anterior incluído no resultado actual',
|
||
'timing',
|
||
'18(2)',
|
||
],
|
||
prior_income: [
|
||
'Proveito de exercício anterior incluído no resultado actual',
|
||
'timing',
|
||
'18(2)',
|
||
],
|
||
estimated_work_loss: [
|
||
'Perda estimada em obra plurianual em curso',
|
||
'expenseCorrections',
|
||
'36(1)m,Lei20/2009',
|
||
],
|
||
work: [
|
||
'Resultado de obra plurianual — percentagem/encerramento',
|
||
'timing',
|
||
'19(1–6)',
|
||
],
|
||
} as const;
|
||
export type CostRule = keyof typeof costRules;
|
||
export type CostRow = {
|
||
id: string;
|
||
rule: '' | CostRule;
|
||
document: string;
|
||
line: string;
|
||
amount: string;
|
||
sourceCode: string;
|
||
reference: string;
|
||
conditions: '' | 'yes';
|
||
facts: Record<string, string>;
|
||
};
|
||
export type NormalCostsInput = {
|
||
version: 1;
|
||
rows: CostRow[];
|
||
advertisingReference: string;
|
||
salesOrdinary: '' | 'yes';
|
||
creditBase: string;
|
||
creditPrevious: string;
|
||
creditReference: string;
|
||
creditRegime: '' | 'ordinary' | 'regulated';
|
||
};
|
||
export const newNormalCosts = (): NormalCostsInput => ({
|
||
version: 1,
|
||
rows: [],
|
||
advertisingReference: '',
|
||
salesOrdinary: '',
|
||
creditBase: '',
|
||
creditPrevious: '',
|
||
creditReference: '',
|
||
creditRegime: '',
|
||
});
|
||
export const newCostRow = (id: string): CostRow => ({
|
||
id,
|
||
rule: '',
|
||
document: '',
|
||
line: '',
|
||
amount: '',
|
||
sourceCode: '',
|
||
reference: '',
|
||
conditions: '',
|
||
facts: {},
|
||
});
|
||
export function validateNormalCosts(i: NormalCostsInput) {
|
||
if (
|
||
!i ||
|
||
i.version !== 1 ||
|
||
!Array.isArray(i.rows) ||
|
||
[
|
||
'advertisingReference',
|
||
'creditBase',
|
||
'creditPrevious',
|
||
'creditReference',
|
||
].some((k) => typeof i[k as 'creditBase'] !== 'string') ||
|
||
!['', 'yes'].includes(i.salesOrdinary) ||
|
||
!['', 'ordinary', 'regulated'].includes(i.creditRegime)
|
||
)
|
||
throw Error('Mapa de custos/provisões/periodização não recuperável.');
|
||
if (new Set(i.rows.map((r) => r.id)).size !== i.rows.length)
|
||
throw Error('Identificador repetido no mapa de custos.');
|
||
for (const r of i.rows)
|
||
if (
|
||
!r ||
|
||
[
|
||
'id',
|
||
'rule',
|
||
'document',
|
||
'line',
|
||
'amount',
|
||
'sourceCode',
|
||
'reference',
|
||
'conditions',
|
||
].some((k) => typeof r[k as 'id'] !== 'string') ||
|
||
(r.rule !== '' && !Object.hasOwn(costRules, r.rule)) ||
|
||
!['', 'yes'].includes(r.conditions) ||
|
||
!r.facts ||
|
||
typeof r.facts !== 'object' ||
|
||
Array.isArray(r.facts) ||
|
||
Object.values(r.facts).some((v) => typeof v !== 'string')
|
||
)
|
||
throw Error(
|
||
'Linha de custos/provisões/periodização não recuperável.',
|
||
);
|
||
}
|
||
type Source = {
|
||
currency: string;
|
||
accountingLines: Array<{
|
||
code: string;
|
||
total: string;
|
||
accountingEffect: string;
|
||
}>;
|
||
cashSchedule?: Array<{
|
||
salesReceivablesBalance: string | null;
|
||
openingReceivablesBalance: string;
|
||
}>;
|
||
};
|
||
export type CostClaim = {
|
||
document: string;
|
||
line: string;
|
||
sourceCode: string;
|
||
amount: string;
|
||
};
|
||
const z = () => fraction(0n),
|
||
cmp = (a: FiscalFraction, b: FiscalFraction) =>
|
||
BigInt(a.numerator) * BigInt(b.denominator) -
|
||
BigInt(b.numerator) * BigInt(a.denominator);
|
||
const mul = (a: FiscalFraction, n: bigint, d = 1n) =>
|
||
fraction(BigInt(a.numerator) * n, BigInt(a.denominator) * d);
|
||
const min = (...v: FiscalFraction[]) =>
|
||
v.reduce((a, b) => (cmp(a, b) < 0n ? a : b)),
|
||
_max0 = (v: FiscalFraction) => (cmp(v, z()) < 0n ? z() : v);
|
||
const costMoney = (v: FiscalFraction | null) => {
|
||
if (!v) return 'Indeterminado';
|
||
const p = fractionPresentation(v);
|
||
return (
|
||
(p.approximate ? '≈' : '') +
|
||
p.decimal +
|
||
' MZN' +
|
||
(p.approximate
|
||
? ' (exacto ' + v.numerator + '/' + v.denominator + ')'
|
||
: '')
|
||
);
|
||
};
|
||
const identity = (r: { document: string; line: string }) =>
|
||
r.document.trim() + '\u0000' + r.line.trim();
|
||
export function reusableCreditBalance(source: Source | null) {
|
||
const l = source?.cashSchedule?.at(-1);
|
||
if (!l || l.salesReceivablesBalance === null) return null;
|
||
try {
|
||
return addFractions([
|
||
fiscalMoney(l.salesReceivablesBalance),
|
||
fiscalMoney(l.openingReceivablesBalance),
|
||
]);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
export function calculateNormalCosts(
|
||
input: NormalCostsInput,
|
||
source: Source | null,
|
||
existing: CostClaim[],
|
||
period: { from: string; to: string },
|
||
short = false,
|
||
) {
|
||
validateNormalCosts(input);
|
||
const rows = input.rows,
|
||
claims = rows.flatMap((r) => [
|
||
{ ...r },
|
||
...(r.rule === 'work'
|
||
? [
|
||
{
|
||
id: r.id,
|
||
document: r.document,
|
||
line: r.line + '/custos',
|
||
sourceCode: r.facts.costSource ?? '',
|
||
amount: r.facts.bookCost ?? '',
|
||
},
|
||
]
|
||
: []),
|
||
]),
|
||
pending: string[] = [];
|
||
const sourceAmount = (code: string, effect: string) => {
|
||
const l = source?.accountingLines.find((l) => l.code === code);
|
||
if (!l || l.accountingEffect !== effect)
|
||
throw Error('Seleccione uma linha contabilística compatível.');
|
||
return fiscalMoney(l.total);
|
||
};
|
||
const pool = (rule: CostRule) =>
|
||
addFractions(
|
||
rows
|
||
.filter((r) => r.rule === rule)
|
||
.map((r) => fiscalMoney(r.amount)),
|
||
);
|
||
const pools: Record<
|
||
string,
|
||
{
|
||
label: string;
|
||
base: FiscalFraction;
|
||
previous: FiscalFraction | null;
|
||
annualLimit: FiscalFraction;
|
||
accumulatedLimit: FiscalFraction | null;
|
||
reference: string;
|
||
}
|
||
> = {};
|
||
const results = rows.map((r) => {
|
||
const issues: string[] = [],
|
||
rule = r.rule ? costRules[r.rule] : null,
|
||
topic = rule?.[1] ?? '',
|
||
f = r.facts;
|
||
let amount: FiscalFraction | null = null,
|
||
accepted: FiscalFraction | null = null,
|
||
adjustment: FiscalFraction | null = null,
|
||
limit: FiscalFraction | null = null;
|
||
const details: string[] = [];
|
||
const a = (key: string) => {
|
||
if (!f[key]?.trim())
|
||
throw Error('Falta parâmetro: ' + (factLabels[key] ?? key));
|
||
return fiscalMoney(f[key]);
|
||
};
|
||
const yesno = (key: string) => {
|
||
if (!['yes', 'no'].includes(f[key]))
|
||
throw Error(
|
||
'Falta condição documentada: ' + (factLabels[key] ?? key),
|
||
);
|
||
return f[key] === 'yes';
|
||
};
|
||
const ref = (key: string) => {
|
||
if (!f[key]?.trim())
|
||
throw Error('Falta suporte: ' + (factLabels[key] ?? key));
|
||
};
|
||
const balance = () => {
|
||
ref('balanceKey');
|
||
if (
|
||
rows.some((q) => q !== r && q.facts.balanceKey === f.balanceKey)
|
||
)
|
||
throw Error(
|
||
'Saldo/objecto fiscal repetido entre linhas; agregue as parcelas no mesmo mapa para não reutilizar o limite.',
|
||
);
|
||
return a('previous');
|
||
};
|
||
const checkOrigin = (
|
||
code: string,
|
||
value: FiscalFraction,
|
||
effect: string,
|
||
) => {
|
||
if (cmp(value, z()) === 0n && !code) return;
|
||
if (['depreciation', 'interest_expense'].includes(code))
|
||
throw Error(
|
||
'Parcela de amortização/juros exige reconciliação própria com o mapa existente; sem dupla qualificação neste incremento.',
|
||
);
|
||
const total = sourceAmount(code, effect);
|
||
const allocated = addFractions(
|
||
[...existing, ...claims]
|
||
.filter((c) => c.sourceCode === code)
|
||
.map((c) => fiscalMoney(c.amount)),
|
||
);
|
||
if (cmp(allocated, total) > 0n)
|
||
throw Error(
|
||
'Parcelas dos mapas excedem a linha de origem; reconcilie sem duplicar ajustamentos.',
|
||
);
|
||
};
|
||
if (
|
||
!rule ||
|
||
!r.document.trim() ||
|
||
!r.line.trim() ||
|
||
!r.reference.trim() ||
|
||
r.conditions !== 'yes'
|
||
)
|
||
issues.push(
|
||
'Regra, documento/linha, condições e suporte em falta.',
|
||
);
|
||
if (
|
||
[
|
||
...existing,
|
||
...claims.filter((c) => c !== r && c.id !== r.id),
|
||
].some((c) => identity(c) === identity(r))
|
||
)
|
||
issues.push('Documento/linha repetidos entre mapas.');
|
||
if (claims.filter((c) => identity(c) === identity(r)).length > 1)
|
||
issues.push('Documento/linha repetidos no mapa.');
|
||
if (short && ['credit', 'advertising'].includes(r.rule))
|
||
issues.push(
|
||
'Limite anual neste período curto por corresponder;sem proporcionalização ou anualização presumida.',
|
||
);
|
||
if (source?.currency !== 'MZN' || !period.to.startsWith('2026-'))
|
||
issues.push(
|
||
'Recorte anual emMZN com termo em2026 por reconciliar.',
|
||
);
|
||
try {
|
||
amount = fiscalMoney(r.amount);
|
||
if (cmp(amount, z()) <= 0n && r.rule !== 'work')
|
||
throw Error('Identifique parcela positiva conhecida.');
|
||
const isIncome = [
|
||
'provision_reversal',
|
||
'taxed_reversal',
|
||
'prior_income',
|
||
'work',
|
||
].includes(r.rule);
|
||
const omitted =
|
||
r.rule === 'provision_reversal' && f.inResult === 'no';
|
||
if (!omitted)
|
||
checkOrigin(
|
||
r.sourceCode,
|
||
amount,
|
||
isIncome ? 'income' : 'expense',
|
||
);
|
||
else if (r.sourceCode)
|
||
throw Error(
|
||
'Reversão não incluída no resultado não deve repetir uma linha de proveitos.',
|
||
);
|
||
switch (r.rule) {
|
||
case 'allowance': {
|
||
const billed = a('billed'),
|
||
taxed = a('taxedNotBilled');
|
||
if (cmp(addFractions([billed, taxed]), amount) > 0n)
|
||
throw Error(
|
||
'Parcelas facturadas e tributadas emIRPS não podem sobrepor-se ou exceder o encargo.',
|
||
);
|
||
const limited = subtractFractions(
|
||
subtractFractions(amount, billed),
|
||
taxed,
|
||
);
|
||
adjustment = mul(limited, 1n, 2n);
|
||
accepted = subtractFractions(amount, adjustment);
|
||
limit = mul(limited, 1n, 2n);
|
||
details.push(
|
||
'50% só da parcela não facturada a clientes nem tributada emIRPS; parcelas mutuamente exclusivas.',
|
||
);
|
||
break;
|
||
}
|
||
case 'representation':
|
||
accepted = mul(amount, 1n, 5n);
|
||
adjustment = subtractFractions(amount, accepted);
|
||
details.push(
|
||
'20% aceite;80% acrescido. Eventual tributação autónoma não calculada, âmbitoL7.',
|
||
);
|
||
break;
|
||
case 'advertising': {
|
||
if (f.campaign !== 'no')
|
||
throw Error(
|
||
'Campanha de lançamento com horizonte superior a um ano: periodização36-A e articulação do limite não correspondidas; não tratar como publicidade ordinária.',
|
||
);
|
||
if (
|
||
input.salesOrdinary !== 'yes' ||
|
||
!input.advertisingReference.trim()
|
||
)
|
||
throw Error(
|
||
'Confirme completude da publicidade e correspondência do volume de vendas normal do exercício à receita declarada.',
|
||
);
|
||
const total = pool('advertising');
|
||
limit = mul(sourceAmount('revenue', 'income'), 1n, 100n);
|
||
pools.advertising = {
|
||
label: 'Publicidade — limite anual partilhado',
|
||
base: sourceAmount('revenue', 'income'),
|
||
previous: null,
|
||
annualLimit: limit,
|
||
accumulatedLimit: null,
|
||
reference: input.advertisingReference,
|
||
};
|
||
const eligible = min(total, limit);
|
||
accepted = fraction(
|
||
BigInt(amount.numerator) *
|
||
BigInt(eligible.numerator) *
|
||
BigInt(total.denominator),
|
||
BigInt(amount.denominator) *
|
||
BigInt(eligible.denominator) *
|
||
BigInt(total.numerator),
|
||
);
|
||
adjustment = subtractFractions(amount, accepted);
|
||
details.push(
|
||
'Limite único anual1% das vendas normais; repartição aritmética proporcional entre parcelas para a simulação, sem ordem fiscal obrigatória.',
|
||
);
|
||
break;
|
||
}
|
||
case 'vehicle': {
|
||
if (!yesno('passenger')) {
|
||
accepted = amount;
|
||
details.push(
|
||
'Viatura fora da categoria ligeira de passageiros;36(4)não aplicado, restantes condições ordinárias documentadas.',
|
||
);
|
||
} else {
|
||
accepted = yesno('publicOrRental')
|
||
? amount
|
||
: mul(amount, 1n, 2n);
|
||
details.push(
|
||
'50% dos outros encargos de passageiros, excepto serviço público de transportes/locação na actividade normal; não inclui quotas, aluguer sem condutor ou combustível já discriminados.',
|
||
);
|
||
}
|
||
adjustment = subtractFractions(amount, accepted);
|
||
break;
|
||
}
|
||
case 'fuel':
|
||
case 'rental': {
|
||
let eligible: FiscalFraction;
|
||
if (r.rule === 'fuel') {
|
||
eligible = a('supportedNormal');
|
||
if (cmp(eligible, amount) > 0n)
|
||
throw Error(
|
||
'Consumo normal comprovado excede o encargo.',
|
||
);
|
||
details.push(
|
||
'36i: parcela ligada a activo próprio/locado e consumo normal comprovados.',
|
||
);
|
||
} else {
|
||
const excluded = a('nonAcceptedQuota');
|
||
ref('quotaReference');
|
||
if (cmp(excluded, amount) > 0n)
|
||
throw Error(
|
||
'Parcela de quota não aceite excede a renda.',
|
||
);
|
||
eligible = subtractFractions(amount, excluded);
|
||
details.push(
|
||
'36h: reutiliza parcela de quota não aceite documentada; o portal não apura automaticamente essa quota de viatura.',
|
||
);
|
||
}
|
||
if (yesno('passenger') && !yesno('publicOrRental')) {
|
||
if (cmp(eligible, amount) !== 0n)
|
||
throw Error(
|
||
'Articulação da parcela excluída por36h/i com os50% de36(4) ainda não correspondida; este caso combinado fica pendente, sem somar nem repartir correcções por presunção.',
|
||
);
|
||
eligible = mul(eligible, 1n, 2n);
|
||
details.push(
|
||
'36(4):50% do encargo; sem parcela excluída concorrente porh/i.',
|
||
);
|
||
}
|
||
accepted = eligible;
|
||
adjustment = subtractFractions(amount, accepted);
|
||
break;
|
||
}
|
||
case 'credit': {
|
||
if (input.creditRegime !== 'ordinary')
|
||
throw Error(
|
||
'Regime das provisões por identificar ou sujeito a supervisão financeira/seguradora28d; sem limites29 aplicados por aproximação.',
|
||
);
|
||
if (!input.creditReference.trim())
|
||
throw Error(
|
||
'Saldo de créditos ordinários e provisão fiscal anterior por documentar.',
|
||
);
|
||
if (!input.creditBase.trim())
|
||
throw Error(
|
||
'Créditos ordinários existentes no fim do exercício em falta.',
|
||
);
|
||
if (!input.creditPrevious.trim())
|
||
throw Error(
|
||
'Provisão fiscal anterior dos créditos em falta; não presumir zero.',
|
||
);
|
||
const base = fiscalMoney(input.creditBase),
|
||
previous = fiscalMoney(input.creditPrevious),
|
||
total = pool('credit'),
|
||
annual = mul(base, 15n, 1000n),
|
||
accumulated = mul(base, 6n, 100n);
|
||
pools.credit = {
|
||
label: 'Créditos — limites anual e acumulado',
|
||
base,
|
||
previous,
|
||
annualLimit: annual,
|
||
accumulatedLimit: accumulated,
|
||
reference: input.creditReference,
|
||
};
|
||
if (cmp(previous, accumulated) > 0n)
|
||
throw Error(
|
||
'Provisão fiscal anterior excede6% dos créditos finais; reconcilie subsistência/reversão28(2), sem saldo presumido.',
|
||
);
|
||
limit = min(
|
||
annual,
|
||
subtractFractions(accumulated, previous),
|
||
);
|
||
const eligible = min(total, limit);
|
||
accepted = fraction(
|
||
BigInt(amount.numerator) *
|
||
BigInt(eligible.numerator) *
|
||
BigInt(total.denominator),
|
||
BigInt(amount.denominator) *
|
||
BigInt(eligible.denominator) *
|
||
BigInt(total.numerator),
|
||
);
|
||
adjustment = subtractFractions(amount, accepted);
|
||
details.push(
|
||
'29:1,5% anual,6% acumulado sobre créditos da actividade normal existentes no fim; saldo fiscal anterior conhecido. Repartição proporcional do limite único, sem consumir saldo oficial.',
|
||
);
|
||
break;
|
||
}
|
||
case 'stock':
|
||
case 'litigation': {
|
||
let previous: FiscalFraction;
|
||
if (r.rule === 'litigation') {
|
||
if (!yesno('caseOngoing')) {
|
||
accepted = z();
|
||
adjustment = amount;
|
||
details.push(
|
||
'Não há processo judicial em curso; provisão não elegível por28c.',
|
||
);
|
||
break;
|
||
}
|
||
if (!yesno('chargeDeductible')) {
|
||
accepted = z();
|
||
adjustment = amount;
|
||
details.push(
|
||
'Obrigação não daria lugar a custo dedutível; provisão não aceite por28c.',
|
||
);
|
||
break;
|
||
}
|
||
} else if (!yesno('lossObserved')) {
|
||
accepted = z();
|
||
adjustment = amount;
|
||
details.push(
|
||
'Perda de valor não efectivamente observada; estimativa não aceite por28b.',
|
||
);
|
||
break;
|
||
}
|
||
previous = balance();
|
||
limit = a(
|
||
r.rule === 'stock' ? 'observedLoss' : 'obligation',
|
||
);
|
||
if (cmp(previous, limit) > 0n)
|
||
throw Error(
|
||
'Saldo fiscal anterior excede a cobertura actual; reconcilie a reversão28(2).',
|
||
);
|
||
accepted = min(amount, subtractFractions(limit, previous));
|
||
adjustment = subtractFractions(amount, accepted);
|
||
details.push(
|
||
r.rule === 'stock'
|
||
? '28b:limite das perdas efectivamente observadas, incluindo saldo anterior.'
|
||
: '28c:obrigações de processo judicial em curso que seriam custos dedutíveis, com saldo anterior reconciliado.',
|
||
);
|
||
break;
|
||
}
|
||
case 'provision_reversal':
|
||
case 'taxed_reversal': {
|
||
const previous = balance();
|
||
if (cmp(amount, previous) > 0n)
|
||
throw Error(
|
||
'Reversão excede a parcela fiscal anterior demonstrada.',
|
||
);
|
||
if (!yesno('eventEnded'))
|
||
throw Error(
|
||
'Evento não cessou/não houve utilização diferente; fundamento da reversão por corresponder.',
|
||
);
|
||
const inResult = yesno('inResult');
|
||
if (r.rule === 'taxed_reversal' && !inResult)
|
||
throw Error(
|
||
'Só excluir proveito de reversão já incluído na contabilidade e antes não deduzido.',
|
||
);
|
||
accepted = amount;
|
||
adjustment =
|
||
r.rule === 'taxed_reversal'
|
||
? mul(amount, -1n)
|
||
: inResult
|
||
? z()
|
||
: amount;
|
||
details.push(
|
||
r.rule === 'taxed_reversal'
|
||
? 'Parcela antes tributada comprovada; exclui a reversão contabilística sem duplicar dedução.'
|
||
: '28(2):proveito da provisão antes deduzida; só acresce quando ainda não incluído no resultado.',
|
||
);
|
||
break;
|
||
}
|
||
case 'prior_cost':
|
||
case 'prior_income': {
|
||
if (
|
||
!/^20\d{2}$/.test(f.originalYear) ||
|
||
Number(f.originalYear) >= 2026
|
||
)
|
||
throw Error(
|
||
'Exercício de origem deve ser anterior ao exercício actual.',
|
||
);
|
||
ref('closingEvidence');
|
||
const unknown = yesno('unknownAtClose');
|
||
accepted = unknown ? amount : z();
|
||
adjustment = unknown
|
||
? z()
|
||
: r.rule === 'prior_cost'
|
||
? amount
|
||
: mul(amount, -1n);
|
||
details.push(
|
||
unknown
|
||
? '18(2):imprevisível ou manifestamente desconhecido no encerramento anterior; imputável ao actual.'
|
||
: '18(2):conhecido/previsível no encerramento anterior; excluído do actual, sem alterar declarações/históricos anteriores.',
|
||
);
|
||
break;
|
||
}
|
||
case 'estimated_work_loss':
|
||
accepted = z();
|
||
adjustment = amount;
|
||
details.push(
|
||
'36m:perda meramente estimada em obra em curso não dedutível; custo efectivo não confundido com estimativa.',
|
||
);
|
||
break;
|
||
case 'work': {
|
||
const price = a('price'),
|
||
totalCost = a('totalCost'),
|
||
incurred = a('incurred'),
|
||
priorRevenue = a('priorRevenue'),
|
||
priorCost = a('priorCost'),
|
||
bookCost = a('bookCost');
|
||
ref('projectKey');
|
||
if (
|
||
rows.some(
|
||
(q) =>
|
||
q !== r && q.facts.projectKey === f.projectKey,
|
||
)
|
||
)
|
||
throw Error(
|
||
'Obra repetida; agregue os valores acumulados sem duplicar resultados.',
|
||
);
|
||
checkOrigin(f.costSource ?? '', bookCost, 'expense');
|
||
if (
|
||
cmp(totalCost, z()) <= 0n ||
|
||
cmp(incurred, totalCost) > 0n ||
|
||
cmp(priorCost, incurred) > 0n ||
|
||
cmp(priorRevenue, price) > 0n
|
||
)
|
||
throw Error(
|
||
'Preço, custo total, incorrido e valores fiscais anteriores incoerentes.',
|
||
);
|
||
if (!yesno('consistentMethod'))
|
||
throw Error(
|
||
'Mudança de critério exige correspondência à autorização19(6).',
|
||
);
|
||
const mandatory =
|
||
yesno('partialBilling') ||
|
||
yesno('ownFractionalDelivery');
|
||
let revenue: FiscalFraction, cost: FiscalFraction;
|
||
if (f.method === 'percentage') {
|
||
revenue = fraction(
|
||
BigInt(price.numerator) *
|
||
BigInt(incurred.numerator) *
|
||
BigInt(totalCost.denominator),
|
||
BigInt(price.denominator) *
|
||
BigInt(incurred.denominator) *
|
||
BigInt(totalCost.numerator),
|
||
);
|
||
cost = incurred;
|
||
} else if (f.method === 'closing') {
|
||
if (mandatory)
|
||
throw Error(
|
||
'Facturação parcial com acabamento correspondente/entrega fraccionada: critério da percentagem obrigatório19(2).',
|
||
);
|
||
const concluded =
|
||
cmp(mul(incurred, 100n), mul(totalCost, 95n)) >=
|
||
0n || yesno('publicReception');
|
||
revenue = concluded ? price : z();
|
||
cost = concluded ? incurred : z();
|
||
} else
|
||
throw Error(
|
||
'Seleccione percentagem de acabamento ou encerramento.',
|
||
);
|
||
if (
|
||
cmp(priorRevenue, revenue) > 0n ||
|
||
cmp(priorCost, cost) > 0n
|
||
)
|
||
throw Error(
|
||
'Acumulados fiscais anteriores excedem o resultado reconhecível pelo critério; reconcilie sem correcção retrospectiva presumida.',
|
||
);
|
||
const deferred = a('deferredReceipt');
|
||
if (cmp(deferred, z()) !== 0n)
|
||
throw Error(
|
||
'Receita antecipada19(5) de montante não zero exige correspondência própria.',
|
||
);
|
||
const currentRevenue = subtractFractions(
|
||
revenue,
|
||
priorRevenue,
|
||
),
|
||
currentCost = subtractFractions(cost, priorCost);
|
||
accepted = currentCost;
|
||
adjustment = addFractions([
|
||
subtractFractions(currentRevenue, amount),
|
||
subtractFractions(bookCost, currentCost),
|
||
]);
|
||
limit = currentRevenue;
|
||
details.push(
|
||
'19:grau=incorrido/custo total;valores fiscais do exercício obtidos dos acumulados menos o já reconhecido. Proveito fiscal corrente:' +
|
||
costMoney(currentRevenue) +
|
||
';custo fiscal corrente:' +
|
||
costMoney(currentCost) +
|
||
';sem imposto/pagamento ou perda estimada presumidos.',
|
||
);
|
||
break;
|
||
}
|
||
default:
|
||
throw Error('Categoria por identificar.');
|
||
}
|
||
} catch (e) {
|
||
issues.push(e instanceof Error ? e.message : String(e));
|
||
accepted = null;
|
||
adjustment = null;
|
||
}
|
||
if (issues.length) {
|
||
accepted = null;
|
||
adjustment = null;
|
||
pending.push((r.document || r.id) + ': ' + issues.join(' '));
|
||
}
|
||
return {
|
||
...structuredClone(r),
|
||
topic,
|
||
amountValue: amount,
|
||
accepted,
|
||
adjustment,
|
||
limit,
|
||
status: issues.length
|
||
? ('indeterminate' as const)
|
||
: ('calculated' as const),
|
||
reason: issues.join(' ') || details.join(' '),
|
||
ruleSource: rule ? 'CIRPC' + rule[2] : '',
|
||
};
|
||
});
|
||
return {
|
||
input: structuredClone(input),
|
||
rows: results,
|
||
pools,
|
||
pending,
|
||
effects: results.map((r) => ({
|
||
id: r.id,
|
||
topic: r.topic,
|
||
amount: r.amountValue,
|
||
effect: r.adjustment,
|
||
reason: r.reason,
|
||
})),
|
||
net: addFractions(
|
||
results.flatMap((r) => (r.adjustment ? [r.adjustment] : [])),
|
||
),
|
||
source: 'CIRPC18(2)/19/28/29/36;Lei34/2007,Lei20/2009,alterações4/2012 e19/2013 e diplomas posteriores inventariados. Recorte2026/MZN,condições documentadas,sem efeitos retroactivos ou consumo de saldos oficiais;L7/L8 separados.',
|
||
};
|
||
}
|
||
export type NormalCostsModel = ReturnType<typeof calculateNormalCosts>;
|
||
const factLabels: Record<string, string> = {
|
||
billed: 'Facturado a clientes',
|
||
taxedNotBilled: 'Tributado emIRPS e não facturado',
|
||
campaign: 'Campanha plurianual',
|
||
passenger: 'Ligeira de passageiros',
|
||
publicOrRental: 'Serviço público/locação na actividade normal',
|
||
supportedNormal: 'Consumo normal comprovado',
|
||
nonAcceptedQuota: 'Quota não aceite documentada',
|
||
quotaReference: 'Suporte da quota',
|
||
balanceKey: 'Identificação do saldo/objecto',
|
||
previous: 'Parcela fiscal anterior',
|
||
lossObserved: 'Perda efectivamente observada',
|
||
observedLoss: 'Perda total observada',
|
||
caseOngoing: 'Processo judicial em curso',
|
||
chargeDeductible: 'Obrigação fiscalmente dedutível',
|
||
obligation: 'Obrigação coberta',
|
||
eventEnded: 'Evento cessou/utilização diferente',
|
||
inResult: 'Incluído no resultado',
|
||
originalYear: 'Exercício de origem',
|
||
unknownAtClose: 'Imprevisível/desconhecido no encerramento',
|
||
closingEvidence: 'Suporte do encerramento',
|
||
projectKey: 'Obra',
|
||
method: 'Critério',
|
||
price: 'Preço',
|
||
totalCost: 'Custo total estimado',
|
||
incurred: 'Custos incorporados acumulados',
|
||
priorRevenue: 'Proveitos fiscais anteriores',
|
||
priorCost: 'Custos fiscais anteriores',
|
||
costSource: 'Origem dos custos',
|
||
bookCost: 'Custo contabilizado corrente',
|
||
partialBilling: 'Facturação parcial com acabamento correspondente',
|
||
ownFractionalDelivery: 'Venda/entrega fraccionada por conta própria',
|
||
consistentMethod: 'Critério uniforme e mantido',
|
||
publicReception: 'Recepção provisória pública',
|
||
deferredReceipt: 'Receita antecipada proposta',
|
||
};
|
||
const originLabels: Record<string, string> = {
|
||
revenue: 'Receitas do plano',
|
||
cost_of_sales: 'Custo das vendas',
|
||
operating_expenses: 'Despesas operacionais',
|
||
personnel: 'Pessoal',
|
||
depreciation: 'Depreciação contabilística',
|
||
interest_expense: 'Juros financeiros',
|
||
};
|
||
export function normalCostReport(
|
||
m: NormalCostsModel,
|
||
): Array<[string, string, string, string]> {
|
||
const money = costMoney;
|
||
return [
|
||
[
|
||
'IRPC normal — L6',
|
||
'Custos com limites, provisões e periodização',
|
||
`Efeito líquido das parcelas resolvidas:${money(m.net)};${m.pending.join(' | ')}`,
|
||
m.source,
|
||
],
|
||
...Object.values(m.pools).map(
|
||
(p) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
p.label,
|
||
`Base documentada:${money(p.base)};limite anual:${money(p.annualLimit)};saldo fiscal anterior:${p.previous ? money(p.previous) : 'Não aplicável'};limite acumulado:${p.accumulatedLimit ? money(p.accumulatedLimit) : 'Não aplicável'};limite partilhado entre todas as parcelas, sem consumo do saldo oficial.`,
|
||
p.reference,
|
||
] as [string, string, string, string],
|
||
),
|
||
...m.rows.map(
|
||
(r) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
(r.rule
|
||
? costRules[r.rule][0]
|
||
: 'Categoria por identificar') +
|
||
' — ' +
|
||
r.document +
|
||
'/' +
|
||
r.line,
|
||
`Estado:${r.status === 'calculated' ? 'Calculado no recorte' : 'Informação insuficiente'};montante identificado:${money(r.amountValue)};parcela aceite/custo fiscal ou reversão:${money(r.accepted)};limite/base ou proveito fiscal:${r.limit ? money(r.limit) : r.status === 'calculated' ? 'Sem limite monetário próprio; ver regra' : 'Em falta/não aplicável à parcela'};ajustamento com sinal:${money(r.adjustment)};${r.reason}`,
|
||
`${r.ruleSource};${r.reference};origem:${originLabels[r.sourceCode] ?? 'Fora do resultado, documentado'};parâmetros:${Object.entries(
|
||
r.facts,
|
||
)
|
||
.filter(([k]) => factLabels[k])
|
||
.map(
|
||
([k, v]) =>
|
||
factLabels[k] +
|
||
': ' +
|
||
(v === 'yes'
|
||
? 'Sim'
|
||
: v === 'no'
|
||
? 'Não'
|
||
: v === 'percentage'
|
||
? 'Percentagem de acabamento'
|
||
: v === 'closing'
|
||
? 'Encerramento'
|
||
: (originLabels[v] ??
|
||
(v || 'Em falta'))),
|
||
)
|
||
.join(
|
||
'; ',
|
||
)};sem alteração do histórico ou saldo oficial.`,
|
||
] as [string, string, string, string],
|
||
),
|
||
];
|
||
}
|