982 lines
39 KiB
TypeScript
982 lines
39 KiB
TypeScript
import { inputText } from './input-text.ts';
|
||
import {
|
||
calculateEconomicInclusions,
|
||
validateEconomicInclusions,
|
||
type EconomicInclusions,
|
||
} from './irpc-normal-economic.ts';
|
||
import { irpcSpecialCombinationsVersion } from './irpc-special-combinations.ts';
|
||
import {
|
||
irpcSpecialVersion,
|
||
irpcSpecialMonthlyVersion,
|
||
irpcSpecialStartPeriodVersion,
|
||
irpcSpecialCessationPriorVersion,
|
||
specialCalculationVersion,
|
||
validateSpecial,
|
||
calculateSpecial,
|
||
specialReportRows,
|
||
specialDependency,
|
||
type SpecialInput,
|
||
type SpecialModel,
|
||
} from './irpc-special.ts';
|
||
import {
|
||
normalFormPreparationVersion,
|
||
validateNormalFormPreparation,
|
||
prepareNormalForms,
|
||
normalFormPreparationReport,
|
||
type NormalFormPreparationInput,
|
||
type NormalFormPreparationModel,
|
||
} from './irpc-normal-form-preparation.ts';
|
||
import {
|
||
normalPeriodFxVersion,
|
||
validateNormalPeriodFx,
|
||
mapNormalPeriodFx,
|
||
normalPeriodFxDependency,
|
||
normalPeriodFxReport,
|
||
type NormalPeriodFxInput,
|
||
type NormalPeriodFxModel,
|
||
} from './irpc-normal-period-fx.ts';
|
||
import {
|
||
normalDepreciationVersion,
|
||
validateNormalDepreciation,
|
||
calculateNormalDepreciation,
|
||
normalDepreciationReport,
|
||
type NormalDepreciationInput,
|
||
type NormalDepreciationModel,
|
||
} from './irpc-normal-depreciation.ts';
|
||
import {
|
||
normalCostsVersion,
|
||
validateNormalCosts,
|
||
calculateNormalCosts,
|
||
normalCostReport,
|
||
type NormalCostsInput,
|
||
type NormalCostsModel,
|
||
} from './irpc-normal-costs.ts';
|
||
import { normalFormFields } from './irpc-normal-forms.ts';
|
||
import {
|
||
normalSchedulesVersion,
|
||
normalSchedulePeriod,
|
||
validateSchedules,
|
||
calculateSchedules,
|
||
type NormalSchedules,
|
||
type SchedulesModel,
|
||
} from './irpc-normal-schedules.ts';
|
||
import {
|
||
normalAssessmentVersion,
|
||
validateNormalAssessment,
|
||
calculateNormalAssessment,
|
||
type NormalAssessmentInput,
|
||
type NormalAssessmentModel,
|
||
} from './irpc-normal-assessment.ts';
|
||
import {
|
||
normalBaseVersion,
|
||
validateNormalBase,
|
||
calculateNormalBase,
|
||
type NormalBaseInput,
|
||
type NormalBaseModel,
|
||
} from './irpc-normal-base.ts';
|
||
import {
|
||
normalProfitVersion,
|
||
validateNormalProfit,
|
||
closeNormalProfit,
|
||
type NormalProfitInput,
|
||
type NormalProfitModel,
|
||
} from './irpc-normal-profit.ts';
|
||
import {
|
||
forecastBusinessPlan,
|
||
restoreBusinessPlanSnapshot,
|
||
} from './business-financial-plan.ts';
|
||
import { buildFinancialFiscalBridge } from './financial-fiscal-bridge.ts';
|
||
import { acquisitionScope } from './iva-acquisitions.ts';
|
||
import {
|
||
addFractions,
|
||
decimalFraction,
|
||
fraction,
|
||
fractionPresentation,
|
||
type FiscalFraction,
|
||
} from './fiscal-rational.ts';
|
||
|
||
export const irpcNormalVersion = 'IRPC-normal-adjustments/1';
|
||
export const irpcNormalSource =
|
||
'CIRPC Lei34/2007 art17(1),PDF6/p768(60):resultado e correcções;art36(1)c,PDF10/p768(64):multas e encargos por infracções sem origem contratual,incluindo juros compensatórios. Lei4/2012 PDF2 altera17(6)/acrescenta36(1)o;Lei19/2013 PDF2 mantém36(1)a–o e acrescenta p/q. Lei12/2025 altera3/5/20/62/67/75,adita61-A e revoga39/47/75(2)/76;sem alteração deste recorte36(1)c. Não é apuramento integral nem Modelo20/22.';
|
||
export type NormalAdjustment = {
|
||
id: string;
|
||
sourceCode: string;
|
||
document: string;
|
||
line: string;
|
||
amount: string;
|
||
rule: '' | 'noncontractual_fine' | 'compensatory_interest' | 'other';
|
||
included: '' | 'yes' | 'no';
|
||
reference: string;
|
||
};
|
||
export type NormalBook = {
|
||
economicInclusions?: EconomicInclusions;
|
||
version: 1;
|
||
year: string;
|
||
mappingReference: string;
|
||
scope: '' | 'resident_general' | 'ep_general' | 'other';
|
||
scopeReference: string;
|
||
rows: NormalAdjustment[];
|
||
special?: SpecialInput;
|
||
profit?: NormalProfitInput;
|
||
base?: NormalBaseInput;
|
||
assessment?: NormalAssessmentInput;
|
||
schedules?: NormalSchedules;
|
||
costs?: NormalCostsInput;
|
||
depreciation?: NormalDepreciationInput;
|
||
forms?: NormalFormPreparationInput;
|
||
periodsFx?: NormalPeriodFxInput;
|
||
result?: NormalRecord;
|
||
history?: NormalRecord[];
|
||
};
|
||
export type NormalModel = {
|
||
economicInclusions?: ReturnType<typeof calculateEconomicInclusions>;
|
||
special?: SpecialModel;
|
||
formPreparation?: NormalFormPreparationModel;
|
||
periodsFx?: NormalPeriodFxModel;
|
||
depreciation?: NormalDepreciationModel;
|
||
costs?: NormalCostsModel;
|
||
fiscalPeriod?: {
|
||
kind: string;
|
||
from: string;
|
||
to: string;
|
||
reference: string;
|
||
pending: string[];
|
||
};
|
||
schedules?: SchedulesModel;
|
||
currentLoss?: FiscalFraction | null;
|
||
assessment?: NormalAssessmentModel;
|
||
colecta?: FiscalFraction | null;
|
||
base?: NormalBaseModel;
|
||
profit?: NormalProfitModel;
|
||
version: string;
|
||
year: string;
|
||
currency: string;
|
||
origin: 'projected';
|
||
accountingProfit: FiscalFraction | null;
|
||
additions: FiscalFraction;
|
||
partialAdjustedResult: FiscalFraction | null;
|
||
taxableProfit: FiscalFraction | null;
|
||
taxableBase: FiscalFraction | null;
|
||
tax: null;
|
||
rows: Array<
|
||
NormalAdjustment & {
|
||
status: 'calculated' | 'indeterminate' | 'outside_scope';
|
||
adjustment: FiscalFraction | null;
|
||
reason: string;
|
||
}
|
||
>;
|
||
pending: string[];
|
||
source: string;
|
||
};
|
||
type NormalRecord = { key: string; createdAt: string; model: NormalModel };
|
||
export const newNormalBook = (): NormalBook => ({
|
||
version: 1,
|
||
year: '',
|
||
mappingReference: '',
|
||
scope: '',
|
||
scopeReference: '',
|
||
rows: [],
|
||
});
|
||
export const newNormalAdjustment = (id: string): NormalAdjustment => ({
|
||
id,
|
||
sourceCode: '',
|
||
document: '',
|
||
line: '',
|
||
amount: '',
|
||
rule: '',
|
||
included: '',
|
||
reference: '',
|
||
});
|
||
export function rawNormalFinancialSource(d: Record<string, unknown>) {
|
||
const p = restoreBusinessPlanSnapshot(
|
||
inputText(d.business_plan_snapshot ?? ''),
|
||
);
|
||
if (!p) return null;
|
||
const forecast = forecastBusinessPlan(p),
|
||
bridge = buildFinancialFiscalBridge(forecast);
|
||
return {
|
||
...bridge,
|
||
periods: forecast.rows.length,
|
||
origin: 'projected' as const,
|
||
scope: acquisitionScope(d),
|
||
};
|
||
}
|
||
export function normalFinancialSource(d: Record<string, unknown>) {
|
||
const raw = rawNormalFinancialSource(d),
|
||
input = normalBook(d).periodsFx;
|
||
return input ? mapNormalPeriodFx(d, input, raw).source : raw;
|
||
}
|
||
function store(d: Record<string, unknown>) {
|
||
const s = JSON.parse(
|
||
inputText(d.modelo_a_snapshot || '{"version":1,"books":{}}'),
|
||
);
|
||
if (
|
||
s.version !== 1 ||
|
||
!s.books ||
|
||
typeof s.books !== 'object' ||
|
||
Array.isArray(s.books)
|
||
)
|
||
throw Error('Livros fiscais não recuperáveis;originais preservados.');
|
||
return s;
|
||
}
|
||
export function validateNormal(b: NormalBook) {
|
||
if (b?.economicInclusions !== undefined)
|
||
validateEconomicInclusions(b.economicInclusions);
|
||
if (b?.special !== undefined) validateSpecial(b.special);
|
||
if (b?.forms !== undefined) validateNormalFormPreparation(b.forms);
|
||
if (b?.periodsFx !== undefined) validateNormalPeriodFx(b.periodsFx);
|
||
if (b?.depreciation !== undefined)
|
||
validateNormalDepreciation(b.depreciation);
|
||
if (b?.costs !== undefined) validateNormalCosts(b.costs);
|
||
if (b?.schedules !== undefined) validateSchedules(b.schedules);
|
||
if (b?.assessment !== undefined) validateNormalAssessment(b.assessment);
|
||
if (b?.base !== undefined) validateNormalBase(b.base);
|
||
if (b?.profit !== undefined) validateNormalProfit(b.profit);
|
||
if (
|
||
!b ||
|
||
b.version !== 1 ||
|
||
['year', 'mappingReference', 'scopeReference'].some(
|
||
(k) => typeof b[k as 'year'] !== 'string',
|
||
) ||
|
||
!['', 'resident_general', 'ep_general', 'other'].includes(b.scope) ||
|
||
!Array.isArray(b.rows) ||
|
||
(b.history !== undefined && !Array.isArray(b.history))
|
||
)
|
||
throw Error('Preparação IRPC normal não recuperável.');
|
||
for (const r of b.rows)
|
||
if (
|
||
!r ||
|
||
Object.keys(newNormalAdjustment('')).some(
|
||
(k) => typeof r[k as keyof NormalAdjustment] !== 'string',
|
||
) ||
|
||
![
|
||
'',
|
||
'noncontractual_fine',
|
||
'compensatory_interest',
|
||
'other',
|
||
].includes(r.rule) ||
|
||
!['', 'yes', 'no'].includes(r.included)
|
||
)
|
||
throw Error(
|
||
'Ajustamento IRPC não recuperável;original conservado.',
|
||
);
|
||
if (new Set(b.rows.map((r) => r.id)).size !== b.rows.length)
|
||
throw Error('Identificador de ajustamento duplicado.');
|
||
for (const r of [b.result, ...(b.history ?? [])])
|
||
if (
|
||
r &&
|
||
(!r.model ||
|
||
![
|
||
irpcSpecialCombinationsVersion,
|
||
irpcSpecialStartPeriodVersion,
|
||
irpcSpecialCessationPriorVersion,
|
||
irpcSpecialMonthlyVersion,
|
||
irpcSpecialVersion,
|
||
irpcNormalVersion,
|
||
normalProfitVersion,
|
||
normalBaseVersion,
|
||
normalAssessmentVersion,
|
||
normalSchedulesVersion,
|
||
normalCostsVersion,
|
||
normalDepreciationVersion,
|
||
normalPeriodFxVersion,
|
||
normalFormPreparationVersion,
|
||
].includes(r.model.version) ||
|
||
typeof r.key !== 'string' ||
|
||
typeof r.createdAt !== 'string' ||
|
||
!Array.isArray(r.model.rows) ||
|
||
!Array.isArray(r.model.pending))
|
||
)
|
||
throw Error('Histórico IRPC normal não recuperável.');
|
||
}
|
||
export function normalBook(d: Record<string, unknown>): NormalBook {
|
||
const m = store(d).irpcNormal;
|
||
if (m === undefined) return newNormalBook();
|
||
if (!m || typeof m !== 'object' || Array.isArray(m))
|
||
throw Error('Mapa IRPC normal não recuperável.');
|
||
const b = m[acquisitionScope(d)] ?? newNormalBook();
|
||
validateNormal(b);
|
||
return b;
|
||
}
|
||
export function saveNormalBook(d: Record<string, unknown>, b: NormalBook) {
|
||
validateNormal(b);
|
||
const s = store(d);
|
||
if (
|
||
s.irpcNormal !== undefined &&
|
||
(!s.irpcNormal ||
|
||
typeof s.irpcNormal !== 'object' ||
|
||
Array.isArray(s.irpcNormal))
|
||
)
|
||
throw Error('Mapa IRPC normal não recuperável.');
|
||
return JSON.stringify({
|
||
...s,
|
||
irpcNormal: { ...s.irpcNormal, [acquisitionScope(d)]: b },
|
||
});
|
||
}
|
||
function key(d: Record<string, unknown>, b: NormalBook) {
|
||
return JSON.stringify([
|
||
b.special
|
||
? specialCalculationVersion(b.special)
|
||
: b.forms
|
||
? normalFormPreparationVersion
|
||
: b.periodsFx
|
||
? normalPeriodFxVersion
|
||
: b.depreciation
|
||
? normalDepreciationVersion
|
||
: b.costs
|
||
? normalCostsVersion
|
||
: b.schedules
|
||
? normalSchedulesVersion
|
||
: b.assessment
|
||
? normalAssessmentVersion
|
||
: b.base
|
||
? normalBaseVersion
|
||
: b.profit
|
||
? normalProfitVersion
|
||
: irpcNormalVersion,
|
||
acquisitionScope(d),
|
||
d.business_plan_snapshot ?? '',
|
||
b.year,
|
||
b.mappingReference,
|
||
b.scope,
|
||
b.scopeReference,
|
||
b.rows,
|
||
...(b.profit ? [b.profit] : []),
|
||
...(b.economicInclusions ? [b.economicInclusions] : []),
|
||
...(b.base ? [b.base] : []),
|
||
...(b.assessment ? [b.assessment] : []),
|
||
...(b.schedules ? [b.schedules] : []),
|
||
...(b.costs ? [b.costs] : []),
|
||
...(b.depreciation ? [b.depreciation] : []),
|
||
...(b.periodsFx
|
||
? [b.periodsFx, normalPeriodFxDependency(d, b.periodsFx)]
|
||
: []),
|
||
...(b.forms ? [b.forms] : []),
|
||
...(b.special ? [b.special, specialDependency(d)] : []),
|
||
]);
|
||
}
|
||
export function normalCurrent(d: Record<string, unknown>, b: NormalBook) {
|
||
return !!b.result && b.result.key === key(d, b);
|
||
}
|
||
const zero = () => fraction(0n, 1n);
|
||
const cents = (s: string) => {
|
||
if (!/^-?\d+(?:\.\d{1,2})?$/.test(s))
|
||
throw Error('Montante MZN inválido;máximo dois decimais.');
|
||
const v = decimalFraction(s.startsWith('-') ? s.slice(1) : s);
|
||
return s.startsWith('-')
|
||
? fraction(-BigInt(v.numerator), BigInt(v.denominator))
|
||
: v;
|
||
};
|
||
const integerCents = (v: FiscalFraction) =>
|
||
(BigInt(v.numerator) * 100n) / BigInt(v.denominator);
|
||
export function calculateNormal(
|
||
d: Record<string, unknown>,
|
||
b: NormalBook,
|
||
): NormalModel {
|
||
validateNormal(b);
|
||
const pending: string[] = [],
|
||
raw = rawNormalFinancialSource(d),
|
||
periodsFx = b.periodsFx
|
||
? mapNormalPeriodFx(d, b.periodsFx, raw)
|
||
: undefined,
|
||
source = periodsFx ? periodsFx.source : raw,
|
||
fiscalPeriod = periodsFx
|
||
? {
|
||
kind: b.periodsFx!.kind,
|
||
from: b.periodsFx!.from,
|
||
to: b.periodsFx!.to,
|
||
reference: b.periodsFx!.reference,
|
||
pending: periodsFx.pending,
|
||
}
|
||
: b.schedules
|
||
? normalSchedulePeriod(b.schedules, b.scope)
|
||
: undefined;
|
||
if (
|
||
periodsFx &&
|
||
b.periodsFx!.kind === 'annual' &&
|
||
b.periodsFx!.from !== '2026-01-01'
|
||
) {
|
||
const formal = b.schedules
|
||
? normalSchedulePeriod(b.schedules, b.scope)
|
||
: null;
|
||
if (
|
||
!formal ||
|
||
formal.pending.length ||
|
||
formal.from !== b.periodsFx!.from ||
|
||
formal.to !== b.periodsFx!.to
|
||
)
|
||
fiscalPeriod!.pending.push(
|
||
'Ano diferente do civil exige o período autorizado/comunicado já documentado nos mapas,com as mesmas datas.',
|
||
);
|
||
}
|
||
if (fiscalPeriod) pending.push(...fiscalPeriod.pending);
|
||
if (!source) pending.push('Falta plano financeiro reconciliado.');
|
||
if (source?.currency !== 'MZN')
|
||
pending.push(
|
||
'Ponte em MZN necessária;sem conversão fiscal anual presumida.',
|
||
);
|
||
if (
|
||
!/^2026$/.test(b.year) ||
|
||
!b.mappingReference.trim() ||
|
||
source?.periods !== (periodsFx?.months ?? 12)
|
||
)
|
||
pending.push(
|
||
'Documente correspondência dos 12 meses do plano ao exercício2026;outros exercícios/horizontes permanecem separados.',
|
||
);
|
||
if (
|
||
!(
|
||
b.scope === 'resident_general' ||
|
||
(b.schedules && b.scope === 'ep_general')
|
||
) ||
|
||
!b.scopeReference.trim()
|
||
)
|
||
pending.push(
|
||
'Identifique sujeito residente no regime geral,fora de transparência/regimes especiais;EP e outros âmbitos não abrangidos.',
|
||
);
|
||
const rows: NormalModel['rows'] = b.rows.map((r) => ({
|
||
...structuredClone(r),
|
||
status: 'indeterminate',
|
||
adjustment: null,
|
||
reason: '',
|
||
}));
|
||
const amounts = new Map<string, FiscalFraction>(),
|
||
groups = new Map<string, NormalModel['rows']>();
|
||
for (const r of rows) {
|
||
const line = source?.accountingLines.find(
|
||
(l) => l.code === r.sourceCode,
|
||
);
|
||
const issue: string[] = [];
|
||
if (!r.document.trim() || !r.line.trim() || !r.reference.trim())
|
||
issue.push(
|
||
'Identifique documento,linha e suporte da qualificação.',
|
||
);
|
||
if (!line || line.accountingEffect !== 'expense')
|
||
issue.push('Seleccione linha de custo existente na ponte.');
|
||
if (r.included !== 'yes')
|
||
issue.push(
|
||
'Custo incluído no resultado de partida por demonstrar;sem acréscimo duplicado de montante já excluído.',
|
||
);
|
||
try {
|
||
const a = cents(r.amount);
|
||
if (integerCents(a) <= 0n) throw Error();
|
||
amounts.set(r.id, a);
|
||
} catch {
|
||
issue.push(
|
||
'Montante positivo MZN,até dois decimais,por identificar.',
|
||
);
|
||
}
|
||
if (r.rule === 'other') {
|
||
r.status = 'outside_scope';
|
||
issue.push(
|
||
'Outro ajustamento por corresponder;sem regra inventada.',
|
||
);
|
||
} else if (!r.rule)
|
||
issue.push(
|
||
'Seleccione natureza da multa não contratual ou dos juros compensatórios;juros comerciais/penalidades contratuais não equiparados.',
|
||
);
|
||
if (
|
||
rows.some(
|
||
(q) =>
|
||
q !== r &&
|
||
q.document.trim() === r.document.trim() &&
|
||
q.line.trim() === r.line.trim() &&
|
||
r.document.trim() &&
|
||
r.line.trim(),
|
||
)
|
||
)
|
||
issue.push(
|
||
'Documento/linha repetidos;reconcilie sem duplicar o encargo.',
|
||
);
|
||
if (line) {
|
||
const g = groups.get(line.code) ?? [];
|
||
g.push(r);
|
||
groups.set(line.code, g);
|
||
}
|
||
r.reason = issue.join(' ');
|
||
}
|
||
for (const [code, group] of groups) {
|
||
const line = source!.accountingLines.find((l) => l.code === code)!;
|
||
const requested = addFractions(
|
||
group.map((r) => amounts.get(r.id) ?? zero()),
|
||
);
|
||
if (integerCents(requested) > integerCents(cents(line.total)))
|
||
for (const r of group)
|
||
r.reason +=
|
||
' Parcelas excedem o custo da linha de origem;sem dupla contagem.';
|
||
}
|
||
for (const r of rows) {
|
||
if (!r.reason) {
|
||
r.status = 'calculated';
|
||
r.adjustment = amounts.get(r.id)!;
|
||
r.reason =
|
||
'Acréscimo integral de custo incluído no resultado de partida,qualificado pelo utilizador como ' +
|
||
(r.rule === 'noncontractual_fine'
|
||
? 'multa/encargo de infracção sem origem contratual'
|
||
: 'juros compensatórios') +
|
||
';CIRPC36(1)c. Projecção pelos pressupostos do plano.';
|
||
} else pending.push(r.document + ': ' + r.reason);
|
||
}
|
||
const additions = addFractions(
|
||
rows.flatMap((r) => (r.adjustment ? [r.adjustment] : [])),
|
||
);
|
||
const accountingProfit =
|
||
source?.currency === 'MZN'
|
||
? cents(source.accountingProfitBeforeTax)
|
||
: null;
|
||
const contextReady =
|
||
(!fiscalPeriod || fiscalPeriod.pending.length === 0) &&
|
||
!!source &&
|
||
source.currency === 'MZN' &&
|
||
source.periods === (periodsFx?.months ?? 12) &&
|
||
b.year === '2026' &&
|
||
!!b.mappingReference.trim() &&
|
||
(b.scope === 'resident_general' ||
|
||
(!!b.schedules && b.scope === 'ep_general')) &&
|
||
!!b.scopeReference.trim();
|
||
if (rows.length === 0 && !b.profit)
|
||
pending.push(
|
||
'Sem ajustamentos documentados;ausência de registos não prova inexistência de correcções.',
|
||
);
|
||
if (!b.profit)
|
||
pending.push(
|
||
'L6 parcial:resultado ajustado apenas destes acréscimos;restantes custos/variações patrimoniais,amortizações,perdas,deduções e colecta por tratar. Lucro tributável,matéria colectável e imposto indeterminados.',
|
||
);
|
||
const depreciation = b.depreciation
|
||
? calculateNormalDepreciation(
|
||
b.depreciation,
|
||
source,
|
||
fiscalPeriod ?? {
|
||
from: b.year + '-01-01',
|
||
to: b.year + '-12-31',
|
||
},
|
||
[
|
||
...b.rows,
|
||
...(b.schedules?.rows ?? []),
|
||
...(b.costs?.rows ?? []),
|
||
],
|
||
!!periodsFx?.short,
|
||
)
|
||
: undefined;
|
||
const schedules = b.schedules
|
||
? calculateSchedules(
|
||
b.schedules,
|
||
source,
|
||
rows,
|
||
depreciation
|
||
? {
|
||
value: depreciation.adjustment,
|
||
pending: depreciation.pending,
|
||
}
|
||
: undefined,
|
||
)
|
||
: undefined;
|
||
const costs = b.costs
|
||
? calculateNormalCosts(
|
||
b.costs,
|
||
source,
|
||
[...b.rows, ...(b.schedules?.rows ?? [])],
|
||
fiscalPeriod ?? { from: '2026-01-01', to: b.year + '-12-31' },
|
||
!!periodsFx?.short,
|
||
)
|
||
: undefined;
|
||
const economicInclusions = b.economicInclusions
|
||
? calculateEconomicInclusions(
|
||
b.economicInclusions,
|
||
b.base?.subjectNuit ?? '',
|
||
b.year,
|
||
)
|
||
: undefined;
|
||
if (
|
||
economicInclusions &&
|
||
(b.special ||
|
||
b.periodsFx ||
|
||
(b.schedules && b.schedules.period.kind !== 'calendar') ||
|
||
b.scope !== 'resident_general')
|
||
)
|
||
economicInclusions.pending.push(
|
||
'Inclusão económica automática delimitada à base ordinária residente/ano civil2026/MZN; este âmbito exige correspondência própria.',
|
||
);
|
||
const profit = b.profit
|
||
? closeNormalProfit(
|
||
b.profit,
|
||
source,
|
||
rows,
|
||
accountingProfit,
|
||
additions,
|
||
contextReady,
|
||
schedules,
|
||
costs,
|
||
depreciation,
|
||
economicInclusions,
|
||
)
|
||
: undefined;
|
||
if (profit && periodsFx)
|
||
profit.source =
|
||
'CIRPC17/18/20/21/22/24/36;' +
|
||
periodsFx.rule +
|
||
' L7 mantém benefícios/casos especiais;L8 mantém saldo final.';
|
||
if (profit)
|
||
pending.push(
|
||
...profit.pending,
|
||
...(!b.base
|
||
? [
|
||
'Matéria colectável,perdas reportadas,deduções e imposto não apurados neste percurso.',
|
||
]
|
||
: []),
|
||
);
|
||
const base = b.base
|
||
? calculateNormalBase(
|
||
b.base,
|
||
profit?.taxableProfit ?? null,
|
||
b.year,
|
||
b.schedules?.currentLossReference,
|
||
!!periodsFx?.short,
|
||
)
|
||
: undefined;
|
||
if (base)
|
||
pending.push(
|
||
...base.pending,
|
||
...(!b.assessment
|
||
? [
|
||
'Colecta/imposto a pagar,créditos anuais eM20/M22 não determinados;matéria colectável não é imposto.',
|
||
]
|
||
: []),
|
||
);
|
||
const assessment = b.assessment
|
||
? calculateNormalAssessment(b.assessment, base?.taxableBase ?? null, {
|
||
year: b.year,
|
||
scope: b.scope,
|
||
extended: !!b.schedules || !!periodsFx,
|
||
short: !!periodsFx?.short,
|
||
currency: source?.currency ?? '',
|
||
...(fiscalPeriod
|
||
? { period: { from: fiscalPeriod.from, to: fiscalPeriod.to } }
|
||
: {}),
|
||
})
|
||
: undefined;
|
||
if (assessment)
|
||
pending.push(...assessment.pending, assessment.finalBalance.reason);
|
||
const currentLoss =
|
||
b.schedules &&
|
||
profit?.taxableProfit &&
|
||
BigInt(profit.taxableProfit.numerator) < 0n
|
||
? fraction(
|
||
-BigInt(profit.taxableProfit.numerator),
|
||
BigInt(profit.taxableProfit.denominator),
|
||
)
|
||
: null;
|
||
const model: NormalModel = {
|
||
...(periodsFx ? { periodsFx } : {}),
|
||
...(depreciation ? { depreciation } : {}),
|
||
...(costs ? { costs } : {}),
|
||
...(schedules ? { schedules, currentLoss, fiscalPeriod } : {}),
|
||
...(assessment ? { assessment, colecta: assessment.colecta } : {}),
|
||
...(base ? { base } : {}),
|
||
...(profit ? { profit } : {}),
|
||
version: b.special
|
||
? specialCalculationVersion(b.special)
|
||
: b.forms
|
||
? normalFormPreparationVersion
|
||
: b.periodsFx
|
||
? normalPeriodFxVersion
|
||
: b.depreciation
|
||
? normalDepreciationVersion
|
||
: b.costs
|
||
? normalCostsVersion
|
||
: b.schedules
|
||
? normalSchedulesVersion
|
||
: b.assessment
|
||
? normalAssessmentVersion
|
||
: b.base
|
||
? normalBaseVersion
|
||
: b.profit
|
||
? normalProfitVersion
|
||
: irpcNormalVersion,
|
||
year: b.year,
|
||
currency: source?.currency ?? '',
|
||
origin: 'projected',
|
||
accountingProfit,
|
||
additions,
|
||
partialAdjustedResult:
|
||
contextReady &&
|
||
rows.length > 0 &&
|
||
rows.every((r) => r.status === 'calculated') &&
|
||
accountingProfit
|
||
? addFractions([accountingProfit, additions])
|
||
: null,
|
||
taxableProfit: profit?.taxableProfit ?? null,
|
||
...(economicInclusions ? { economicInclusions } : {}),
|
||
taxableBase: base?.taxableBase ?? null,
|
||
tax: null,
|
||
rows,
|
||
pending,
|
||
source:
|
||
irpcNormalSource +
|
||
(periodsFx
|
||
? ' ' + periodsFx.rule
|
||
: b.schedules
|
||
? ' Mapas complementares: CIRPC18/21/22/24/36; Decreto72/2013 art3–7. EP: CIRPC15(1)c, apenas resultado imputável ao estabelecimento no regime geral, anual2026/MZN.'
|
||
: ''),
|
||
};
|
||
if (b.special) model.special = calculateSpecial(d, b, model, source);
|
||
if (b.forms) {
|
||
model.formPreparation = prepareNormalForms(b.forms, model, source);
|
||
model.pending.push(
|
||
...model.formPreparation.pending.map(
|
||
(p) => 'Preparação M20/M22: ' + p,
|
||
),
|
||
);
|
||
}
|
||
return model;
|
||
}
|
||
export function recordNormal(
|
||
d: Record<string, unknown>,
|
||
b: NormalBook,
|
||
now = new Date().toISOString(),
|
||
): NormalBook {
|
||
return {
|
||
...b,
|
||
history: b.result
|
||
? [...(b.history ?? []), structuredClone(b.result)]
|
||
: (b.history ?? []),
|
||
result: {
|
||
key: key(d, b),
|
||
createdAt: now,
|
||
model: calculateNormal(d, b),
|
||
},
|
||
};
|
||
}
|
||
const money = (v: FiscalFraction | null) =>
|
||
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
|
||
function normalBaseReport(
|
||
m: NormalBaseModel,
|
||
): Array<[string, string, string, string]> {
|
||
return [
|
||
[
|
||
'IRPC normal — L6',
|
||
'Matéria colectável — deduções/perdas',
|
||
`Lucro tributável:${money(m.taxableProfit)};perdas propostas elegíveis:${money(m.requestedLosses)};perdas deduzidas nesta simulação:${money(m.deductedLosses)};benefícios deduzidos:${money(m.benefitDeduction)};matéria colectável simulada:${money(m.taxableBase)};imposto a pagar:Indeterminado. ${m.pending.join(' | ')}`,
|
||
`NUIT:${m.input.subjectNuit};${m.input.identityReference};método:${m.input.method === 'direct' ? 'Declarativo/directo' : m.input.method === 'indirect' ? 'Indirecto' : 'Por identificar'};${m.input.methodReference};perdas:${m.input.lossesState === 'absent' ? 'Ausência declarada' : m.input.lossesState === 'known' ? 'Conhecidas' : 'Informação em falta'};${m.input.lossesReference};objecto:${m.input.unchangedObject === 'yes' ? 'Sem alteração declarada' : m.input.unchangedObject === 'no' ? 'Alteração declarada' : 'Por esclarecer'};${m.input.objectReference};${m.source}`,
|
||
],
|
||
[
|
||
'IRPC normal — L6',
|
||
'Benefícios à matéria colectável',
|
||
`Estado:${m.input.benefits.state === 'known' ? 'Valor conhecido' : m.input.benefits.state === 'absent' ? 'Ausência declarada' : 'Informação em falta'};montante original:${m.input.benefits.amount || 'Não preenchido'};dedução:${money(m.benefitDeduction)};não inclui créditos à colecta ou retenções sofridas.`,
|
||
m.input.benefits.reference,
|
||
],
|
||
...m.losses.map(
|
||
(l) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
'Perda fiscal de ' + l.year,
|
||
`Estado:${l.status === 'eligible' ? 'Elegível neste recorte' : l.status === 'excluded' ? 'Excluída neste exercício' : 'Indeterminada'};apurada:${l.assessed};utilizada antes:${l.previouslyUsed};saldo:${money(l.available)};utilização proposta:${l.requested};dedução nesta simulação:${money(l.deducted)};saldo aritmético posterior:${money(l.remaining)};último exercício ordinário:${l.lastOrdinaryYear ?? 'Por determinar'};${l.reason}`,
|
||
`NUIT:${l.subjectNuit};${l.reference};sem consumo do saldo oficial nem reporte automático.`,
|
||
] as [string, string, string, string],
|
||
),
|
||
];
|
||
}
|
||
function normalAssessmentReport(
|
||
m: NormalAssessmentModel,
|
||
): Array<[string, string, string, string]> {
|
||
return [
|
||
[
|
||
'IRPC normal — L6',
|
||
'Colecta — regime normal',
|
||
`Matéria colectável:${money(m.taxableBase)};taxa:${m.rate ? m.rate.numerator + '%' : 'Por identificar'};colecta simulada:${money(m.colecta)};${m.classification};${m.centExact === false ? 'Valor racional exacto com fracção de cêntimo;sem arredondamento fiscal aplicado.' : ''}${m.pending.join(' | ')}`,
|
||
`Exercício:${m.period.year};período:${m.period.from}–${m.period.to};${m.input.reference};${m.rule};${m.source}`,
|
||
],
|
||
[
|
||
'IRPC normal — L6',
|
||
'Saldo final — âmbito L8',
|
||
'A pagar:Indeterminado;a recuperar:Indeterminado. ' +
|
||
m.finalBalance.reason,
|
||
'Sequência:lucro tributável → matéria colectável → colecta → saldo final;retenções,pagamentos e créditos anuais reservados aL8.',
|
||
],
|
||
];
|
||
}
|
||
export function normalReportRows(
|
||
d: Record<string, unknown>,
|
||
): Array<[string, string, string, string]> {
|
||
try {
|
||
const b = normalBook(d),
|
||
r = b.result;
|
||
if (!r) return [];
|
||
const m = r.model;
|
||
return [
|
||
...(m.economicInclusions?.effects.map(
|
||
(e) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
'Inclusão/correcção económica — ' + e.id,
|
||
'Parcela reconciliada: ' +
|
||
money(e.amount) +
|
||
'; efeito com sinal: ' +
|
||
money(e.effect),
|
||
e.reason,
|
||
] as [string, string, string, string],
|
||
) ?? []),
|
||
[
|
||
'IRPC normal — L6',
|
||
'Ajustamentos ao resultado financeiro',
|
||
`Exercício:${m.year};resultado contabilístico antes de imposto:${money(m.accountingProfit)};acréscimos demonstrados:${money(m.additions)};resultado parcial ajustado:${money(m.partialAdjustedResult)};${m.profit ? 'lucro tributável simulado:' + money(m.taxableProfit) + (m.base ? ';matéria colectável simulada:' + money(m.taxableBase) + (m.assessment ? ';colecta simulada:' + money(m.colecta ?? null) + ';saldo final a pagar ou recuperar:Indeterminado (L8).' : ';imposto a pagar:Indeterminado.') : ';matéria colectável/imposto:Indeterminados.') : 'lucro tributável/matéria colectável/imposto:Indeterminados.'} Projecção com pressupostos;${m.pending.join(' | ')}`,
|
||
`${normalCurrent(d, b) ? 'Entradas correspondentes' : 'Histórico;origem ou entradas alteradas'};${r.createdAt};${m.source}`,
|
||
],
|
||
...(m.special ? specialReportRows(m.special) : []),
|
||
...(m.formPreparation
|
||
? normalFormPreparationReport(m.formPreparation)
|
||
: []),
|
||
...(m.periodsFx ? normalPeriodFxReport(m.periodsFx) : []),
|
||
...(m.depreciation ? normalDepreciationReport(m.depreciation) : []),
|
||
...(m.costs ? normalCostReport(m.costs) : []),
|
||
...(m.base ? normalBaseReport(m.base) : []),
|
||
...(m.assessment ? normalAssessmentReport(m.assessment) : []),
|
||
...(m.schedules
|
||
? [
|
||
[
|
||
'IRPC normal — L6',
|
||
'Mapas complementares',
|
||
`Efeito líquido adicional:${money(m.schedules.net)};perda fiscal do próprio exercício:${money(m.currentLoss ?? null)};sem reporte ou consumo automático. ${m.schedules.pending.join(' | ')}`,
|
||
m.schedules.source,
|
||
] as [string, string, string, string],
|
||
...m.schedules.effects.map((e) => {
|
||
const i = m.schedules!.input.rows.find(
|
||
(r) => r.id === e.id,
|
||
)!;
|
||
return [
|
||
'IRPC normal — L6',
|
||
i.document + '/' + i.line,
|
||
`Montante:${money(e.amount)};efeito:${money(e.effect)};${e.reason}`,
|
||
`${i.reference};exercício de imputação:${i.fiscalYear || 'Não aplicável'}`,
|
||
] as [string, string, string, string];
|
||
}),
|
||
...m.schedules.assets.map(
|
||
(a) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
a.description,
|
||
`Depreciação contabilística:${money(a.accountingValue)};quota fiscal:${money(a.fiscal)};acréscimo:${money(a.adjustment)};${a.reason}`,
|
||
`${a.tableReference};${a.reference}`,
|
||
] as [string, string, string, string],
|
||
),
|
||
[
|
||
'IRPC normal — L6',
|
||
'Juros financeiros ordinários',
|
||
`Remanescente aceite no recorte:${money(m.schedules.interest)};não corresponde a juros de mora/compensatórios L5.`,
|
||
m.schedules.input.interest.reference,
|
||
] as [string, string, string, string],
|
||
]
|
||
: []),
|
||
...(m.schedules && !m.formPreparation
|
||
? [
|
||
...normalFormFields(m).fields.map(
|
||
(f) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
'M22 campo ' + f.code + ' — ' + f.label,
|
||
`${f.display};exacto:${f.value ? f.value.numerator + '/' + f.value.denominator : 'Indeterminado'};transcrição:${f.transcriptionReady ? 'Preparação parcial com valor exacto ao cêntimo' : 'Pendente ou sem aplicação'};${f.reason}`,
|
||
normalFormFields(m).source,
|
||
] as [string, string, string, string],
|
||
),
|
||
...normalFormFields(m).m20.fields.map(
|
||
(f) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
'M20 A.1 — conta ' +
|
||
f.account +
|
||
' — ' +
|
||
f.label,
|
||
`${f.display};representação em meticais inteiros:${f.transcriptionReady ? 'Exacta' : 'Pendente'};${f.reason}`,
|
||
`${f.reference};${normalFormFields(m).m20.source}`,
|
||
] as [string, string, string, string],
|
||
),
|
||
[
|
||
'IRPC normal — L6',
|
||
'M20 — correspondência',
|
||
normalFormFields(m).m20.reason,
|
||
normalFormFields(m).m20.source,
|
||
] as [string, string, string, string],
|
||
]
|
||
: []),
|
||
...m.rows.map(
|
||
(r) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
r.document + '/' + r.line,
|
||
`${{ calculated: 'Calculado neste recorte', indeterminate: 'Indeterminado', outside_scope: 'Fora deste recorte' }[r.status]};montante:${r.amount};acréscimo:${money(r.adjustment)};${r.reason}`,
|
||
`Origem:${({ revenue: 'Receitas', cost_of_sales: 'Custo das vendas', operating_expenses: 'Despesas operacionais', personnel: 'Pessoal', depreciation: 'Depreciação contabilística', interest_expense: 'Juros reconhecidos' } as Record<string, string>)[r.sourceCode] ?? 'Por corresponder'};incluído:${r.included === 'yes' ? 'Sim' : r.included === 'no' ? 'Não' : 'Por demonstrar'};suporte:${r.reference}`,
|
||
] as [string, string, string, string],
|
||
),
|
||
...(m.profit
|
||
? [
|
||
...m.profit.lines.map(
|
||
(l) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
'Conferência: ' + l.label,
|
||
`Estado:${l.state === 'known' ? 'Valor conhecido' : l.state === 'absent' ? 'Ausência declarada' : 'Informação em falta'};origem:${l.amount};acréscimos:${money(l.addedBack)};remanescente:${money(l.remainder)};${l.reason}`,
|
||
l.reference,
|
||
] as [string, string, string, string],
|
||
),
|
||
...m.profit.topics.map(
|
||
(t) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
t.label,
|
||
`Estado:${t.state === 'known' ? 'Valor conhecido' : t.state === 'absent' ? 'Ausência declarada' : 'Informação em falta'};montante:${money(t.value)};${t.reason}`,
|
||
t.reference,
|
||
] as [string, string, string, string],
|
||
),
|
||
[
|
||
'IRPC normal — L6',
|
||
'Âmbito da conferência',
|
||
m.profit.input.completenessReference,
|
||
m.profit.source,
|
||
] as [string, string, string, string],
|
||
]
|
||
: []),
|
||
];
|
||
} catch (e) {
|
||
return [
|
||
[
|
||
'IRPC normal — L6',
|
||
'Não recuperável',
|
||
String(e),
|
||
'Originais conservados.',
|
||
],
|
||
];
|
||
}
|
||
}
|
||
export function normalHtml(d: Record<string, unknown>) {
|
||
const b = normalBook(d);
|
||
if (!normalCurrent(d, b))
|
||
throw Error('Calcule e guarde as entradas actuais antes de exportar.');
|
||
const esc = (s: string) =>
|
||
s.replace(
|
||
/[&<>"']/g,
|
||
(c) =>
|
||
({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": ''',
|
||
})[c]!,
|
||
);
|
||
return (
|
||
'<!doctype html><html lang="pt"><meta charset="utf-8"><title>IRPC normal — ajustamentos</title><h1>IRPC — L6 de referência e tratamentos L7 quando qualificados</h1><p>Projecção pelos pressupostos do plano. Não é demonstração contabilística,liquidação ou Modelo20/22.</p>' +
|
||
normalReportRows(d)
|
||
.map(
|
||
(r) =>
|
||
'<section><h2>' +
|
||
esc(r[1]) +
|
||
'</h2><p>' +
|
||
esc(r[2]) +
|
||
'</p><small>' +
|
||
esc(r[3]) +
|
||
'</small></section>',
|
||
)
|
||
.join('') +
|
||
'</html>'
|
||
);
|
||
}
|