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 = { 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; 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 )[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 )[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 = { '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; 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.`, ], ]; }