Files
Investidor-V2/resources/js/lib/irpc-normal-schedules.ts
T
2026-10-26 09:24:06 +02:00

73 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {addFractions,decimalFraction,fraction,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
export const normalSchedulesVersion='IRPC-normal-adjustments/5';
export const scheduleRules={
tax_own:['IRPC e impostos sobre o lucro contabilizados','expenseCorrections','36(1)a',1],
tax_other:['Impostos da responsabilidade de outrem','expenseCorrections','36(1)b',1],
undocumented:['Despesa não documentada','expenseCorrections','36(1)g',1],
capital_in:['Entrada de capital fora do resultado','positivePatrimonial','21(1)a',0],
patrimonial_gain:['Variação positiva ordinária fora do resultado','positivePatrimonial','21',1],
capital_out:['Distribuição aos titulares fora do resultado','negativePatrimonial','24(1)c',0],
patrimonial_cost:['Variação negativa indispensável, sem exclusões do artigo24','negativePatrimonial','22/24(1)',-1],
future_cost:['Custo contabilizado em2026 imputável ao exercício seguinte','timing','18(1)',1],
future_income:['Proveito contabilizado em2026 imputável ao exercício seguinte','timing','18(1)',-1],
current_cost:['Custo de2026 ainda não reflectido no resultado','timing','18(1)',-1],
current_income:['Proveito de2026 ainda não reflectido no resultado','timing','18(1)',1],
} as const;
export type ScheduleRule=keyof typeof scheduleRules;
export type ScheduleRow={id:string;rule:''|ScheduleRule;amount:string;sourceCode:string;document:string;line:string;reference:string;fiscalYear:string;conditions:''|'yes'};
export type NormalAsset={id:string;category:''|'computer'|'server'|'other';description:string;cost:string;residual:string;rate:string;accounting:string;previousFiscal:string;inUseFrom:string;tableReference:string;reference:string;conditions:''|'yes'};
export type NormalSchedules={version:1;period:{kind:''|'calendar'|'authorised_annual'|'ep_annual';from:string;to:string;reference:string};rows:ScheduleRow[];interest:{amount:string;reference:string;conditions:''|'yes'};assets:NormalAsset[];assetsReference:string;currentLossReference:string};
export const newNormalSchedules=():NormalSchedules=>({version:1,period:{kind:'',from:'',to:'',reference:''},rows:[],interest:{amount:'',reference:'',conditions:''},assets:[],assetsReference:'',currentLossReference:''});
export const newScheduleRow=(id:string):ScheduleRow=>({id,rule:'',amount:'',sourceCode:'',document:'',line:'',reference:'',fiscalYear:'',conditions:''});
export const newNormalAsset=(id:string):NormalAsset=>({id,category:'',description:'',cost:'',residual:'',rate:'',accounting:'',previousFiscal:'',inUseFrom:'',tableReference:'',reference:'',conditions:''});
export function validateSchedules(s:NormalSchedules){
if(!s||s.version!==1||!Array.isArray(s.rows)||!Array.isArray(s.assets)||!s.interest||typeof s.assetsReference!=='string'||typeof s.currentLossReference!=='string'||['amount','reference','conditions'].some(k=>typeof s.interest[k as 'amount']!=='string')||!['','yes'].includes(s.interest.conditions))throw Error('Mapas L6 não recuperáveis.');
for(const [rows,empty]of [[s.rows,newScheduleRow('')],[s.assets,newNormalAsset('')]]as const){if(new Set(rows.map(r=>r.id)).size!==rows.length)throw Error('Identificador duplicado no mapa L6.');for(const r of rows)if(!r||Object.keys(empty).some(k=>typeof (r as unknown as Record<string,unknown>)[k]!=='string')||!['','yes'].includes(r.conditions))throw Error('Linha L6 não recuperável.');}
if(s.rows.some(r=>r.rule!==''&&!Object.hasOwn(scheduleRules,r.rule)))throw Error('Regra L6 desconhecida.');
if(s.assets.some(a=>!['','computer','server','other'].includes(a.category)))throw Error('Categoria de activo desconhecida.');
if(!s.period||!['','calendar','authorised_annual','ep_annual'].includes(s.period.kind)||['from','to','reference'].some(k=>typeof s.period[k as 'from']!=='string'))throw Error('Período complementar não recuperável.');
}
export function normalSchedulePeriod(s:NormalSchedules,scope:string){
const p=s.period,pending:string[]=[];
const iso=(v:string)=>/^20\d{2}-\d{2}-\d{2}$/.test(v)&&Number.isFinite(Date.parse(v))&&new Date(v).toISOString().slice(0,10)===v;
if(!iso(p.from)||!iso(p.to)||!p.from.endsWith('-01')||!p.to.startsWith('2026-'))pending.push('Período anual por identificar: início no primeiro dia do mês e termo em2026, sem período curto ou alinhamento mensal presumido.');
else{const end=new Date(p.from+'T00:00:00Z');end.setUTCFullYear(end.getUTCFullYear()+1);end.setUTCDate(end.getUTCDate()-1);if(end.toISOString().slice(0,10)!==p.to)pending.push('Os12 meses do plano não correspondem ao período anual indicado.');}
if(!p.kind||!p.reference.trim())pending.push('Identifique tipo de período e correspondência dos12 meses; ausência de informação não é ano civil presumido.');
if(p.kind==='calendar'&&(p.from!=='2026-01-01'||p.to!=='2026-12-31'))pending.push('Datas incompatíveis com o ano civil2026.');
if(p.kind==='ep_annual'&&scope!=='ep_general')pending.push('Período do artigo7(3) exige EP e comunicação documentada; não aplicar a um residente.');
return{...structuredClone(p),pending};
}
const z=()=>fraction(0n);
export const fiscalMoney=(s:string)=>{if(!/^\d{1,18}(?:\.\d{1,2})?$/.test(s))throw Error('Montante não negativo com até dois decimais necessário.');return decimalFraction(s);};
const cmp=(a:FiscalFraction,b:FiscalFraction)=>BigInt(a.numerator)*BigInt(b.denominator)-BigInt(b.numerator)*BigInt(a.denominator);
export type SchedulesModel=ReturnType<typeof calculateSchedules>;
export function calculateSchedules(s:NormalSchedules,source:{accountingLines:Array<{code:string;total:string;accountingEffect:string}>}|null,oldRows:Array<{sourceCode:string;document:string;line:string;amount:string;adjustment:FiscalFraction|null}>,depreciationOverride?:{value:FiscalFraction|null;pending:string[]}){
validateSchedules(s);const pending:string[]=[],effects:Array<{id:string;topic:string;amount:FiscalFraction|null;effect:FiscalFraction|null;reason:string}>=[];
for(const r of s.rows){const issues:string[]=[],rule=r.rule?scheduleRules[r.rule]:null;let amount:FiscalFraction|null=null,effect:FiscalFraction|null=null;
if(!rule||!r.reference.trim()||!r.document.trim()||!r.line.trim()||r.conditions!=='yes')issues.push('Regra, documento/linha, suporte e condições por identificar.');
if(s.rows.some(q=>q!==r&&q.document.trim()===r.document.trim()&&q.line.trim()===r.line.trim())||oldRows.some(q=>q.document.trim()===r.document.trim()&&q.line.trim()===r.line.trim()))issues.push('Documento/linha duplicados entre mapas; sem dupla contagem.');
try{amount=fiscalMoney(r.amount);if(cmp(amount,z())<=0n)throw Error();}catch{issues.push('Parcela positiva por identificar.');}
const topic=rule?.[1]??'';const needsOrigin=topic==='expenseCorrections'||['future_cost','future_income'].includes(r.rule);
if(needsOrigin){const line=source?.accountingLines.find(l=>l.code===r.sourceCode);const wanted=r.rule==='future_income'?'income':'expense';if(!line||line.accountingEffect!==wanted)issues.push('Parcela deve estar incluída numa linha de origem compatível.');else try{const allocated=addFractions([...s.rows.filter(q=>(q.rule&&(['expenseCorrections'].includes(scheduleRules[q.rule][1])||['future_cost','future_income'].includes(q.rule)))&&q.sourceCode===r.sourceCode).map(q=>fiscalMoney(q.amount)),...oldRows.filter(q=>q.sourceCode===r.sourceCode).map(q=>fiscalMoney(q.amount))]);if(cmp(allocated,fiscalMoney(line.total))>0n)issues.push('Parcelas excedem a origem contabilística.');}catch{issues.push('Reconciliação da origem incompleta.');}}
else if(r.sourceCode)issues.push('Variação fora do resultado/custo ou proveito omitido não deve repetir linha já contabilizada.');
if(topic==='timing'&&r.fiscalYear!==(r.rule.startsWith('future_')?'2027':'2026'))issues.push('Exercício de imputação incompatível; este recorte cobre2026/seguinte, sem resolver exercícios anteriores.');
if(!issues.length&&amount&&rule)effect=fraction(BigInt(amount.numerator)*BigInt(rule[3]),BigInt(amount.denominator));
const reason=issues.join(' ')||'CIRPC'+rule?.[2]+'; classificação e condições documentadas pelo utilizador; projecção, sem alteração da contabilidade de origem.';if(issues.length)pending.push(r.document+': '+reason);effects.push({id:r.id,topic,amount,effect,reason});
}
let interest:FiscalFraction|null=null;const interestOrigin=source?.accountingLines.find(l=>l.code==='interest_expense');
try{const corrected=s.rows.filter(r=>r.sourceCode==='interest_expense'&&r.rule&&(scheduleRules[r.rule][1]==='expenseCorrections'||r.rule==='future_cost'));const corrections=corrected.map(r=>{const e=effects.find(e=>e.id===r.id);if(!e?.effect)throw Error();return e.effect;});const remaining=subtractFractions(fiscalMoney(interestOrigin?.total??''),addFractions([...oldRows.filter(r=>r.sourceCode==='interest_expense').flatMap(r=>r.adjustment?[r.adjustment]:[]),...corrections]));interest=fiscalMoney(s.interest.amount);if(cmp(interest,remaining)!==0n||!s.interest.reference.trim()||s.interest.conditions!=='yes')throw Error();}catch{interest=null;}
const assets=(depreciationOverride?[]:s.assets).map(a=>{const issues:string[]=[];let accounting:FiscalFraction|null=null,fiscal:FiscalFraction|null=null,adjustment:FiscalFraction|null=null;
try{const cost=fiscalMoney(a.cost),residual=fiscalMoney(a.residual),previous=fiscalMoney(a.previousFiscal),rate=fiscalMoney(a.rate);accounting=fiscalMoney(a.accounting);const depreciable=subtractFractions(cost,residual);if(cmp(depreciable,z())<0n||cmp(previous,depreciable)>0n||cmp(rate,z())<=0n||cmp(rate,fraction(100n))>0n)throw Error();const annual=fraction(BigInt(depreciable.numerator)*BigInt(rate.numerator),BigInt(depreciable.denominator)*BigInt(rate.denominator)*100n);const available=subtractFractions(depreciable,previous);fiscal=cmp(annual,available)>0n?available:annual;
if(cmp(fiscal,accounting)>0n)issues.push('Quota fiscal superior à contabilizada neste exercício; dedução de quotas anteriormente tributadas exige mapa próprio, sem presunção.');
}catch{issues.push('Custo, residual, taxa, acumulado fiscal e depreciação contabilizada por reconciliar.');}
if(!['computer','server'].includes(a.category)||a.rate!==(a.category==='computer'?'25':'20'))issues.push('Categoria/taxa não correspondida: este recorte cobre tabelaII, grupo3, itens9 (25%) e9.1 (20%), sem regra específica da tabelaI prevalecente.');
if(!/^20\d{2}-\d{2}-\d{2}$/.test(a.inUseFrom)||!Number.isFinite(Date.parse(a.inUseFrom))||new Date(a.inUseFrom).toISOString().slice(0,10)!==a.inUseFrom||a.inUseFrom>(s.period.from||'2026-01-01')||a.inUseFrom<'2014-01-01'||!a.description.trim()||!a.tableReference.trim()||!a.reference.trim()||a.conditions!=='yes')issues.push('Identifique activo próprio, em uso no início do período, tabela/taxa aplicável, vida útil e condições do método constante anual; casos especiais/duodécimos separados.');
if(['computer','server'].includes(a.category)&&/^20\d{2}-\d{2}-\d{2}$/.test(a.inUseFrom)){const limit=new Date(a.inUseFrom+'T00:00:00Z');if(Number.isFinite(+limit)){limit.setUTCFullYear(limit.getUTCFullYear()+(a.category==='computer'?8:10));if((s.period.from||'2026-01-01')>=limit.toISOString().slice(0,10))issues.push('Período máximo de vida útil excedido, artigo5; excepção específica não implementada.');}}
if(!issues.length&&accounting&&fiscal)adjustment=subtractFractions(accounting,fiscal);if(issues.length)pending.push(a.description+': '+issues.join(' '));return{...structuredClone(a),accountingValue:accounting,fiscal,adjustment,reason:issues.join(' ')||'Decreto72/2013 art3–7; quota constante anual documentada, limitada ao saldo fiscal depreciável; diferença para a depreciação contabilística.'};});
let depreciation:FiscalFraction|null=depreciationOverride?.value??null;if(!depreciationOverride)try{const total=addFractions(assets.map(a=>a.accountingValue??z()));if(s.assetsReference.trim()&&assets.every(a=>a.adjustment!==null)&&cmp(total,fiscalMoney(source?.accountingLines.find(l=>l.code==='depreciation')?.total??''))===0n)depreciation=addFractions(assets.map(a=>a.adjustment!));}catch{/* linha de origem desconhecida */}
if(assets.length&&(oldRows.some(r=>r.sourceCode==='depreciation')||s.rows.some(r=>r.sourceCode==='depreciation'))){depreciation=null;pending.push('Depreciação repartida entre mapas de ajustamentos e activos: reconcilie as parcelas sem sobreposição antes de aplicar a quota fiscal.');}
if(assets.length&&depreciation===null)pending.push('Mapa de activos sem reconciliação integral com a depreciação da origem; efeito fiscal não aplicado.');
if(depreciationOverride)pending.push(...depreciationOverride.pending);
return{input:structuredClone(s),effects,interest,depreciation,assets,pending,net: addFractions([...effects.flatMap(r=>r.effect?[r.effect]:[]),...(depreciation?[depreciation]:[])]),source:'CIRPC18/21/22/24/36; Decreto72/2013 art3–7. Entradas documentadas, sem taxas de activos sugeridas nem casos especiais presumidos.'};
}