764 lines
28 KiB
TypeScript
764 lines
28 KiB
TypeScript
import {
|
||
addFractions,
|
||
decimalFraction,
|
||
fraction,
|
||
fractionPresentation,
|
||
subtractFractions,
|
||
type FiscalFraction,
|
||
} from './fiscal-rational.ts';
|
||
import type { normalFinancialSource } from './irpc-normal.ts';
|
||
import type { SchedulesModel } from './irpc-normal-schedules.ts';
|
||
import type { NormalCostsModel } from './irpc-normal-costs.ts';
|
||
import type { NormalDepreciationModel } from './irpc-normal-depreciation.ts';
|
||
import type { NormalBaseModel } from './irpc-normal-base.ts';
|
||
export const normalFormPreparationVersion = 'IRPC-normal-adjustments/9';
|
||
export const m20Accounts = {
|
||
'7.1': 'Venda de bens',
|
||
'7.2': 'Prestação de serviços',
|
||
'7.3': 'Investimentos realizados pela própria empresa',
|
||
'7.4': 'Reversões do período',
|
||
'7.5': 'Rendimentos suplementares',
|
||
'7.6': 'Outros rendimentos e ganhos operacionais',
|
||
'7.8': 'Rendimentos e ganhos financeiros',
|
||
'7.9': 'Ganho por aumento do justo valor',
|
||
'6.1.1': 'Custos dos inventários vendidos ou consumidos',
|
||
'6.2': 'Gastos com pessoal',
|
||
'6.3': 'Fornecimentos e serviços de terceiros',
|
||
'6.4': 'Perda por imparidade / ajustamento',
|
||
'6.5': 'Amortizações do período',
|
||
'6.6': 'Provisões do período',
|
||
'6.7': 'Perda por redução do justo valor',
|
||
'6.8': 'Outros gastos e perdas operacionais',
|
||
'6.9': 'Gastos e perdas financeiras',
|
||
'8.5': 'Imposto sobre o rendimento',
|
||
} as const;
|
||
export type FormAllocation = {
|
||
id: string;
|
||
sourceCode: string;
|
||
account: '' | keyof typeof m20Accounts;
|
||
amount: string;
|
||
reference: string;
|
||
};
|
||
export type NormalFormPreparationInput = {
|
||
version: 1;
|
||
edition: '' | 'accepted_pdf_and_m20a1';
|
||
reference: string;
|
||
completenessReference: string;
|
||
allocations: FormAllocation[];
|
||
incomeTax: {
|
||
state: '' | 'known' | 'absent';
|
||
amount: string;
|
||
placement: '' | 'included' | 'outside';
|
||
reference: string;
|
||
};
|
||
reducedRate: { state: '' | 'known' | 'absent'; reference: string };
|
||
};
|
||
export const newNormalFormPreparation = (): NormalFormPreparationInput => ({
|
||
version: 1,
|
||
edition: '',
|
||
reference: '',
|
||
completenessReference: '',
|
||
allocations: [],
|
||
incomeTax: { state: '', amount: '', placement: '', reference: '' },
|
||
reducedRate: { state: '', reference: '' },
|
||
});
|
||
export const newFormAllocation = (id: string): FormAllocation => ({
|
||
id,
|
||
sourceCode: '',
|
||
account: '',
|
||
amount: '',
|
||
reference: '',
|
||
});
|
||
const allowed: Record<string, string[]> = {
|
||
revenue: ['7.1', '7.2', '7.3', '7.4', '7.5', '7.6', '7.8', '7.9'],
|
||
cost_of_sales: ['6.1.1'],
|
||
personnel: ['6.2'],
|
||
depreciation: ['6.5'],
|
||
interest_expense: ['6.9'],
|
||
operating_expenses: ['6.3', '6.4', '6.6', '6.7', '6.8', '8.5'],
|
||
};
|
||
export const normalFormAccountsFor = (source: string) => allowed[source] ?? [];
|
||
export function validateNormalFormPreparation(i: NormalFormPreparationInput) {
|
||
if (
|
||
!i ||
|
||
i.version !== 1 ||
|
||
!['', 'accepted_pdf_and_m20a1'].includes(i.edition) ||
|
||
typeof i.reference !== 'string' ||
|
||
typeof i.completenessReference !== 'string' ||
|
||
!Array.isArray(i.allocations) ||
|
||
new Set(i.allocations.map((a) => a.id)).size !== i.allocations.length ||
|
||
i.allocations.some(
|
||
(a) =>
|
||
!a ||
|
||
Object.keys(newFormAllocation('')).some(
|
||
(k) => typeof a[k as keyof FormAllocation] !== 'string',
|
||
) ||
|
||
(a.account !== '' && !Object.hasOwn(m20Accounts, a.account)),
|
||
) ||
|
||
!i.incomeTax ||
|
||
!['', 'known', 'absent'].includes(i.incomeTax.state) ||
|
||
!['', 'included', 'outside'].includes(i.incomeTax.placement) ||
|
||
typeof i.incomeTax.amount !== 'string' ||
|
||
typeof i.incomeTax.reference !== 'string' ||
|
||
!i.reducedRate ||
|
||
!['', 'known', 'absent'].includes(i.reducedRate.state) ||
|
||
typeof i.reducedRate.reference !== 'string'
|
||
)
|
||
throw Error(
|
||
'Preparação M20/M22 não recuperável;originais conservados.',
|
||
);
|
||
}
|
||
const zero = () => fraction(0n),
|
||
neg = (v: FiscalFraction) =>
|
||
fraction(-BigInt(v.numerator), BigInt(v.denominator)),
|
||
positive = (v: FiscalFraction) => BigInt(v.numerator) >= 0n;
|
||
const eq = (a: FiscalFraction, b: FiscalFraction) =>
|
||
BigInt(a.numerator) * BigInt(b.denominator) ===
|
||
BigInt(b.numerator) * BigInt(a.denominator);
|
||
function money(s: string) {
|
||
if (!/^\d{1,18}(?:\.\d{1,6})?$/.test(s))
|
||
throw Error(
|
||
'Montante não negativo conhecido em falta;zero não é presumido.',
|
||
);
|
||
return decimalFraction(s);
|
||
}
|
||
export function normalFormRepresentation(
|
||
v: FiscalFraction | null,
|
||
unit: 'whole' | 'cent' = 'cent',
|
||
parentheses = false,
|
||
) {
|
||
if (!v)
|
||
return {
|
||
exact: null,
|
||
display: 'Indeterminado',
|
||
represented: null,
|
||
ready: false,
|
||
reason: 'Valor em falta;sem zero presumido.',
|
||
};
|
||
const n = BigInt(v.numerator),
|
||
d = BigInt(v.denominator);
|
||
if (d <= 0n) throw Error('Representação racional inválida.');
|
||
const p = fractionPresentation(v),
|
||
whole = n % d === 0n,
|
||
cent = (n * 100n) % d === 0n;
|
||
const abs = n < 0n ? -n : n,
|
||
scaled = (abs * 100n) / d;
|
||
const exact = n + '/' + d + ' MZN',
|
||
value = whole
|
||
? parentheses && n < 0n
|
||
? '(' + String(-n / d) + ')'
|
||
: String(n / d)
|
||
: unit === 'cent' && cent
|
||
? (n < 0n ? '-' : '') +
|
||
scaled / 100n +
|
||
'.' +
|
||
String(scaled % 100n).padStart(2, '0')
|
||
: null;
|
||
return {
|
||
exact,
|
||
display: whole
|
||
? value + ' MZN'
|
||
: (p.approximate ? '≈' : '') +
|
||
p.decimal +
|
||
' MZN (exacto ' +
|
||
n +
|
||
'/' +
|
||
d +
|
||
')',
|
||
represented: value,
|
||
ready: value !== null,
|
||
reason: whole
|
||
? 'Valor exacto em meticais inteiros,sem arredondamento.'
|
||
: unit === 'whole'
|
||
? 'Exemplar M20 exige meticais sem centavos;regra de representação da fracção por corresponder.'
|
||
: cent
|
||
? 'Valor exacto ao cêntimo na preparação M22 já demonstrada;sem arredondamento fiscal ou declaração oficial presumidos.'
|
||
: 'Fracção de cêntimo conservada;regra de representação fiscal por corresponder.',
|
||
};
|
||
}
|
||
type Financial = ReturnType<typeof normalFinancialSource>;
|
||
export function reuseDirectFormAllocations(source: Financial) {
|
||
return (source?.accountingLines ?? [])
|
||
.filter((l) =>
|
||
[
|
||
'cost_of_sales',
|
||
'personnel',
|
||
'depreciation',
|
||
'interest_expense',
|
||
].includes(l.code),
|
||
)
|
||
.map((l) => ({
|
||
id: 'source-' + l.code,
|
||
sourceCode: l.code,
|
||
account: allowed[l.code][0] as keyof typeof m20Accounts,
|
||
amount: l.total,
|
||
reference:
|
||
'Reutilizado da linha financeira ' +
|
||
l.origin +
|
||
';conferir completude da conta no suporte da preparação.',
|
||
}));
|
||
}
|
||
type Model = {
|
||
accountingProfit: FiscalFraction | null;
|
||
taxableProfit: FiscalFraction | null;
|
||
taxableBase: FiscalFraction | null;
|
||
colecta?: FiscalFraction | null;
|
||
rows: Array<{
|
||
rule: string;
|
||
document: string;
|
||
line: string;
|
||
adjustment: FiscalFraction | null;
|
||
}>;
|
||
schedules?: SchedulesModel;
|
||
depreciation?: NormalDepreciationModel;
|
||
costs?: NormalCostsModel;
|
||
base?: NormalBaseModel;
|
||
};
|
||
export function prepareNormalForms(
|
||
input: NormalFormPreparationInput,
|
||
m: Model,
|
||
source: Financial,
|
||
) {
|
||
validateNormalFormPreparation(input);
|
||
const pending: string[] = [],
|
||
edition =
|
||
input.edition === 'accepted_pdf_and_m20a1' &&
|
||
!!input.reference.trim();
|
||
if (!edition)
|
||
pending.push(
|
||
'Identifique os exemplares M22 PDF e M20A.1 inventariados;sem equivalência automática com as outras edições.',
|
||
);
|
||
if (!input.completenessReference.trim())
|
||
pending.push(
|
||
'Suporte da completude contabilística e discriminação da origem em falta.',
|
||
);
|
||
const allocations = input.allocations.map((a) => {
|
||
let value: FiscalFraction | null = null;
|
||
const issues: string[] = [];
|
||
try {
|
||
value = money(a.amount);
|
||
} catch (e) {
|
||
issues.push(String(e));
|
||
}
|
||
if (
|
||
!normalFormAccountsFor(a.sourceCode).includes(a.account) ||
|
||
!source?.accountingLines.some((l) => l.code === a.sourceCode) ||
|
||
!a.reference.trim()
|
||
)
|
||
issues.push('Origem/conta e suporte compatíveis por identificar.');
|
||
return {
|
||
...structuredClone(a),
|
||
value: issues.length ? null : value,
|
||
reason: issues.join(' '),
|
||
};
|
||
});
|
||
const lines = (source?.accountingLines ?? []).map((l) => {
|
||
const rows = allocations.filter((a) => a.sourceCode === l.code),
|
||
sum = addFractions(rows.flatMap((a) => (a.value ? [a.value] : []))),
|
||
ready =
|
||
rows.length > 0 &&
|
||
rows.every((a) => a.value !== null) &&
|
||
eq(sum, money(l.total));
|
||
const reason = ready
|
||
? 'Parcelas reconciliadas exactamente com a linha de origem.'
|
||
: 'Discriminação em falta,incompleta ou superior à origem;sem duplicar montantes.';
|
||
if (!ready) pending.push(l.origin + ': ' + reason);
|
||
return { code: l.code, total: l.total, allocated: sum, ready, reason };
|
||
});
|
||
for (const a of allocations)
|
||
if (a.reason) pending.push(a.id + ': ' + a.reason);
|
||
if (!source)
|
||
pending.push(
|
||
'Resultado financeiro do período/moeda em falta;preparação não cria um resultado substituto.',
|
||
);
|
||
const tax = input.incomeTax;
|
||
let bookTax: FiscalFraction | null = null;
|
||
if (!tax.reference.trim() || !tax.state)
|
||
pending.push(
|
||
'Imposto sobre rendimento contabilizado:valor/ausência e suporte por identificar;colecta não substitui gasto contabilístico.',
|
||
);
|
||
else if (tax.state === 'absent') {
|
||
if (tax.amount.trim() || tax.placement)
|
||
pending.push(
|
||
'Ausência de imposto contabilizado contradiz montante/localização preenchidos.',
|
||
);
|
||
else bookTax = zero();
|
||
} else {
|
||
try {
|
||
bookTax = money(tax.amount);
|
||
if (!tax.placement) throw Error();
|
||
} catch {
|
||
bookTax = null;
|
||
pending.push(
|
||
'Imposto contabilizado conhecido exige montante e indicação se já incluído ou exterior ao resultado do plano.',
|
||
);
|
||
}
|
||
}
|
||
const allocatedTax = addFractions(
|
||
allocations
|
||
.filter((a) => a.account === '8.5' && a.value)
|
||
.map((a) => a.value!),
|
||
);
|
||
if (
|
||
bookTax &&
|
||
!eq(allocatedTax, tax.placement === 'included' ? bookTax : zero())
|
||
) {
|
||
pending.push(
|
||
'Conta8.5 não reconciliada com imposto já incluído/exterior ao plano;sem subtracção duplicada.',
|
||
);
|
||
bookTax = null;
|
||
}
|
||
const includedTaxEvidence = (m.schedules?.effects ?? []).filter(
|
||
(e) =>
|
||
m.schedules!.input.rows.find((r) => r.id === e.id)?.rule ===
|
||
'tax_own',
|
||
);
|
||
if (
|
||
bookTax &&
|
||
tax.placement !== 'included' &&
|
||
includedTaxEvidence.some(
|
||
(e) => e.effect && BigInt(e.effect.numerator) > 0n,
|
||
)
|
||
) {
|
||
pending.push(
|
||
tax.state === 'absent'
|
||
? 'Ausência de imposto contabilizado contradiz imposto próprio já identificado nos custos do plano.'
|
||
: 'Imposto incluído e exterior coexistem:discriminação das duas componentes ainda por corresponder;sem misturar ou subtrair duas vezes.',
|
||
);
|
||
bookTax = null;
|
||
}
|
||
const classificationReady =
|
||
edition &&
|
||
!!input.completenessReference.trim() &&
|
||
source !== null &&
|
||
lines.length > 0 &&
|
||
lines.every((l) => l.ready) &&
|
||
allocations.every((a) => a.value !== null),
|
||
outsideTax = bookTax && tax.placement === 'outside' ? bookTax : zero();
|
||
const m20 = Object.entries(m20Accounts).map(([account, label]) => {
|
||
const related = allocations.filter((a) => a.account === account),
|
||
categorySources = Object.keys(allowed).filter((k) =>
|
||
allowed[k].includes(account),
|
||
);
|
||
const complete =
|
||
edition &&
|
||
!!input.completenessReference.trim() &&
|
||
source !== null &&
|
||
categorySources.every((k) =>
|
||
lines.some((l) => l.code === k && l.ready),
|
||
);
|
||
let value = complete
|
||
? addFractions(related.flatMap((a) => (a.value ? [a.value] : [])))
|
||
: null;
|
||
if (account === '8.5') value = complete && bookTax ? bookTax : null;
|
||
return {
|
||
account,
|
||
label,
|
||
value,
|
||
representation: normalFormRepresentation(value, 'whole'),
|
||
reference: related.map((a) => a.reference).join(' | '),
|
||
reason: complete
|
||
? 'Classificação documentada e reconciliada no M20A.1/quadro4.'
|
||
: 'Campo dependente da discriminação contabilística;sem zero por falta de parcelas.',
|
||
};
|
||
});
|
||
const netResult =
|
||
classificationReady && bookTax && m.accountingProfit
|
||
? subtractFractions(m.accountingProfit, outsideTax)
|
||
: null;
|
||
const entries: Array<{
|
||
code: string | null;
|
||
origin: string;
|
||
value: FiscalFraction | null;
|
||
reason: string;
|
||
}> = [];
|
||
const add = (
|
||
code: string | null,
|
||
origin: string,
|
||
value: FiscalFraction | null,
|
||
reason = '',
|
||
) => entries.push({ code, origin, value, reason });
|
||
for (const r of m.rows)
|
||
add(
|
||
r.rule === 'noncontractual_fine' ||
|
||
r.rule === 'compensatory_interest'
|
||
? '215'
|
||
: null,
|
||
r.document + '/' + r.line,
|
||
r.adjustment,
|
||
'CIRPC36(1)c;PDF M22/quadro10 campo215.',
|
||
);
|
||
for (const e of m.schedules?.effects ?? []) {
|
||
const r = m.schedules!.input.rows.find((r) => r.id === e.id)!;
|
||
if (
|
||
['capital_in', 'capital_out'].includes(r.rule) &&
|
||
e.effect &&
|
||
eq(e.effect, zero())
|
||
)
|
||
continue;
|
||
const code =
|
||
(
|
||
{
|
||
patrimonial_gain: '201',
|
||
patrimonial_cost: '202',
|
||
tax_other: '213',
|
||
undocumented: '223',
|
||
} as Record<string, string>
|
||
)[r.rule] ?? null;
|
||
add(
|
||
code,
|
||
r.document + '/' + r.line,
|
||
e.effect,
|
||
code
|
||
? 'Correspondência do rótulo do PDF M22.'
|
||
: r.rule === 'tax_own'
|
||
? 'Imposto próprio36(1)a:campo específico por corresponder;campo213 do PDF refere apenas responsabilidade de outrem.'
|
||
: 'Periodização/variação sem campo correspondido neste recorte;efeito preservado no cálculo.',
|
||
);
|
||
}
|
||
if (m.depreciation)
|
||
for (const asset of m.depreciation.assets) {
|
||
const a = asset.adjustment;
|
||
add(
|
||
a && positive(a) ? '206' : null,
|
||
'Mapa de amortizações — ' + asset.description,
|
||
a,
|
||
a && positive(a)
|
||
? 'Reintegrações/amortizações não aceites:PDF campo206.'
|
||
: 'Recuperação de quota anterior/ajustamento negativo:campo de dedução específico ainda por corresponder.',
|
||
);
|
||
}
|
||
else if (m.schedules?.depreciation) {
|
||
const a = m.schedules.depreciation;
|
||
add(positive(a) ? '206' : null, 'Amortizações do mapa inicial', a);
|
||
}
|
||
for (const r of m.costs?.rows ?? []) {
|
||
const code =
|
||
(
|
||
{
|
||
allowance: '219',
|
||
representation: '220',
|
||
advertising: '221',
|
||
vehicle: '226',
|
||
fuel: '225',
|
||
rental: '224',
|
||
credit: '209',
|
||
stock: '209',
|
||
litigation: '209',
|
||
taxed_reversal: '239',
|
||
} as Record<string, string>
|
||
)[r.rule] ?? null;
|
||
add(
|
||
code,
|
||
r.document + '/' + r.line,
|
||
r.adjustment,
|
||
code
|
||
? 'Correspondência do tratamento demonstrado ao rótulo do PDF M22.'
|
||
: 'Tratamento demonstrado sem campo específico correspondido;não usar campo livre por presunção.',
|
||
);
|
||
}
|
||
if (bookTax && tax.placement === 'outside')
|
||
add(
|
||
null,
|
||
'Imposto contabilístico exterior ao plano',
|
||
bookTax,
|
||
'Imposto próprio36(1)a:campo específico por corresponder;213 do exemplar é de outrem. Resultado líquido conhecido,sem código presumido ou alteração do motor fiscal.',
|
||
);
|
||
const ownTaxEffects = (m.schedules?.effects ?? []).filter(
|
||
(e) =>
|
||
m.schedules!.input.rows.find((r) => r.id === e.id)?.rule ===
|
||
'tax_own',
|
||
);
|
||
const includedTaxCorresponds =
|
||
!bookTax ||
|
||
tax.placement !== 'included' ||
|
||
(ownTaxEffects.every((e) => e.effect !== null) &&
|
||
eq(
|
||
addFractions(
|
||
ownTaxEffects.flatMap((e) => (e.effect ? [e.effect] : [])),
|
||
),
|
||
bookTax,
|
||
));
|
||
if (!includedTaxCorresponds)
|
||
pending.push(
|
||
'Imposto já incluído agora discriminado não corresponde ao ajustamento tax_own existente;conferir esse caso sem alterar cálculos ou históricos.',
|
||
);
|
||
const unmapped = entries.filter(
|
||
(e) => !e.code && (!e.value || !eq(e.value, zero())),
|
||
);
|
||
for (const e of unmapped) pending.push(e.origin + ': ' + e.reason);
|
||
const field = (
|
||
code: string,
|
||
label: string,
|
||
value: FiscalFraction | null,
|
||
reason: string,
|
||
notApplicable = false,
|
||
) => ({
|
||
code,
|
||
label,
|
||
value,
|
||
application: notApplicable
|
||
? 'not_applicable'
|
||
: value
|
||
? 'known'
|
||
: 'missing',
|
||
representation: notApplicable
|
||
? {
|
||
...normalFormRepresentation(null),
|
||
display: 'Sem aplicação',
|
||
reason: 'Lucro/perda conhecido:não preencher o campo de sinal oposto.',
|
||
}
|
||
: normalFormRepresentation(value),
|
||
reason,
|
||
transcriptionReady:
|
||
edition && code !== '200' && normalFormRepresentation(value).ready,
|
||
});
|
||
const mappedValue = (code: string, negative = false) => {
|
||
if (code === '206' && m.depreciation?.adjustment === null) return null;
|
||
const es = entries.filter((e) => e.code === code);
|
||
if (
|
||
es.some((e) => e.value === null) ||
|
||
(!es.length && m.taxableProfit === null)
|
||
)
|
||
return null;
|
||
const value = addFractions(
|
||
es.flatMap((e) =>
|
||
e.value ? [negative ? neg(e.value) : e.value] : [],
|
||
),
|
||
);
|
||
return positive(value) ? value : null;
|
||
};
|
||
const fields = [
|
||
field(
|
||
'200',
|
||
'Resultado do exercício',
|
||
netResult,
|
||
'Valor contabilístico reconciliado próprio;transporte automático M20→M22 impedido só pela divergência201/Q8 versus200/Q10.',
|
||
),
|
||
];
|
||
const labels: Record<string, string> = {
|
||
'201': 'Variações patrimoniais positivas',
|
||
'202': 'Variações patrimoniais negativas',
|
||
'206': 'Amortizações não aceites',
|
||
'209': 'Provisões/imparidade além dos limites',
|
||
'213': 'Impostos e encargos da responsabilidade de outrem',
|
||
'215': 'Multas e juros compensatórios',
|
||
'219': 'Parcela não aceite de ajudas de custo',
|
||
'220': 'Parcela não aceite de representação',
|
||
'221': 'Publicidade além do limite',
|
||
'223': 'Despesas não documentadas',
|
||
'224': 'Parcela não aceite de aluguer',
|
||
'225': 'Combustíveis sem prova/consumo normal',
|
||
'226': 'Encargos não aceites com viaturas',
|
||
'239': 'Reposição/redução de provisões tributadas',
|
||
};
|
||
for (const [code, label] of Object.entries(labels))
|
||
fields.push(
|
||
field(
|
||
code,
|
||
label,
|
||
mappedValue(code, ['202', '239'].includes(code)),
|
||
'PDF M22/quadro10;valor do ajustamento demonstrado,sem reaplicar percentagens do rótulo.',
|
||
),
|
||
);
|
||
const v = (code: string) => fields.find((f) => f.code === code)!.value,
|
||
patrimonial =
|
||
netResult && v('201') && v('202')
|
||
? subtractFractions(
|
||
addFractions([netResult, v('201')!]),
|
||
v('202')!,
|
||
)
|
||
: null;
|
||
fields.push(
|
||
field(
|
||
'203',
|
||
'Resultado após variações patrimoniais',
|
||
patrimonial,
|
||
'200+201−202,sem transferência entre edições.',
|
||
),
|
||
);
|
||
const up = entries.filter(
|
||
(e) =>
|
||
!['201', '202'].includes(e.code ?? '') &&
|
||
e.value &&
|
||
positive(e.value) &&
|
||
!eq(e.value, zero()),
|
||
),
|
||
down = entries.filter(
|
||
(e) =>
|
||
!['201', '202'].includes(e.code ?? '') &&
|
||
e.value &&
|
||
!positive(e.value),
|
||
);
|
||
const sumsReady =
|
||
includedTaxCorresponds &&
|
||
m.taxableProfit !== null &&
|
||
entries.every((e) => e.value !== null) &&
|
||
unmapped.length === 0 &&
|
||
patrimonial !== null;
|
||
const additions = sumsReady
|
||
? addFractions([patrimonial!, ...up.map((e) => e.value!)])
|
||
: null,
|
||
deductions = sumsReady
|
||
? addFractions(down.map((e) => neg(e.value!)))
|
||
: null,
|
||
reconstructed =
|
||
additions && deductions
|
||
? subtractFractions(additions, deductions)
|
||
: null;
|
||
const matches =
|
||
reconstructed !== null &&
|
||
m.taxableProfit !== null &&
|
||
eq(reconstructed, m.taxableProfit);
|
||
if (reconstructed && !matches)
|
||
pending.push(
|
||
'Percurso M22 não reconcilia com o lucro tributável existente;rever localização do imposto e campos,sem alterar os cálculos demonstrados.',
|
||
);
|
||
fields.push(
|
||
field(
|
||
'238',
|
||
'Soma após acréscimos',
|
||
additions,
|
||
'Campo203+campos204–237;indeterminado se há efeito sem correspondência.',
|
||
),
|
||
field(
|
||
'258',
|
||
'Soma das deduções',
|
||
deductions,
|
||
'Campos239–257;sem presumir campo para dedução não correspondida.',
|
||
),
|
||
);
|
||
const p = m.taxableProfit,
|
||
loss = p && !positive(p) ? neg(p) : null,
|
||
profit = p && positive(p) ? p : null;
|
||
for (const [code, label, value] of [
|
||
['259', 'Prejuízo fiscal', loss],
|
||
['260', 'Lucro tributável', profit],
|
||
['269', 'Prejuízo fiscal — regime geral', loss],
|
||
['270', 'Lucro tributável — regime geral', profit],
|
||
['276', 'Prejuízos fiscais deduzidos', m.base?.deductedLosses ?? null],
|
||
[
|
||
'277',
|
||
'Benefícios fiscais a deduzir',
|
||
m.base?.benefitDeduction ?? null,
|
||
],
|
||
['278', 'Matéria colectável — regime geral', m.taxableBase],
|
||
['300', 'Imposto à taxa normal', m.colecta ?? null],
|
||
] as const)
|
||
fields.push(
|
||
field(
|
||
code,
|
||
label,
|
||
value,
|
||
'Reutiliza o cálculo demonstrado;campos independentes da divergência de transporte.',
|
||
p !== null &&
|
||
value === null &&
|
||
['259', '260', '269', '270'].includes(code),
|
||
),
|
||
);
|
||
const reducedAbsent =
|
||
input.reducedRate.state === 'absent' &&
|
||
!!input.reducedRate.reference.trim();
|
||
fields.push(
|
||
field(
|
||
'301',
|
||
'Imposto à taxa reduzida',
|
||
reducedAbsent ? zero() : null,
|
||
'Ausência declarada com suporte;caso conhecido pertence aL7.',
|
||
),
|
||
field(
|
||
'302',
|
||
'Colecta total',
|
||
reducedAbsent ? (m.colecta ?? null) : null,
|
||
'300+301 apenas com ausência de taxa reduzida demonstrada;sem saldoL8.',
|
||
),
|
||
);
|
||
if (!reducedAbsent)
|
||
pending.push(
|
||
'Parcela de taxa reduzida em falta ou conhecida:colecta total302 depende desse âmbitoL7;colecta normal300 preservada.',
|
||
);
|
||
fields.push(
|
||
field(
|
||
'309–316',
|
||
'Liquidação e saldo final',
|
||
null,
|
||
'L8:retenções,pagamentos,créditos e saldo final não apurados.',
|
||
),
|
||
);
|
||
return {
|
||
input: structuredClone(input),
|
||
allocations,
|
||
lines,
|
||
m20,
|
||
netResult,
|
||
netRepresentation: normalFormRepresentation(netResult, 'whole', true),
|
||
entries,
|
||
unmapped,
|
||
fields,
|
||
reconciliation: {
|
||
reconstructedProfit: reconstructed,
|
||
existingProfit: m.taxableProfit,
|
||
matches,
|
||
},
|
||
pending,
|
||
source: 'PDF M22 SHA2564b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88,Q10–12;M20A.1 SHA2561ceca6f4c6d77853c1b2b445876599b42093175fa89eecf40590180956d83a9e,Q4/M12/D13–D35. Rótulos e estrutura reutilizados;sem valores de terceiros,fórmulas do exemplar ou submissão.',
|
||
transport: {
|
||
ready: false,
|
||
reason: 'M20A.1 D35 remete paraM22 campo201/quadro8;PDF M22 usa200/quadro10. Impedimento restrito ao transporte deste resultado;campos independentes continuam preparados.',
|
||
},
|
||
};
|
||
}
|
||
export type NormalFormPreparationModel = ReturnType<typeof prepareNormalForms>;
|
||
export function normalFormPreparationReport(
|
||
m: NormalFormPreparationModel,
|
||
): Array<[string, string, string, string]> {
|
||
return [
|
||
[
|
||
'IRPC normal — L6',
|
||
'Representação e preparação M20/M22',
|
||
m.pending.join(' | ') ||
|
||
'Preparação parcial dos campos correspondidos;sem declaração oficial ou submissão.',
|
||
m.source,
|
||
],
|
||
...m.m20.map(
|
||
(f) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
'M20 A.1 conta' + f.account + ' — ' + f.label,
|
||
`${f.representation.display};exacto:${f.representation.exact ?? 'Indeterminado'};representável:${f.representation.ready ? 'Sim' : 'Pendente'};${f.representation.reason}`,
|
||
`${f.reference};${f.reason}`,
|
||
] as [string, string, string, string],
|
||
),
|
||
[
|
||
'IRPC normal — L6',
|
||
'Resultado líquido M20',
|
||
m.netRepresentation.display + ';' + m.netRepresentation.reason,
|
||
'Imposto contabilístico identificado;sem usar colecta como despesa presumida.',
|
||
],
|
||
...m.fields.map(
|
||
(f) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
'M22 PDF campo' + f.code + ' — ' + f.label,
|
||
`${f.representation.display};exacto:${f.representation.exact ?? 'Indeterminado'};preparação de transcrição:${f.transcriptionReady ? 'Exacta neste recorte' : 'Pendente/sem aplicação'};${f.representation.reason}`,
|
||
f.reason,
|
||
] as [string, string, string, string],
|
||
),
|
||
...m.unmapped.map(
|
||
(e) =>
|
||
[
|
||
'IRPC normal — L6',
|
||
'Ajustamento sem campo correspondido — ' + e.origin,
|
||
normalFormRepresentation(e.value).display,
|
||
e.reason,
|
||
] as [string, string, string, string],
|
||
),
|
||
[
|
||
'IRPC normal — L6',
|
||
'Transporte entre edições',
|
||
m.transport.reason,
|
||
`Lucro reconstruído:${normalFormRepresentation(m.reconciliation.reconstructedProfit).display};lucro existente:${normalFormRepresentation(m.reconciliation.existingProfit).display};reconciliação:${m.reconciliation.matches ? 'Exacta' : 'Pendente'};sem alteração de cálculos.`,
|
||
],
|
||
];
|
||
}
|