858 lines
35 KiB
TypeScript
858 lines
35 KiB
TypeScript
import {
|
||
addFractions,
|
||
decimalFraction,
|
||
fraction,
|
||
subtractFractions,
|
||
fractionPresentation,
|
||
type FiscalFraction,
|
||
} from './fiscal-rational.ts';
|
||
import { fiscalMoney, type NormalAsset } from './irpc-normal-schedules.ts';
|
||
export const normalDepreciationVersion = 'IRPC-normal-adjustments/7';
|
||
export const depreciationCatalog = {
|
||
computer: {
|
||
label: 'Computadores, impressoras, UPS e material informático',
|
||
table: 'II',
|
||
code: '3/9',
|
||
rate: '25',
|
||
kind: 'equipment',
|
||
page: 17,
|
||
},
|
||
server: {
|
||
label: 'Servidores',
|
||
table: 'II',
|
||
code: '3/9.1',
|
||
rate: '20',
|
||
kind: 'equipment',
|
||
page: 17,
|
||
},
|
||
security: {
|
||
label: 'Equipamento de vigilância e segurança',
|
||
table: 'II',
|
||
code: '3/23',
|
||
rate: '20',
|
||
kind: 'equipment',
|
||
page: 18,
|
||
},
|
||
machine: {
|
||
label: 'Máquinas não especificadas',
|
||
table: 'II',
|
||
code: '3/22',
|
||
rate: '10',
|
||
kind: 'equipment',
|
||
page: 18,
|
||
},
|
||
furniture: {
|
||
label: 'Mobiliário',
|
||
table: 'II',
|
||
code: '5/7',
|
||
rate: '10',
|
||
kind: 'furniture',
|
||
page: 18,
|
||
},
|
||
commercial_building: {
|
||
label: 'Edifícios comerciais e administrativos',
|
||
table: 'II',
|
||
code: '1/2.2',
|
||
rate: '2',
|
||
kind: 'building',
|
||
page: 16,
|
||
},
|
||
passenger_vehicle: {
|
||
label: 'Veículos automóveis ligeiros e mistos',
|
||
table: 'II',
|
||
code: '4/9.2',
|
||
rate: '25',
|
||
kind: 'vehicle',
|
||
page: 18,
|
||
},
|
||
software: {
|
||
label: 'Programas de computadores',
|
||
table: 'II',
|
||
code: '5/8',
|
||
rate: '25',
|
||
kind: 'software',
|
||
page: 18,
|
||
},
|
||
hotel_decoration: {
|
||
label: 'Decorações interiores — hotéis e actividades similares, classe84.2',
|
||
table: 'I',
|
||
code: '84.2/B',
|
||
rate: '20',
|
||
kind: 'furniture',
|
||
page: 16,
|
||
},
|
||
laundry_machine: {
|
||
label: 'Maquinaria específica de lavandarias/tinturarias',
|
||
table: 'I',
|
||
code: '84.3/A',
|
||
rate: '12.5',
|
||
kind: 'equipment',
|
||
page: 16,
|
||
},
|
||
} as const;
|
||
export type DepreciationCategory = keyof typeof depreciationCatalog;
|
||
export type DepreciationAsset = {
|
||
id: string;
|
||
assetKey: string;
|
||
description: string;
|
||
document: string;
|
||
line: string;
|
||
category: '' | DepreciationCategory | 'other' | 'nondepreciable';
|
||
valuation: '' | 'historical_new' | 'used' | 'revalued' | 'other';
|
||
specificRate: '' | 'yes' | 'no';
|
||
method: '' | 'constant' | 'declining' | 'low_value';
|
||
rate: string;
|
||
factor: string;
|
||
timing: '' | 'annual' | 'months';
|
||
start: string;
|
||
exitState: '' | 'absent' | 'known';
|
||
exitDate: string;
|
||
cost: string;
|
||
land: string;
|
||
residual: string;
|
||
accounting: string;
|
||
previousAccounting: string;
|
||
previousFiscal: string;
|
||
priorAvailable: string;
|
||
previousReference: string;
|
||
newTangible: '' | 'yes' | 'no';
|
||
consistentMethod: '' | 'yes' | 'no';
|
||
switchToStraight: '' | 'yes' | 'no';
|
||
priorFullQuotas: '' | 'yes' | 'no';
|
||
vehicleException: '' | 'yes' | 'no';
|
||
limitedRights: '' | 'yes' | 'no';
|
||
individual: '' | 'yes' | 'no';
|
||
conditions: '' | 'yes';
|
||
reference: string;
|
||
};
|
||
export type NormalDepreciationInput = {
|
||
version: 1;
|
||
completenessReference: string;
|
||
assets: DepreciationAsset[];
|
||
};
|
||
export const newNormalDepreciation = (): NormalDepreciationInput => ({
|
||
version: 1,
|
||
completenessReference: '',
|
||
assets: [],
|
||
});
|
||
export const newDepreciationAsset = (id: string): DepreciationAsset => ({
|
||
id,
|
||
assetKey: '',
|
||
description: '',
|
||
document: '',
|
||
line: '',
|
||
category: '',
|
||
valuation: '',
|
||
specificRate: '',
|
||
method: '',
|
||
rate: '',
|
||
factor: '',
|
||
timing: '',
|
||
start: '',
|
||
exitState: '',
|
||
exitDate: '',
|
||
cost: '',
|
||
land: '',
|
||
residual: '',
|
||
accounting: '',
|
||
previousAccounting: '',
|
||
previousFiscal: '',
|
||
priorAvailable: '',
|
||
previousReference: '',
|
||
newTangible: '',
|
||
consistentMethod: '',
|
||
switchToStraight: '',
|
||
priorFullQuotas: '',
|
||
vehicleException: '',
|
||
limitedRights: '',
|
||
individual: '',
|
||
conditions: '',
|
||
reference: '',
|
||
});
|
||
export function reuseNormalAssets(
|
||
assets: NormalAsset[],
|
||
): NormalDepreciationInput {
|
||
return {
|
||
version: 1,
|
||
completenessReference: '',
|
||
assets: assets.map((a) => ({
|
||
...newDepreciationAsset(a.id),
|
||
assetKey: a.id,
|
||
description: a.description,
|
||
category: a.category,
|
||
document: a.reference,
|
||
line: a.id,
|
||
method: 'constant',
|
||
timing: 'annual',
|
||
specificRate: 'no',
|
||
consistentMethod: 'yes',
|
||
conditions: a.conditions,
|
||
rate: a.rate,
|
||
cost: a.cost,
|
||
residual: a.residual,
|
||
accounting: a.accounting,
|
||
previousFiscal: a.previousFiscal,
|
||
start: a.inUseFrom,
|
||
reference: a.reference,
|
||
})),
|
||
};
|
||
}
|
||
export function validateNormalDepreciation(i: NormalDepreciationInput) {
|
||
if (
|
||
!i ||
|
||
i.version !== 1 ||
|
||
typeof i.completenessReference !== 'string' ||
|
||
!Array.isArray(i.assets) ||
|
||
new Set(i.assets.map((a) => a.id)).size !== i.assets.length
|
||
)
|
||
throw Error('Mapa de amortizações não recuperável.');
|
||
for (const a of i.assets) {
|
||
if (
|
||
!a ||
|
||
Object.keys(newDepreciationAsset('')).some(
|
||
(k) => typeof a[k as keyof DepreciationAsset] !== 'string',
|
||
) ||
|
||
![
|
||
'',
|
||
'other',
|
||
'nondepreciable',
|
||
...Object.keys(depreciationCatalog),
|
||
].includes(a.category) ||
|
||
!['', 'historical_new', 'used', 'revalued', 'other'].includes(
|
||
a.valuation,
|
||
) ||
|
||
!['', 'constant', 'declining', 'low_value'].includes(a.method) ||
|
||
!['', 'annual', 'months'].includes(a.timing) ||
|
||
!['', 'absent', 'known'].includes(a.exitState) ||
|
||
!['', 'yes'].includes(a.conditions) ||
|
||
[
|
||
'specificRate',
|
||
'newTangible',
|
||
'consistentMethod',
|
||
'switchToStraight',
|
||
'priorFullQuotas',
|
||
'vehicleException',
|
||
'limitedRights',
|
||
'individual',
|
||
].some((k) => !['', 'yes', 'no'].includes(a[k as 'specificRate']))
|
||
)
|
||
throw Error(
|
||
'Activo ou parâmetros de amortização não recuperáveis.',
|
||
);
|
||
}
|
||
}
|
||
const zero = () => fraction(0n),
|
||
cmp = (a: FiscalFraction, b: FiscalFraction) =>
|
||
BigInt(a.numerator) * BigInt(b.denominator) -
|
||
BigInt(b.numerator) * BigInt(a.denominator);
|
||
const min = (...a: FiscalFraction[]) =>
|
||
a.reduce((v, w) => (cmp(v, w) < 0n ? v : w)),
|
||
positive = (a: FiscalFraction) => (cmp(a, zero()) < 0n ? zero() : a);
|
||
const multiply = (a: FiscalFraction, b: FiscalFraction) =>
|
||
fraction(
|
||
BigInt(a.numerator) * BigInt(b.numerator),
|
||
BigInt(a.denominator) * BigInt(b.denominator),
|
||
);
|
||
const ratio = (a: FiscalFraction, b: FiscalFraction) =>
|
||
fraction(
|
||
BigInt(a.numerator) * BigInt(b.denominator),
|
||
BigInt(a.denominator) * BigInt(b.numerator),
|
||
);
|
||
const date = (v: string) =>
|
||
/^20\d{2}-\d{2}-\d{2}$/.test(v) &&
|
||
Number.isFinite(Date.parse(v)) &&
|
||
new Date(v).toISOString().slice(0, 10) === v;
|
||
const month = (v: string) =>
|
||
Number(v.slice(0, 4)) * 12 + Number(v.slice(5, 7)) - 1;
|
||
const show = (v: FiscalFraction | null) => {
|
||
if (!v) return 'Indeterminado';
|
||
const p = fractionPresentation(v);
|
||
return (
|
||
(p.approximate ? '≈' : '') +
|
||
p.decimal +
|
||
' MZN' +
|
||
(p.approximate
|
||
? ' (exacto ' + v.numerator + '/' + v.denominator + ')'
|
||
: '')
|
||
);
|
||
};
|
||
export function calculateNormalDepreciation(
|
||
input: NormalDepreciationInput,
|
||
source: {
|
||
currency: string;
|
||
accountingLines: Array<{ code: string; total: string }>;
|
||
} | null,
|
||
period: { from: string; to: string },
|
||
claims: Array<{
|
||
sourceCode: string;
|
||
document: string;
|
||
line: string;
|
||
amount: string;
|
||
}>,
|
||
short = false,
|
||
acceleratedIds: ReadonlySet<string> = new Set(),
|
||
) {
|
||
validateNormalDepreciation(input);
|
||
const pending: string[] = [];
|
||
if (!input.completenessReference.trim())
|
||
pending.push(
|
||
'Reconciliação integral dos activos e quotas anteriores com a depreciação da origem em falta.',
|
||
);
|
||
if (
|
||
source?.currency !== 'MZN' ||
|
||
!date(period.from) ||
|
||
!date(period.to) ||
|
||
!period.to.startsWith('2026-') ||
|
||
(!short && month(period.to) - month(period.from) !== 11) ||
|
||
(short &&
|
||
(month(period.to) < month(period.from) ||
|
||
month(period.to) - month(period.from) >= 11))
|
||
)
|
||
pending.push(
|
||
'Período anual emMZN com termo em2026 por reconciliar; períodos curtos permanecem separados.',
|
||
);
|
||
const assets = input.assets.map((a) => {
|
||
const issues: string[] = [],
|
||
notes: string[] = [];
|
||
let accounting: FiscalFraction | null = null,
|
||
basis: FiscalFraction | null = null,
|
||
taxBasis: FiscalFraction | null = null,
|
||
quota: FiscalFraction | null = null,
|
||
deduction: FiscalFraction | null = null,
|
||
adjustment: FiscalFraction | null = null,
|
||
priorUsed: FiscalFraction | null = null,
|
||
priorRemaining: FiscalFraction | null = null,
|
||
remainingFiscal: FiscalFraction | null = null,
|
||
eligibleCurrent: FiscalFraction | null = null,
|
||
months: number | null = null;
|
||
const entry = Object.hasOwn(depreciationCatalog, a.category)
|
||
? depreciationCatalog[a.category as DepreciationCategory]
|
||
: null;
|
||
if (
|
||
short &&
|
||
a.category !== 'nondepreciable' &&
|
||
(a.timing !== 'months' || a.method === 'low_value')
|
||
)
|
||
issues.push(
|
||
'Período curto exige duodécimos;opção integral art17 neste período por corresponder.',
|
||
);
|
||
const money = (s: string, label: string) => {
|
||
if (!s.trim()) throw Error(label + ' em falta; não presumir zero.');
|
||
return fiscalMoney(s);
|
||
};
|
||
const yesno = (v: string, label: string) => {
|
||
if (!['yes', 'no'].includes(v))
|
||
throw Error(label + ' por documentar.');
|
||
return v === 'yes';
|
||
};
|
||
if (
|
||
!a.assetKey.trim() ||
|
||
!a.description.trim() ||
|
||
!a.document.trim() ||
|
||
!a.line.trim() ||
|
||
!a.reference.trim() ||
|
||
a.conditions !== 'yes'
|
||
)
|
||
issues.push(
|
||
'Identificação do activo, documento/linha, condições ordinárias e suporte em falta.',
|
||
);
|
||
if (
|
||
input.assets.some(
|
||
(b) =>
|
||
b !== a &&
|
||
(b.assetKey.trim() === a.assetKey.trim() ||
|
||
(b.document.trim() === a.document.trim() &&
|
||
b.line.trim() === a.line.trim())),
|
||
) ||
|
||
claims.some(
|
||
(c) =>
|
||
c.document.trim() === a.document.trim() &&
|
||
c.line.trim() === a.line.trim(),
|
||
)
|
||
)
|
||
issues.push(
|
||
'Activo ou documento/linha repetido entre mapas; reconcilie sem dedução duplicada.',
|
||
);
|
||
try {
|
||
accounting = money(
|
||
a.accounting,
|
||
'Depreciação contabilística corrente',
|
||
);
|
||
const cost = money(a.cost, 'Custo histórico');
|
||
if (a.category === 'nondepreciable') {
|
||
basis = cost;
|
||
taxBasis = zero();
|
||
quota = zero();
|
||
deduction = zero();
|
||
adjustment = accounting;
|
||
notes.push(
|
||
'CIRPC26/27:elemento expressamente qualificado como não sujeito a deperecimento; depreciação contabilística integralmente acrescida.',
|
||
);
|
||
} else {
|
||
if (a.valuation !== 'historical_new')
|
||
throw Error(
|
||
'Tipo de aquisição/base por identificar ou fora do recorte de bens novos a custo histórico; usados/abertura de escrita7(2) e reavaliados15 exigem regra própria.',
|
||
);
|
||
if (!entry)
|
||
throw Error(
|
||
'Categoria/tabela ainda não correspondida; sem taxa livre ou código provisório.',
|
||
);
|
||
if (entry) {
|
||
const specific = yesno(
|
||
a.specificRate,
|
||
'Existência de taxa específica da tabelaI',
|
||
);
|
||
if (entry.table === 'II' && specific)
|
||
throw Error(
|
||
'Taxa específica da tabelaI prevalece; seleccione a categoria específica correspondente.',
|
||
);
|
||
if (entry.table === 'I' && !specific)
|
||
throw Error(
|
||
'Categoria específica da tabelaI sem enquadramento documentado.',
|
||
);
|
||
}
|
||
if (!date(a.start) || a.start < '2014-01-01')
|
||
throw Error(
|
||
'Entrada em funcionamento/utilização desde2014 por identificar; activos anteriores exigem correspondência histórica própria.',
|
||
);
|
||
if (
|
||
!yesno(a.consistentMethod, 'Método uniforme desde o início')
|
||
)
|
||
throw Error(
|
||
'Mudança de método/razão excepcional requer correspondência própria do artigo10.',
|
||
);
|
||
if (
|
||
(a.exitState === 'known' &&
|
||
(!date(a.exitDate) || a.exitDate < a.start)) ||
|
||
!a.exitState ||
|
||
(a.exitState === 'absent' && a.exitDate.trim())
|
||
)
|
||
throw Error(
|
||
'Estado/data de transmissão,inutilização ou termo de vida útil incoerente ou em falta.',
|
||
);
|
||
if (!a.previousReference.trim())
|
||
throw Error(
|
||
'Suporte das quotas contabilísticas/fiscais anteriores e da parcela temporariamente tributada em falta.',
|
||
);
|
||
const priorBook = money(
|
||
a.previousAccounting,
|
||
'Quotas contabilísticas anteriores',
|
||
),
|
||
priorFiscal = money(
|
||
a.previousFiscal,
|
||
'Quotas fiscais anteriores',
|
||
),
|
||
available = money(
|
||
a.priorAvailable,
|
||
'Parcela anterior temporariamente tributada recuperável',
|
||
);
|
||
if (
|
||
cmp(priorFiscal, priorBook) > 0n ||
|
||
cmp(available, subtractFractions(priorBook, priorFiscal)) >
|
||
0n
|
||
)
|
||
throw Error(
|
||
'Quotas anteriores incoerentes; parcela recuperável não pode exceder a diferença contabilizada e não deduzida, excluindo encargos permanentemente não aceites.',
|
||
);
|
||
const residual = money(a.residual, 'Residual'),
|
||
land =
|
||
entry?.kind === 'building'
|
||
? money(a.land, 'Terreno/parte não depreciável')
|
||
: zero();
|
||
if (
|
||
entry?.kind !== 'building' &&
|
||
a.land.trim() &&
|
||
cmp(fiscalMoney(a.land), zero()) !== 0n
|
||
)
|
||
throw Error(
|
||
'Terreno não corresponde a esta categoria de activo.',
|
||
);
|
||
basis = subtractFractions(
|
||
subtractFractions(cost, residual),
|
||
land,
|
||
);
|
||
if (cmp(basis, zero()) <= 0n)
|
||
throw Error('Base depreciável positiva por reconciliar.');
|
||
if (entry?.kind !== 'building' && cmp(land, zero()) !== 0n)
|
||
throw Error(
|
||
'Terreno só deve ser separado no imóvel correspondente.',
|
||
);
|
||
taxBasis = basis;
|
||
if (
|
||
entry?.kind === 'vehicle' &&
|
||
!yesno(
|
||
a.vehicleException,
|
||
'Afectação a serviço público de transportes/locação na actividade normal',
|
||
)
|
||
) {
|
||
taxBasis = min(
|
||
basis,
|
||
positive(
|
||
subtractFractions(fraction(800000n), residual),
|
||
),
|
||
);
|
||
notes.push(
|
||
'CIRPC27e:limite de aquisição800000MZN,sem afectar a depreciação contabilística de origem.',
|
||
);
|
||
}
|
||
if (
|
||
entry?.kind === 'software' &&
|
||
!yesno(
|
||
a.limitedRights,
|
||
'Aquisição onerosa e direitos de utilização exclusiva por tempo limitado',
|
||
)
|
||
)
|
||
throw Error(
|
||
'Direitos de software/intangível sem condições do artigo16 documentadas.',
|
||
);
|
||
if (cmp(priorFiscal, taxBasis) > 0n)
|
||
throw Error(
|
||
'Quotas fiscais anteriores excedem a base fiscal; reconcilie o saldo anterior.',
|
||
);
|
||
const remaining = subtractFractions(taxBasis, priorFiscal),
|
||
priorEligibleBook = min(
|
||
taxBasis,
|
||
multiply(priorBook, ratio(taxBasis, basis)),
|
||
);
|
||
if (
|
||
cmp(
|
||
available,
|
||
positive(
|
||
subtractFractions(priorEligibleBook, priorFiscal),
|
||
),
|
||
) > 0n
|
||
)
|
||
throw Error(
|
||
'Parcela anterior recuperável inclui quota permanentemente não aceite ou excede a contabilização fiscalmente elegível.',
|
||
);
|
||
eligibleCurrent = min(
|
||
multiply(accounting, ratio(taxBasis, basis)),
|
||
positive(subtractFractions(taxBasis, priorEligibleBook)),
|
||
);
|
||
let maxRate: FiscalFraction | null = entry
|
||
? decimalFraction(entry.rate)
|
||
: null,
|
||
maxLifeMonths: FiscalFraction | null = maxRate
|
||
? ratio(fraction(2400n), maxRate)
|
||
: null;
|
||
if (a.method === 'low_value') {
|
||
if (
|
||
cmp(cost, fraction(20000n)) > 0n ||
|
||
!yesno(
|
||
a.individual,
|
||
'Activo individual fora de conjunto depreciado como um todo',
|
||
) ||
|
||
cmp(priorFiscal, zero()) !== 0n ||
|
||
a.start < period.from ||
|
||
a.start > period.to
|
||
)
|
||
throw Error(
|
||
'Art17:valor unitário até20000MZN,activo individual e início no período,sem amortização fiscal anterior; condições em falta/incompatíveis.',
|
||
);
|
||
quota = remaining;
|
||
months = month(period.to) - month(a.start) + 1;
|
||
notes.push(
|
||
'Art17:opção de depreciação integral em um período,limitada ao que está contabilizado e ao saldo fiscal.',
|
||
);
|
||
} else {
|
||
if (!entry || !maxRate || !maxLifeMonths)
|
||
throw Error('Taxa aplicável não identificada.');
|
||
if (a.method === 'constant') {
|
||
if (!/^\d+(?:\.\d{1,6})?$/.test(a.rate))
|
||
throw Error(
|
||
'Taxa constante escolhida por identificar.',
|
||
);
|
||
const selected = decimalFraction(a.rate);
|
||
if (
|
||
cmp(selected, multiply(maxRate, fraction(1n, 2n))) <
|
||
0n ||
|
||
cmp(selected, maxRate) > 0n
|
||
)
|
||
throw Error(
|
||
'Taxa constante fora do recorte entre metade da máxima e a máxima;quotas inferiores exigem correspondência própria dos limites de vida útil, artigos5/7.',
|
||
);
|
||
quota = multiply(
|
||
taxBasis,
|
||
multiply(
|
||
selected,
|
||
fraction(
|
||
acceleratedIds.has(a.id) ? 3n : 1n,
|
||
acceleratedIds.has(a.id) ? 200n : 100n,
|
||
),
|
||
),
|
||
);
|
||
if (acceleratedIds.has(a.id))
|
||
notes.push(
|
||
'CBF16:L7 qualificado,incremento50% da taxa normal;mantidos contabilização e saldo fiscal.',
|
||
);
|
||
notes.push(
|
||
'Quota constante pela taxa escolhida:' +
|
||
a.rate +
|
||
'%;tabela' +
|
||
entry.table +
|
||
'/' +
|
||
entry.code +
|
||
'.',
|
||
);
|
||
} else if (a.method === 'declining') {
|
||
if (
|
||
!yesno(
|
||
a.newTangible,
|
||
'Activo tangível novo adquirido a terceiros/construído',
|
||
) ||
|
||
['building', 'furniture', 'software'].includes(
|
||
entry.kind,
|
||
) ||
|
||
(entry.kind === 'vehicle' &&
|
||
a.vehicleException !== 'yes')
|
||
)
|
||
throw Error(
|
||
'Art6:quotas degressivas só nos activos tangíveis novos elegíveis;edifícios,mobiliário/equipamento social,intangíveis e veículos sem excepção ficam separados.',
|
||
);
|
||
const life = ratio(fraction(100n), maxRate),
|
||
maxFactor =
|
||
cmp(life, fraction(5n)) < 0n
|
||
? fraction(3n, 2n)
|
||
: cmp(life, fraction(6n)) <= 0n
|
||
? fraction(2n)
|
||
: fraction(5n, 2n);
|
||
if (!/^\d+(?:\.\d{1,6})?$/.test(a.factor))
|
||
throw Error(
|
||
'Coeficiente degressivo escolhido em falta.',
|
||
);
|
||
const factor = decimalFraction(a.factor);
|
||
if (
|
||
cmp(factor, zero()) <= 0n ||
|
||
cmp(factor, maxFactor) > 0n
|
||
)
|
||
throw Error(
|
||
'Coeficiente degressivo excede o máximo do artigo8:1,5/2/2,5 conforme vida útil mínima.',
|
||
);
|
||
quota = multiply(
|
||
remaining,
|
||
multiply(
|
||
maxRate,
|
||
multiply(factor, fraction(1n, 100n)),
|
||
),
|
||
);
|
||
notes.push(
|
||
'Art8:saldo fiscal por amortizar ×taxa máxima' +
|
||
entry.rate +
|
||
'% ×coeficiente' +
|
||
a.factor +
|
||
';máximo' +
|
||
fractionPresentation(maxFactor).decimal +
|
||
'.',
|
||
);
|
||
if (
|
||
yesno(
|
||
a.switchToStraight,
|
||
'Opção pela regra de saldo/vida útil restante8(2)',
|
||
)
|
||
) {
|
||
if (
|
||
!yesno(
|
||
a.priorFullQuotas,
|
||
'Condição das quotas anteriores8(2)',
|
||
) ||
|
||
!a.start.endsWith('-01-01') ||
|
||
period.from !== '2026-01-01' ||
|
||
a.timing !== 'annual'
|
||
)
|
||
throw Error(
|
||
'Art8(2):condição das quotas anteriores por documentar;este recorte da opção exige início emJaneiro e período anual civil.',
|
||
);
|
||
const remainingYears = subtractFractions(
|
||
life,
|
||
fraction(
|
||
BigInt(2026 - Number(a.start.slice(0, 4))),
|
||
),
|
||
);
|
||
if (cmp(remainingYears, zero()) <= 0n)
|
||
throw Error(
|
||
'Art8(2):vida útil mínima esgotada;sem dividir por anos inexistentes.',
|
||
);
|
||
const linear = ratio(remaining, remainingYears);
|
||
if (cmp(quota, linear) < 0n) {
|
||
quota = linear;
|
||
notes.push(
|
||
'Art8(2):usado saldo/número de anos de vida útil mínima restantes,com condições anteriores documentadas.',
|
||
);
|
||
}
|
||
}
|
||
} else
|
||
throw Error('Método de amortização por identificar.');
|
||
if (!a.timing)
|
||
throw Error('Opção anual/duodécimos por identificar.');
|
||
if (a.timing === 'annual') {
|
||
if (a.exitState === 'known' && a.exitDate <= period.to)
|
||
throw Error(
|
||
'Evento de saída em período anual:seleccione duodécimos e reconcilie a regra aplicável;sem quota anual presumida.',
|
||
);
|
||
const lifeEnd =
|
||
month(a.start) +
|
||
Number(
|
||
BigInt(maxLifeMonths.numerator) /
|
||
BigInt(maxLifeMonths.denominator),
|
||
);
|
||
if (
|
||
lifeEnd > month(period.from) &&
|
||
lifeEnd <= month(period.to) &&
|
||
cmp(remaining, zero()) > 0n
|
||
)
|
||
throw Error(
|
||
'Vida útil máxima termina dentro do período;opção anual e quotas anteriores exigem correspondência própria. Duodécimos em vigor são calculados até ao limite temporal.',
|
||
);
|
||
months = a.start > period.to ? 0 : 12;
|
||
} else {
|
||
const start = Math.max(
|
||
month(period.from),
|
||
month(a.start),
|
||
),
|
||
end = Math.min(
|
||
month(period.to) + 1,
|
||
a.exitState === 'known'
|
||
? month(a.exitDate)
|
||
: month(period.to) + 1,
|
||
month(a.start) +
|
||
Number(
|
||
BigInt(maxLifeMonths.numerator) /
|
||
BigInt(maxLifeMonths.denominator),
|
||
),
|
||
);
|
||
months = Math.max(0, end - start);
|
||
quota = multiply(quota, fraction(BigInt(months), 12n));
|
||
notes.push(
|
||
'Art9:entrada incluída desde o mês da utilização;saída exclui o mês do evento;' +
|
||
months +
|
||
'duodécimos.',
|
||
);
|
||
}
|
||
const elapsed = Math.max(
|
||
0,
|
||
month(period.from) - month(a.start),
|
||
);
|
||
if (cmp(fraction(BigInt(elapsed)), maxLifeMonths) >= 0n) {
|
||
quota = zero();
|
||
notes.push(
|
||
'Art5:vida útil máxima esgotada,sem excepção aplicada;nenhuma recuperação de quota fora desse limite.',
|
||
);
|
||
} else if (a.start > period.to) {
|
||
quota = zero();
|
||
notes.push(
|
||
'Art3:activo ainda não entrou emuso no período;sem amortização fiscal.',
|
||
);
|
||
}
|
||
}
|
||
deduction = min(
|
||
quota!,
|
||
remaining,
|
||
addFractions([eligibleCurrent, available]),
|
||
);
|
||
priorUsed = positive(
|
||
subtractFractions(deduction, eligibleCurrent),
|
||
);
|
||
priorRemaining = subtractFractions(available, priorUsed);
|
||
remainingFiscal = subtractFractions(remaining, deduction);
|
||
adjustment = subtractFractions(accounting, deduction);
|
||
notes.push(
|
||
'Art3(3):dedução limitada ao contabilizado no período ou antes desde o uso;quotas anteriores recuperáveis usadas:' +
|
||
show(priorUsed) +
|
||
';saldo anterior aritmético:' +
|
||
show(priorRemaining) +
|
||
'. Sem consumo oficial nem recuperação de quota não contabilizada/permanentemente excluída.',
|
||
);
|
||
}
|
||
} catch (e) {
|
||
issues.push(e instanceof Error ? e.message : String(e));
|
||
}
|
||
if (issues.length) {
|
||
deduction = null;
|
||
adjustment = null;
|
||
priorUsed = null;
|
||
priorRemaining = null;
|
||
remainingFiscal = null;
|
||
pending.push((a.description || a.id) + ': ' + issues.join(' '));
|
||
}
|
||
return {
|
||
...structuredClone(a),
|
||
catalog: entry,
|
||
accountingValue: accounting,
|
||
basis,
|
||
taxBasis,
|
||
quota,
|
||
deduction,
|
||
adjustment,
|
||
eligibleCurrent,
|
||
priorUsed,
|
||
priorRemaining,
|
||
remainingFiscal,
|
||
months,
|
||
status: issues.length
|
||
? ('indeterminate' as const)
|
||
: ('calculated' as const),
|
||
reason: issues.join(' ') || notes.join(' '),
|
||
};
|
||
});
|
||
let accountingTotal: FiscalFraction | null = null;
|
||
try {
|
||
accountingTotal = addFractions(
|
||
assets.map((a) => {
|
||
if (!a.accountingValue) throw Error();
|
||
return a.accountingValue;
|
||
}),
|
||
);
|
||
const origin = fiscalMoney(
|
||
source?.accountingLines.find((l) => l.code === 'depreciation')
|
||
?.total ?? '',
|
||
);
|
||
if (cmp(accountingTotal, origin) !== 0n)
|
||
pending.push(
|
||
'Soma das quotas contabilísticas dos activos não corresponde à depreciação financeira declarada.',
|
||
);
|
||
} catch {
|
||
pending.push('Depreciação contabilística da origem por reconciliar.');
|
||
}
|
||
if (claims.some((c) => c.sourceCode === 'depreciation'))
|
||
pending.push(
|
||
'Depreciação partilhada com outros ajustamentos de origem;reconcilie as parcelas para não duplicar o mesmo encargo.',
|
||
);
|
||
const knownAdjustment = addFractions(
|
||
assets.flatMap((a) => (a.adjustment ? [a.adjustment] : [])),
|
||
),
|
||
adjustment = pending.length === 0 ? knownAdjustment : null;
|
||
return {
|
||
input: structuredClone(input),
|
||
assets,
|
||
accountingTotal,
|
||
knownAdjustment,
|
||
adjustment,
|
||
pending,
|
||
source: 'Decreto72/2013,art3–10/16/17,tabelasI/II;CIRPC26/27. Recorte anualMZN com termo2026,custo histórico,sem reavaliação,turnos intensivos,concessões/fusões ou excepções presumidas. Fotografia7;mapa interno,não formulário oficial de amortizações.',
|
||
};
|
||
}
|
||
export type NormalDepreciationModel = ReturnType<
|
||
typeof calculateNormalDepreciation
|
||
>;
|
||
export function normalDepreciationReport(
|
||
m: NormalDepreciationModel,
|
||
): Array<[string, string, string, string]> {
|
||
return [
|
||
[
|
||
'IRPC normal — L6',
|
||
'Amortizações — tabelas, métodos, duodécimos e quotas anteriores',
|
||
`Quotas contabilísticas:${show(m.accountingTotal)};ajustamento das parcelas conhecidas:${show(m.knownAdjustment)};ajustamento reconciliado:${show(m.adjustment)};${m.pending.join(' | ')}`,
|
||
m.source,
|
||
],
|
||
...m.assets.map(
|
||
(a) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
a.description + ' — ' + a.document + '/' + a.line,
|
||
`Estado:${a.status === 'calculated' ? 'Calculado no recorte' : 'Informação insuficiente'};contabilístico:${show(a.accountingValue)};parcela contabilística elegível:${show(a.eligibleCurrent)};base fiscal:${show(a.taxBasis)};quota calculada:${show(a.quota)};dedução fiscal:${show(a.deduction)};ajustamento com sinal:${show(a.adjustment)};duodécimos:${a.months ?? 'Não aplicável'};quota anterior utilizada:${show(a.priorUsed)};saldo anterior aritmético:${show(a.priorRemaining)};${a.reason}`,
|
||
`${a.catalog ? 'Decreto72/2013,tabela' + a.catalog.table + ',código' + a.catalog.code + ',PDF' + a.catalog.page : 'Categoria sem código de tabela aplicado'};${a.reference};quotas anteriores:${a.previousReference};método:${a.method === 'constant' ? 'Constante' : a.method === 'declining' ? 'Degressivo' : a.method === 'low_value' ? 'Valor reduzido' : 'Por identificar'};sem alteração dos documentos/saldos oficiais ou históricos.`,
|
||
] as [string, string, string, string],
|
||
),
|
||
];
|
||
}
|