49 lines
9.7 KiB
TypeScript
49 lines
9.7 KiB
TypeScript
import {salesBook,salesCurrent} from './iva-operations.ts';
|
|
import {acquisitionBook,acquisitionCurrent} from './iva-acquisitions.ts';
|
|
import {travelBook,travelCurrent} from './iva-travel.ts';
|
|
import {travelEventBook,travelEventCurrent} from './iva-travel-events.ts';
|
|
import {secondhandBook,secondhandCurrent} from './iva-secondhand.ts';
|
|
import {currencyEventBook,currencyEventCurrent} from './iva-currency-events.ts';
|
|
export type FiscalCalculationGap={id:string;book:string;period:string;calculation:string;reason:string;required:string[];resolution:string;kind:'inputs_or_rule'|'implementation'|'stale'|'not_calculated';source:string};
|
|
type Row=Record<string,unknown>;
|
|
const fieldLabels:Record<string,string>={net:'Base líquida',vat:'IVA',gross:'Total com IVA',deductible:'IVA dedutível',baseAmount:'Base tributável',deduction:'Dedução',requested:'Pedido',approved:'Reconhecido',received:'Recebido',awaitingReceipt:'Por receber',delta:'Regularização',grossMargin:'Margem bruta',scenarioAmount:'Montante do cenário',outstanding:'Por liquidar'};
|
|
const financial=new Set(['net','vat','gross','deductible','baseAmount','deduction','requested','approved','received','awaitingReceipt','delta','grossMargin','scenarioAmount','outstanding']);
|
|
const details:Record<string,{required:string[];resolution:string}>={
|
|
ivaBeverageRestitutions:{required:['origem e parcela13(2) actuais,sem sobreposição','referência/data do pedido e da decisão externa,quando exista','montante reconhecido e comprovativos dos recebimentos efectivamente ocorridos'],resolution:'Reconciliar a parcela ou completar apenas o acto externo em falta. Sem decisão/recebimento documentado,conservar esses estados pendentes;sem concessão ou caixa presumidas.'},
|
|
ivaRefunds:{required:['afectação25/26/27 actual','fundamento e origem do crédito','pedido,decisão e comprovativos de recebimento próprios'],resolution:'Resolver a condição concreta do pedido ou registar o acto externo documentado;pedido não equivale a decisão ou recebimento.'},
|
|
ivaSubstitutions:{required:['declaração anterior congelada e fonte actual','referência,motivo e data da substituição','juros e pagamentos adicionais próprios'],resolution:'Reconciliar anterior e actual,sem repetir créditos/pagamentos;guardar novo diferencial. A correspondência ModeloB/edição recebida permanece separada.'},
|
|
|
|
salesOperations:{required:['natureza e localização da operação','preço e indicação de IVA incluído/sem IVA','tratamento, condições e suporte próprios do fornecimento'],resolution:'Resolver o motivo da linha e calcular a operação de venda. O IVA documental da aquisição não é origem do IVA do fornecimento.'},
|
|
books:{required:['IVA documental e documento legal','direito e afectação da aquisição','cobertura das parcelas do período e vendas/regularizações aplicáveis'],resolution:'Resolver as linhas/campos indicados e guardar novo apuramento; preservar versões e valores indeterminados até completar as parcelas.'},
|
|
regulatedSecondhands:{required:['categoria, origem e regime do bem','venda, compra ou adjudicação e comissão documentadas','base da margem e regra de liquidação aplicável'],resolution:'A base/margem já demonstrada conserva-se. Implementar separadamente a liquidação e a integração nos totais com a regra e o âmbito próprios; base calculada não significa IVA calculado.'},
|
|
travelOperations:{required:['operador, período e âmbito do registo global','contraprestação da parte especial e custos directos elegíveis de terceiros','excesso anterior, completude e enquadramento tributável'],resolution:'Completar o registo global e a base; implementar IVA devido, dedução43(d) e integração do período com correspondência própria. A taxa do divisor da base não preenche o IVA devido.'},
|
|
travelSpecialEvents:{required:['origem da operação e natureza do evento','data e referência do pagamento integral/início ou cessação do regime','base, taxa e direito à dedução no cálculo próprio da operação'],resolution:'Resolver o momento do evento e ligá-lo ao apuramento próprio. Momento determinado ou diferimento não calcula base, IVA ou dedução.'},
|
|
currencyEvents:{required:['natureza e âmbito do evento','datas de factura, prazo legal e recebimentos aplicáveis','moeda e câmbio documentado quando aplicável','base e regra fiscal da operação'],resolution:'Resolver os factos/câmbio indicados e implementar ou reutilizar o cálculo próprio da operação; o evento isolado não liquida imposto.'},
|
|
};
|
|
function record(value:unknown):value is Row{return!!value&&typeof value==='object'&&!Array.isArray(value);}
|
|
/** Reads the applicable scope only; never recalculates or promotes a frozen result. */
|
|
export function fiscalCalculationGaps(d:Record<string,unknown>):FiscalCalculationGap[]{
|
|
const store=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(store.version!==1||!record(store.books))throw Error('Livros não recuperáveis; conservar o original antes de diagnosticar.');
|
|
const scope=String(d.context_opportunity||'general'),out:FiscalCalculationGap[]=[];
|
|
const current:Record<string,()=>boolean>={salesOperations:()=>salesCurrent(d,salesBook(d)),books:()=>acquisitionCurrent(d,acquisitionBook(d)),travelOperations:()=>travelCurrent(d,travelBook(d)),travelSpecialEvents:()=>travelEventCurrent(d,travelEventBook(d)),regulatedSecondhands:()=>secondhandCurrent(d,secondhandBook(d)),currencyEvents:()=>currencyEventCurrent(d,currencyEventBook(d))};
|
|
for(const[name,map]of Object.entries(store)){
|
|
if(!record(map)||!(scope in map))continue;if(!record(map[scope]))throw Error(`Livro ${name} não recuperável;original preservado.`);const book=map[scope] as Row;if(book.result!==undefined&&!record(book.result))throw Error(`Resultado ${name} não recuperável;original preservado.`);const result=record(book.result)?book.result:null,inputs=(Array.isArray(book.rows)?book.rows:Array.isArray(book.events)?book.events:[]) as Row[],spec=details[name];
|
|
if(!result){for(const row of inputs)out.push({id:`${name}:${row.id??out.length}:uncalculated`,book:name,period:String(row.period??''),calculation:String(row.description??row.id??name),reason:'Preparação sem resultado guardado; nenhum valor fiscal foi presumido.',required:spec?.required??['dados e condições específicos da preparação existente'],resolution:spec?.resolution??'Completar a preparação no componente existente e guardar o cálculo correspondente.',kind:'not_calculated',source:''});continue;}
|
|
let fresh=true;try{fresh=current[name]?current[name]():!(Array.isArray(result.inputs)&&JSON.stringify(result.inputs)!==JSON.stringify(inputs));}catch{fresh=false;}
|
|
if(!fresh)out.push({id:`${name}:stale`,book:name,period:'',calculation:'Actualidade do resultado',reason:'Entradas ou origem alteradas; resultado histórico conservado.',required:['reconciliação das entradas/origens alteradas'],resolution:'Rever apenas o delta e guardar nova versão antes de usar os valores como actuais.',kind:'stale',source:String(result.source??result.sources??'')});
|
|
const visit=(value:unknown,path:string)=>{
|
|
if(Array.isArray(value)){value.forEach((v,i)=>visit(v,`${path}[${i}]`));return;}if(!record(value)||value.status==='not_applicable')return;
|
|
const missing=Object.entries(value).filter(([k,v])=>v===null&&financial.has(k)).map(([k])=>k);
|
|
if(record(value.fields))for(const[k,v]of Object.entries(value.fields))if(v===null)missing.push(`campo${k}`);
|
|
if(value.status==='indeterminate'||missing.length){
|
|
const frozen=Array.isArray(result.inputs)?result.inputs as Row[]:inputs,input=frozen.find(i=>i.id===value.id),origin=record(input?.source)?input.source:null,prior=record(input?.prior)?input.prior:null,supply=name==='salesOperations'&&String(input?.treatment??'').startsWith('onboard_');
|
|
const implementation=missing.length>0&&value.status!=='indeterminate'&&['regulatedSecondhands','travelOperations','travelSpecialEvents','currencyEvents'].includes(name);
|
|
out.push({id:`${name}:${path}`,book:name,period:String(value.period??input?.period??origin?.period??prior?.period??''),calculation:`${String(input?.description??origin?.documentRef??value.id??(value.period?'Apuramento do período':'Apuramento'))}${missing.length?` — ${missing.map(f=>fieldLabels[f]??f.replace('campo','Campo ')).join(', ')}`:''}`,reason:String(value.reason??(Array.isArray(value.reasons)?value.reasons.join('; '):'Parcelas incompletas; consultar o apuramento existente.')),required:supply?['natureza do abastecimento e meio elegível','tratamento e montante liquidado do fornecimento, separados da aquisição','procedimento próprio de13(2), quando aplicável']:spec?.required??['dados, documentos e parâmetros indicados no motivo desta linha'],resolution:supply?'Identificar e implementar o tratamento do fornecimento e respectivo montante liquidado; conservar o IVA da aquisição separado. O acompanhamento E não resolve este cálculo.':spec?.resolution??'Resolver o motivo concreto no percurso de origem e guardar nova versão; campos em falta continuam indeterminados.',kind:implementation||supply?'implementation':'inputs_or_rule',source:String(value.source??result.source??result.sources??'')});
|
|
}
|
|
for(const[k,v]of Object.entries(value))if(['rows','periods','events'].includes(k))visit(v,`${path}.${k}`);
|
|
};visit(result,'result');
|
|
}
|
|
return out;
|
|
}
|
|
export function fiscalCalculationGapReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{return fiscalCalculationGaps(d).map(g=>['IVA mensal',`Cálculo pendente — ${g.calculation}`,`${g.period};${g.kind};${g.reason};Dados necessários:${g.required.join('; ')};Condição:${g.resolution}`,`${g.book};${g.id};${g.source};sem inferir IVA do fornecimento a partir da aquisição.`]);}catch(error){return[['IVA mensal','Diagnóstico dos cálculos','Livros não recuperáveis; originais preservados',String(error)]];}}
|