Prepare grouped L2 A B C with customs deduction exclusions and exact affected totals

This commit is contained in:
2026-10-25 12:29:10 +02:00
parent 56c948ec45
commit fc7e590d4e
14 changed files with 511 additions and 22 deletions
@@ -0,0 +1,29 @@
import type {Acquisition} from '@/lib/iva-acquisitions';
import {acquisitionImportSources,expenseOptions,newAcquisitionBC,reuseAcquisitionImport,type AcquisitionBC} from '@/lib/iva-acquisition-bc';
export function AcquisitionImportsExclusions({draft,row,onChange}:{draft:Record<string,unknown>;row:Acquisition;onChange:(patch:Partial<Acquisition>)=>void}){
const b=row.bc;const update=(patch:Partial<AcquisitionBC>)=>onChange({bc:{...(b??newAcquisitionBC()),...patch}});
const choice=(key:'reducedSupply'|'belongs'|'ownImportVat',label:string)=><label className="block">{label}<select value={b?.[key]??''} onChange={e=>update({[key]:e.target.value})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>;
const fact=(key:string,label:string)=><label className="block" key={key}>{label}<select value={b?.facts[key]??''} onChange={e=>update({facts:{...b?.facts,[key]:e.target.value as ''|'yes'|'no'}})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>;
if(!b)return <div><button type="button" onClick={()=>onChange({bc:newAcquisitionBC()})}>Analisar exclusões e excepções desta aquisição — C</button><button type="button" onClick={()=>onChange({bc:newAcquisitionBC(),field:'11',domestic:'no',documentType:'customs'})}>Preparar importação — B/campo11 (conserva valores e documento)</button></div>;
const imported=row.domestic==='no';const sources=acquisitionImportSources(draft);
return <fieldset className="space-y-2 rounded border p-3"><legend>Importações e exclusões — B/C</legend>
<p>Dados existentes conservados;as respostas são factos declarados ou pressupostos de projecção. Informação em falta fica indeterminada. Alterar a natureza da despesa conserva respostas anteriores sem as aplicar a outro tratamento.</p>
{imported&&<><p>Campo11:IVA devido na importação de bens pelo documento aduaneiro. Não calcula direitosL13,não copia o valor aduaneiro como IVA e não exige fornecedor estrangeiro inscrito no IVA interno.</p>
<label className="block">Reutilizar registo de importação existente<select value="" onChange={e=>{const source=sources.find(s=>s.map+'/'+s.row.id===e.target.value);if(source)onChange(reuseAcquisitionImport(draft,row,source.map,source.row.id));}}><option value="">Escolher correspondência;apenas preenche dados vazios</option>{sources.map(s=><option key={s.map+'/'+s.row.id} value={s.map+'/'+s.row.id}>{s.label}</option>)}</select></label>
{b.source&&<p>Origem conservada:{b.source.map}/{b.source.id}. Reutilizar novamente adopta a origem actual,sem substituir edições.</p>}
{(['importer','goodsRef','vatRef'] as const).map((key,i)=><label className="block" key={key}>{['Importador identificado','Bens/operação/linha abrangidos','Fonte do montante de IVA devido — bilhete/pressuposto'][i]}<input value={b[key]} onChange={e=>update({[key]:e.target.value})}/></label>)}
{choice('belongs','Bilhete na titularidade do sujeito passivo/importador desta aquisição')}
{fact('exigibleInPeriod','IVA aduaneiro tornou-se exigível no período de dedução indicado,conforme a fonte/documento21(1);não basta a data de emissão do bilhete')}
{choice('ownImportVat','Montante é apenas IVA próprio devido nesta importação,sem somar novamente IVA de serviço incluído na base dos bens')}
<label className="block">Situação do IVA da importação<select value={b.vatBasis} onChange={e=>update({vatBasis:e.target.value as AcquisitionBC['vatBasis']})}><option value="">Por identificar</option><option value="due">IVA devido identificado no documento/pressuposto</option><option value="exempt">Importação isenta — IVAzero expressamente indicado</option><option value="zero">Montantezero expresso,com fundamento</option><option value="unknown">Montante desconhecido</option></select></label>
<p>IVA isento/zero não é montante desconhecido. Para ausência de imposto a deduzir,indique zero expressamente no IVA;isençãoL1 não preenche esse montante.</p></>}
{!imported&&choice('reducedSupply','O IVA suportado respeita a fornecimento abrangido pelo artigo17-A (20(2))')}
<label className="block">Natureza da despesa para dedução<select value={b.expense} onChange={e=>update({expense:e.target.value})}><option value="">Por identificar</option>{expenseOptions.map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label>
{b.expense==='vehicle'&&<>{fact('tourism','Bem abrangido20(1)(a):viatura de turismo,barco de recreio,helicóptero,avião,moto/motociclo. Viatura:inclui reboque;não destinada unicamente a mercadorias/uso agrícola,comercial ou industrial,ou mista/passageiros até9lugares incluindo condutor')}{b.facts.tourism==='yes'&&fact('saleOrExploit','Venda ou exploração deste bem constitui objecto da actividade20(3)(a)')}</>}
{b.expense==='fuel'&&<>{fact('resale','Combustível adquirido para revenda,com actividade e destino identificados20(3)(a)')}{b.facts.resale==='no'&&<><label>Combustível<select value={b.fuelKind} onChange={e=>update({fuelKind:e.target.value as AcquisitionBC['fuelKind']})}><option value="">Por identificar</option><option value="diesel">Gasóleo</option><option value="other">Outro combustível normalmente utilizável em viaturas</option></select></label>{b.fuelKind==='diesel'&&<><label>Utilização do gasóleo<select value={b.dieselUse} onChange={e=>update({dieselUse:e.target.value as AcquisitionBC['dieselUse']})}><option value="">Por identificar</option><option value="heavy_passengers">Veículo pesado de passageiros</option><option value="public_passengers">Veículo licenciado para transporte público de passageiros,excepto rent a car</option><option value="machine">Máquina consumidora de gasóleo que não é veículo matriculado</option><option value="agriculture">Tractor com emprego exclusivo/predominante em operações inerentes à actividade agrícola</option><option value="other">Outro uso — dedução50%</option></select></label>{b.dieselUse&&b.dieselUse!=='other'&&fact('dieselScope','O uso e as características correspondem integralmente à categoria20(1)(b)(i–iv) seleccionada')}</>}</>}</>}
{b.expense==='lodging_food'&&fact('commercialTraveller','Alojamento/alimentação de viajante comercial,agindo por conta própria,no quadro da actividade profissional20(3)(b)')}
{b.expense==='telephone'&&fact('ownLandline','Serviço de telefone fixo em nome do sujeito passivo20(1)(e)')}
{b.expense==='beverages'&&<>{fact('beverageSpecial','Bebidas de abastecimento de bordo nas alíneas13(1)(c/d/h),com origemL1 identificada;tabaco e recepção não estão abrangidos por esta excepção')}{b.facts.beverageSpecial==='yes'&&<><label>Via13(2) para esta parcela<select value={b.beveragePath} onChange={e=>update({beveragePath:e.target.value as AcquisitionBC['beveragePath']})}><option value="">Por escolher</option><option value="deduction">Dedução nesta aquisição — L2</option><option value="refund">Restituição — percursoL3,sem apuramento aqui</option></select></label>{fact('onlyDeduction','Parcela identificada exclusivamente para dedução;sem pedido/restituição da mesma parcela')}{fact('notCoastalFishing','Se origem13(1)(d):embarcação de salvamento/assistência marítima,sem ser pesca costeira cujas provisões ficam excluídas')}</>}</>}
<label className="block">Factos e referência documental da classificação/excepção<input value={b.evidence} onChange={e=>update({evidence:e.target.value})}/></label>
</fieldset>;
}
@@ -1,11 +1,11 @@
import {acquisitionOperationChoices,createAcquisitionRightLink,linkedOperationRight,type AcquisitionRightLink} from '@/lib/iva-acquisition-right';
import {acquisitionOperationChoices,createAcquisitionRightLink,linkedOperationRight,linkedBeverageDeductionRight,type AcquisitionRightLink} from '@/lib/iva-acquisition-right';
import type {Acquisition} from '@/lib/iva-acquisitions';
export function AcquisitionOperationLink({draft,row,onChange}:{draft:Record<string,unknown>;row:Acquisition;onChange:(patch:Partial<Acquisition>)=>void}){
const choices=acquisitionOperationChoices(draft),link=row.deductionLink;
const adopt=(id:string)=>onChange({deductionLink:createAcquisitionRightLink(draft,id)});
const patch=(value:Partial<AcquisitionRightLink>)=>{if(link)onChange({deductionLink:{...link,...value}});};
const result=link?linkedOperationRight(draft,link):null;
const result=link?(row.bc?.expense==='beverages'&&row.bc.facts.beverageSpecial==='yes'&&row.bc.beveragePath==='deduction'?linkedBeverageDeductionRight(draft,link,row.bc.facts.notCoastalFishing==='yes'):linkedOperationRight(draft,link)):null;
return <fieldset className="space-y-2 rounded border p-3"><legend>Aquisição → direito à dedução — L2-A</legend>
<p>Reutilize a classificação existente. A ligação conserva montantes, documento e escolhas da aquisição; não cria outra compra. Importações, despesas especiais e afectação parcial conservam enquadramento próprio.</p>
{choices.length===1&&!link?<button type="button" onClick={()=>adopt(choices[0].id)}>Reutilizar operação existente: {choices[0].label}</button>:<label>Operação de destino<select value={link?.operationId??''} onChange={e=>e.target.value?adopt(e.target.value):onChange({deductionLink:undefined})}><option value="">Sem vínculo L2-A</option>{choices.map(c=><option key={c.id} value={c.id}>{c.label}</option>)}</select></label>}
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { AcquisitionOperationLink } from './acquisition-operation-link';
import { AcquisitionImportsExclusions } from './acquisition-imports-exclusions';
import { acquisitionRightVersion } from '@/lib/iva-acquisition-right';
import { acquisitionBook, acquisitionCurrent, acquisitionExcludedFields, acquisitionPlanReferences, acquisitionReportRows, recordAcquisitions, newAcquisition, reuseAcquisitions, saveAcquisitionBook, type Acquisition, type AcquisitionBook } from '@/lib/iva-acquisitions';
import { acquisitionTreatmentIssue, continueWithoutFormerExploration, formerExplorationSource, type AcquisitionTreatment } from '@/lib/iva-acquisition-continuity';
@@ -22,7 +23,7 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
<p>Oportunidade em tratamento: {String(draft.context_opportunity||'entrada geral')}. Moeda de origem: {book.currency||'por indicar'}. A compra registada não gera automaticamente IVA dedutível.</p>
{book.currency && draft.currency!==book.currency && <p role="alert">As compras conservam {book.currency}; o projecto está em {String(draft.currency||'moeda por indicar')}. Reveja a moeda no projecto. Os montantes e o resultado anterior não foram convertidos ou reinterpretados.</p>}
{!book.currency && !!draft.currency && <button type="button" className="underline" onClick={()=>save({...book,currency:String(draft.currency),rows:book.rows.map(row=>row.sourceCurrency?row:{...row,sourceCurrency:String(draft.currency)})})}>Confirmar explicitamente {String(draft.currency)} como moeda de origem dos valores sem moeda, conservando os montantes</button>}
<p>L2-A: aquisições internas em MZN e afectação exclusiva a uma operação classificada, com direito, sem direito ou indeterminado. Documento e dedução no mesmo período de2026; importações, exclusões especiais, afectação parcial e regularizações conservam percursos próprios. Nas projecções, cada confirmação é um pressuposto. Indique dois valores entre líquido, IVA e bruto; o terceiro é obtido sem taxa de compra presumida.</p>
<p>Aquisições em MZN/2026,ligadas à operação de destino:com direito,sem direito ou indeterminadas. A/B/C distinguem compras internas,importações de bens e exclusões/excepções. Documento e dedução no mesmo mês;afectação mista,proporções18(6),dedução posterior e regularizações conservam percursos próprios. Nas projecções,cada confirmação é um pressuposto. Compras internas:indique dois valores entre líquido,IVA e bruto. Importações:IVA expressamente identificado no bilhete;custos líquido/bruto são opcionais,sem calcular direitos ou presumir taxa.</p>
<label className="block">Adquirente sujeito passivo de IVA no regime normal (efectivo ou pressuposto da projecção)<select className="block rounded border p-2" value={book.taxpayerRegistered??''} onChange={e=>save({...book,taxpayerRegistered:e.target.value as AcquisitionBook['taxpayerRegistered']})}><option value="">Por confirmar</option><option value="yes">Sim</option><option value="no">Não / outro enquadramento — fora deste lote</option></select></label>
{error&&<p role="alert">{error}</p>}
<details className="rounded border p-2"><summary>Indicar mês da aquisição pelo documento ou pressuposto da projecção</summary>
@@ -34,6 +35,7 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
<button type="button" className="ml-3 underline" disabled={!book.periods.length} onClick={()=>save({...book,rightVersion:acquisitionRightVersion,rows:[...book.rows,newAcquisition(crypto.randomUUID(),book.periods[0]?.period??'',book.currency)]})}>Acrescentar aquisição em falta</button>
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Aquisição adicional'} — {row.period} — campo{row.field}</summary>
<AcquisitionOperationLink draft={draft} row={row} onChange={patch=>change(row.id,patch)}/>
<AcquisitionImportsExclusions draft={draft} row={row} onChange={patch=>change(row.id,patch)}/>
<details className="rounded border p-2"><summary>Enquadramento especial da aquisição / referência anterior</summary>
<label>Tratamento da aquisição<select value={row.acquisitionTreatment??''} onChange={e=>row.acquisitionTreatment==='former_exploration_exemption'&&e.target.value!=='former_exploration_exemption'?run(()=>save({...book,rows:book.rows.map(item=>item.id===row.id?continueWithoutFormerExploration(item):item)})):change(row.id,{acquisitionTreatment:e.target.value as AcquisitionTreatment})}><option value="">Enquadramento normal; sem benefício especial indicado</option><option value="former_exploration_exemption">Antiga isenção de prospecção/pesquisa —14(2)(c),eliminada</option><option value="normal">Continuidade pelo enquadramento normal</option></select></label>
{acquisitionTreatmentIssue(row.acquisitionTreatment)&&<p role="status">{acquisitionTreatmentIssue(row.acquisitionTreatment)}</p>}
@@ -44,19 +46,19 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
<div className="grid gap-3 md:grid-cols-2">
<label>Descrição<input className="block rounded border p-2" value={row.description} onChange={e=>change(row.id,{description:e.target.value})}/></label>
{row.sourceMonth===null && <label>Reutilizar valor existente, sem presumir que o orçamento é uma factura<select className="block rounded border p-2" value="" onChange={e=>run(()=>{const ref=references.find(r=>r.key===e.target.value);if(ref)change(row.id,{sourceMonth:ref.month,sourcePeriod:ref.period,sourceField:ref.kind,sourceAmount:ref.amount,sourceCurrency:ref.currency,...(ref.kind==='asset'?{}:{period:ref.period})});})}><option value="">Escolher apenas se corresponde a esta aquisição</option>{references.filter(ref=>ref.kind===(row.field==='08'?'asset':row.field==='10'?'operating':'inventory')).map(ref=><option key={ref.key} value={ref.key}>{ref.label}: {ref.amount??'sem informação'} {ref.currency}</option>)}</select></label>}
{select(row,'field','Categoria',[['08','08 — Imobilizado'],['09','09 — Existências'],['10','10 — Outros bens e serviços']])}
{select(row,'field','Categoria',[['08','08 — Imobilizado'],['09','09 — Existências'],['10','10 — Outros bens e serviços'],...(row.bc?[['11','11 — Importações de bens'] as [string,string]]:[])])}
{select(row,'origin','Natureza dos dados',[['projected','Projectados / pressupostos'],['effective','Efectivos declarados']])}
<label>Período de dedução<input className="block rounded border p-2" type="month" value={row.period} onChange={e=>change(row.id,{period:e.target.value})}/></label>
{row.sourceMonth!==null && <label>O valor de origem corresponde a<select className="block rounded border p-2" value={row.sourceBasis} onChange={e=>{const basis=e.target.value as Acquisition['sourceBasis'];change(row.id,{sourceBasis:basis,...(basis==='net'&&!row.net?{net:row.sourceAmount??''}:basis==='gross'&&!row.gross?{gross:row.sourceAmount??''}:{})});}}><option value="">Por classificar</option><option value="net">Valor sem IVA</option><option value="gross">Valor com IVA</option></select></label>}
{(['net','vat','gross'] as const).map((field,index)=><label key={field}>{['Valor líquido','IVA indicado','Valor bruto'][index]} ({row.sourceCurrency})<input className="block rounded border p-2" inputMode="decimal" value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
{select(row,'documentType','Tipo de documento',[['invoice','Factura'],['equivalent','Documento equivalente']])}
{select(row,'documentType','Tipo de documento',row.domestic==='no'&&row.bc?[['customs','Bilhete de despacho de importação']]:[['invoice','Factura'],['equivalent','Documento equivalente']])}
<label>Referência do documento / previsão<input className="block rounded border p-2" value={row.documentRef} onChange={e=>change(row.id,{documentRef:e.target.value})}/></label>
<label>Linha do documento, se discriminado em várias aquisições<input className="block rounded border p-2" value={row.documentLine} onChange={e=>change(row.id,{documentLine:e.target.value})}/></label>
<label>Data do documento / prevista<input className="block rounded border p-2" type="date" value={row.documentDate} onChange={e=>change(row.id,{documentDate:e.target.value})}/></label>
<label>Fornecedor / previsto<input className="block rounded border p-2" value={row.supplier} onChange={e=>change(row.id,{supplier:e.target.value})}/></label>
{!(row.bc&&row.domestic==='no')&&<label>Fornecedor / previsto<input className="block rounded border p-2" value={row.supplier} onChange={e=>change(row.id,{supplier:e.target.value})}/></label>}
{select(row,'domestic','Aquisição interna',yesNo)}
{row.acquisitionTreatment!=='former_exploration_exemption'&&<>
{select(row,'supplierRegistered','Fornecedor inscrito no IVA',yesNo)}
{!(row.bc&&row.domestic==='no')&&select(row,'supplierRegistered','Fornecedor inscrito no IVA',yesNo)}
{select(row,'legalDocument','Documento emitido em forma legal',yesNo)}
{select(row,'held','Documento na posse do adquirente / previsto na projecção',yesNo)}
{select(row,'allocation','Afectação',[['taxable_only',row.deductionLink?'Exclusiva à operação ligada':'Exclusiva a operações tributadas'],['mixed','Mista / parcial — fora deste lote'],['no_right','Operações sem direito à dedução']])}
@@ -64,11 +66,12 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
{select(row,'integrity','Operação real / pressuposto regular, sem simulação, margem de segunda mão ou impedimentos do art.18(3–5)',yesNo)}
</>}
</div>
<p>Fundamento: CIVA arts.18–21, fontes e versão no resultado. Não se calcula dedução parcial, excepção de despesa ou dedução posterior neste lote.</p>
<p>Fundamento:CIVA18–21,fontes e versão no resultado. {row.bc?'Aplicam-se apenas as exclusões/excepções documentadas escolhidas;gasóleo50% conserva fracção exacta. Sem pro rata,proporções18(6) ou regularização neste incremento.':'Afectação parcial,despesa especial e dedução posterior exigem percurso próprio;use a análiseC quando aplicável.'}</p>
{row.sourceMonth===null && <button type="button" className="mr-3 underline" onClick={()=>save({...book,rows:book.rows.filter(r=>r.id!==row.id)})}>Remover esta aquisição introduzida directamente</button>}
{row.sourceMonth!==null && <button type="button" className="underline" onClick={()=>run(()=>{const ref=acquisitionPlanReferences(draft).find(r=>r.month===row.sourceMonth && r.kind===(row.sourceField??'inventory'));if(!ref)throw new Error('Origem não disponível; conserve o registo e reveja o plano.');if(draft.currency!==row.sourceCurrency)throw new Error('Moeda divergente; não adoptar montantes de outra moeda.');change(row.id,{sourceAmount:ref.amount,sourcePeriod:ref.period,...(row.sourceBasis==='net'?{net:ref.amount??'',gross:''}:row.sourceBasis==='gross'?{gross:ref.amount??'',net:''}:{})});})}>Adoptar explicitamente o valor actual do plano (conserva histórico)</button>}
</details>)}
{book.periods.map(period=><details key={period.period} className="rounded border p-3"><summary>{period.period} — cobertura das parcelas — {period.complete && period.absent.length===acquisitionExcludedFields.length?'confirmada':'por completar'}</summary>
{book.rows.some(r=>r.bc)&&<label className="block"><input type="checkbox" checked={period.importsComplete??false} onChange={e=>save({...book,periods:book.periods.map(p=>p.period===period.period?{...p,importsComplete:e.target.checked}:p)})}/> Incluí todas as importações de bens/campo11 deste período,mesmo isentas ou sem direito;sem somar novamente IVA de serviços incluídos na base.</label>}
<label className="block"><input type="checkbox" checked={period.complete} onChange={e=>save({...book,periods:book.periods.map(p=>p.period===period.period?{...p,complete:e.target.checked}:p)})}/> Incluí todas as aquisições08–10 deste período, incluindo as sem direito à dedução; uma lista vazia significa que não houve essas aquisições.</label>
<p>Campos fora deste lote: assinale apenas quando não existiram as respectivas operações. Uma parcela não assinalada permanece indeterminada.</p>
{acquisitionExcludedFields.map(field=><label className="block" key={field}><input type="checkbox" checked={period.absent.includes(field)} onChange={e=>save({...book,periods:book.periods.map(p=>p.period===period.period?{...p,absent:e.target.checked?[...p.absent,field]:p.absent.filter(f=>f!==field)}:p)})}/> Sem operações no campo{field} — {labels[field]}</label>)}
+98
View File
@@ -0,0 +1,98 @@
import type {Acquisition,AcquisitionResult} from './iva-acquisitions.ts';
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
import {linkedOperationRight,linkedBeverageDeductionRight} from './iva-acquisition-right.ts';
import {acquisitionTreatmentIssue} from './iva-acquisition-continuity.ts';
import {decimalFraction,fraction,fractionPresentation,subtractFractions} from './fiscal-rational.ts';
export const acquisitionBCVersion='acquisition-imports-exclusions/1';
export const acquisitionBCSource='L2-B/C: CIVA18(1)(a/b),18(2),19,20(1)(a–f)/20(2)/20(3)(a/b),21. Lei13/2016 PDF14–15,p1426(272–273);Lei22/2022 PDF3,p2252(27),art20(2–3);Lei10/2025 PDF13,p2180(13),18(2)/19(2). Bebidas13(2):Lei13/2016 PDF12,p1426(270). MZN/2026,mesmo mês documental;sem pro rata18(6),regularizações ou restituiçãoL3.';
type Choice=''|'yes'|'no';
export const expenseOptions=[['ordinary','Despesa sem exclusão especial'],['vehicle','Viatura de turismo,barco de recreio,helicóptero,avião,moto ou outro veículo'],['fuel','Combustível normalmente utilizável em viaturas automóveis'],['travel','Transportes e viagens do sujeito passivo/pessoal'],['lodging_food','Alojamento e alimentação'],['beverages','Bebidas,tabaco e recepção'],['telephone','Comunicações telefónicas'],['luxury','Divertimento e luxo'],['other','Outro caso — por enquadrar']] as const;
export type AcquisitionBC={version:1;expense:string;reducedSupply:Choice;evidence:string;facts:Record<string,Choice>;fuelKind:''|'diesel'|'other';dieselUse:''|'heavy_passengers'|'public_passengers'|'machine'|'agriculture'|'other';beveragePath:''|'deduction'|'refund';importer:string;belongs:Choice;ownImportVat:Choice;vatBasis:''|'due'|'exempt'|'zero'|'unknown';vatRef:string;goodsRef:string;source?:{map:string;id:string;fingerprint:string}};
export function newAcquisitionBC():AcquisitionBC{return{version:1,expense:'',reducedSupply:'',evidence:'',facts:{},fuelKind:'',dieselUse:'',beveragePath:'',importer:'',belongs:'',ownImportVat:'',vatBasis:'',vatRef:'',goodsRef:''};}
export function acquisitionBCDetails(b:AcquisitionBC):string{
const labels:Record<string,string>={exigibleInPeriod:'IVA aduaneiro exigível no período de exercício confirmado pelo documento',tourism:'Bem abrangido20(1)(a)',saleOrExploit:'Venda/exploração como objecto da actividade',resale:'Combustível para revenda',dieselScope:'Âmbito específico do uso de gasóleo confirmado',commercialTraveller:'Viajante comercial por conta própria',ownLandline:'Telefone fixo em nome do sujeito passivo',beverageSpecial:'Bebidas no âmbito13(2)',onlyDeduction:'Parcela exclusivamente para dedução',notCoastalFishing:'Sem provisões de pesca costeira'};
const keys:Record<string,string[]>={vehicle:['tourism',...(b.facts.tourism==='yes'?['saleOrExploit']:[])],fuel:['resale',...(b.fuelKind==='diesel'&&b.dieselUse!=='other'?['dieselScope']:[])],lodging_food:['commercialTraveller'],telephone:['ownLandline'],beverages:['beverageSpecial',...(b.facts.beverageSpecial==='yes'?['onlyDeduction','notCoastalFishing']:[])]};
return [...(b.facts.exigibleInPeriod!==undefined?['exigibleInPeriod']:[]),...(keys[b.expense]??[])].map(k=>`${labels[k]}:${b.facts[k]==='yes'?'Sim':b.facts[k]==='no'?'Não':'Por indicar'}`).concat(b.expense==='fuel'?[`Combustível:${b.fuelKind==='diesel'?'Gasóleo':b.fuelKind==='other'?'Outro':'Por indicar'}`,`Utilização:${({heavy_passengers:'Pesado de passageiros',public_passengers:'Transporte público licenciado,excepto rent a car',machine:'Máquina,não veículo matriculado',agriculture:'Tractor agrícola,emprego exclusivo/predominante',other:'Outro uso50%','':'Por indicar'})[b.dieselUse]}`]:[],b.expense==='beverages'&&b.facts.beverageSpecial==='yes'?[`Via:${b.beveragePath==='deduction'?'DeduçãoL2':b.beveragePath==='refund'?'RestituiçãoL3':'Por indicar'}`]:[]).join('; ');
}
export function validAcquisitionBC(v:unknown):v is AcquisitionBC{
if(!v||typeof v!=='object'||Array.isArray(v))return false;const b=v as AcquisitionBC;
return b.version===1&&['expense','evidence','vatRef','goodsRef','importer','fuelKind','dieselUse','beveragePath','vatBasis'].every(k=>typeof b[k as keyof AcquisitionBC]==='string')&&[b.reducedSupply,b.belongs,b.ownImportVat].every(v=>['','yes','no'].includes(v))&&!!b.facts&&typeof b.facts==='object'&&!Array.isArray(b.facts)&&Object.values(b.facts).every(v=>['','yes','no'].includes(v))&&(!b.source||['map','id','fingerprint'].every(k=>typeof b.source?.[k as keyof typeof b.source]==='string'));
}
type ImportSource={id:string;treatment?:string;description?:string;documentRef?:string;documentDate?:string;period?:string;importDate?:string;importer?:string};
const importMaps=['importExemptions','objectiveImports','remissionImports'];
export function acquisitionImportSources(draft:Record<string,unknown>){const store=readAcquisitionStore(draft.modelo_a_snapshot) as unknown as Record<string,Record<string,{rows:ImportSource[]}>>;return importMaps.flatMap(map=>(store[map]?.[acquisitionScope(draft)]?.rows??[]).map(row=>({map,row,label:`${row.description||row.id} — ${row.period??''} — ${row.documentRef||'documento por indicar'}`})));}
export function reuseAcquisitionImport(draft:Record<string,unknown>,row:Acquisition,map:string,id:string):Acquisition{
const src=acquisitionImportSources(draft).find(v=>v.map===map&&v.row.id===id);if(!src)throw Error('Origem da importação indisponível;dados conservados.');
const bc=row.bc??newAcquisitionBC();return {...row,description:row.description||src.row.description||'',documentRef:row.documentRef||src.row.documentRef||'',documentDate:row.documentDate||src.row.documentDate||src.row.importDate||'',period:row.period||src.row.period||'',bc:{...bc,importer:bc.importer||src.row.importer||'',source:{map,id,fingerprint:JSON.stringify(src.row)}}};
}
export function acquisitionBCSourceKey(draft:Record<string,unknown>,rows:Acquisition[]):string{return rows.some(r=>r.bc)?JSON.stringify({version:acquisitionBCVersion,sources:rows.filter(r=>r.bc?.source).map(r=>acquisitionImportSources(draft).find(s=>s.map===r.bc!.source!.map&&s.row.id===r.bc!.source!.id)?.row??null)}):'';}
function validDate(value:string){const d=new Date(value+'T00:00:00Z');return /^\d{4}-\d{2}-\d{2}$/.test(value)&&Number.isFinite(d.getTime())&&d.toISOString().slice(0,10)===value;}
export function evaluateAcquisitionBC(row:Acquisition,currency:string,registered:Choice,draft:Record<string,unknown>|undefined,raw:AcquisitionResult):AcquisitionResult{
const bc=row.bc!;let right=row.deductionLink&&draft?linkedOperationRight(draft,row.deductionLink):{status:'indeterminate' as const,reason:'Vínculo à operação de destino em falta.',source:acquisitionBCSource};
const stop=(reason:string,status:'eligible'|'no_right'|'indeterminate'='indeterminate',percent?:number):AcquisitionResult=>{
const exact=percent!==undefined&&raw.vat!==null?decimalFraction(raw.vat):null;
const deductibleExact=exact?fraction(BigInt(exact.numerator)*BigInt(percent!),BigInt(exact.denominator)*100n):undefined;
return {...raw,reason,deductible:deductibleExact?fractionPresentation(deductibleExact).decimal:null,...(deductibleExact?{deductibleExact,nonDeductibleExact:subtractFractions(exact!,deductibleExact)}:{}),eligibility:{...right,status,reason,source:acquisitionBCSource+'; '+right.source}};
};
if(!validAcquisitionBC(bc))return stop('CondiçõesB/C não recuperáveis;sem regra por defeito.');
if(currency!=='MZN'||row.sourceCurrency!==currency||!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||raw.vat===null)return stop(raw.reason||'IVA/moeda/período em falta.');
if(row.domestic==='no'){
if(!/^\d{1,15}(\.\d{1,6})?$/.test(row.vat.trim()))return stop('Indique expressamente o IVA do bilhete;diferença entre custo líquido/bruto não constitui montante aduaneiro identificado.');
if(row.net.trim()&&raw.net===null||row.gross.trim()&&raw.gross===null)return stop('Valores de custo preenchidos inválidos;conservar e resolver a divergência.');
}else if(raw.net===null||raw.gross===null)return stop(raw.reason);
if(raw.net!==null&&raw.gross!==null&&fractionPresentation(subtractFractions(decimalFraction(raw.gross),decimalFraction(raw.net))).decimal!==raw.vat)return stop('Líquido + IVA diverge do bruto declarado;sem substituir valores.');
const treatmentIssue=acquisitionTreatmentIssue(row.acquisitionTreatment);if(treatmentIssue)return stop(treatmentIssue);
if(!['projected','effective'].includes(row.origin))return stop('Indique dados efectivos ou pressupostos de projecção.');
if(row.sourceMonth!==null&&(!row.sourceBasis||row.sourceAmount===null||(row.sourceBasis==='net'?raw.net:raw.gross)===null||JSON.stringify(decimalFraction(row.sourceAmount))!==JSON.stringify(decimalFraction((row.sourceBasis==='net'?raw.net:raw.gross)!))))return stop('Valor de origem divergente ou por classificar;sem sobrescrita.');
if(row.domestic==='no'&&row.field!=='11'||row.domestic==='yes'&&row.field==='11'||!['yes','no'].includes(row.domestic))return stop('Separe aquisição interna08–10 da importação de bens11.');
if(bc.source){const src=draft?acquisitionImportSources(draft).find(s=>s.map===bc.source!.map&&s.row.id===bc.source!.id):undefined;if(!src||JSON.stringify(src.row)!==bc.source.fingerprint)return stop('OrigemL1 da importação alterada:adopte explicitamente os dados actuais,conservando edições e histórico.');if(['service_in_customs_value','accessory_service_first_destination'].includes(src.row.treatment??''))return stop('Origem é serviço incluído na base aduaneira12(1)(e),não IVA próprio da importação de bens;identifique o bilhete dos bens,sem somar novamente IVA do serviço.');}
if(registered!=='yes')return stop('Enquadramento do adquirente no regime normal por confirmar.');
if(row.allocation==='no_right'||row.integrity==='no'||row.exclusion==='excluded')return stop('Sem direito pela afectação,exclusão expressa ou impedimento18(3–5) declarado.','no_right',0);
if(row.allocation==='mixed')return stop('Afectação parcial/pro rata depende deD;sem dedução integral presumida.');
if(!row.deductionLink||row.deductionLink.scopeConfirmed!=='yes'||!row.deductionLink.allocationRef.trim()||row.allocation!=='taxable_only')return stop('Confirme afectação exclusiva e referência à operação ligada.');
if(bc.expense==='beverages'&&bc.facts.beverageSpecial==='yes'&&bc.beveragePath==='deduction'&&draft)right=linkedBeverageDeductionRight(draft,row.deductionLink,bc.facts.notCoastalFishing==='yes');
if(right.status==='no_right')return stop(right.reason,'no_right',0);
if(right.status!=='eligible')return stop(right.reason);
if(row.deductionLink.noExcludedPayments==='no')return stop('Pagamentos15(6)(c)/19(2) excluídos.','no_right',0);
if(row.deductionLink.noExcludedPayments!=='yes')return stop('Exclusão19(2) por esclarecer.');
if(!validDate(row.documentDate)||row.documentDate.slice(0,7)!==row.period)return stop('Documento e exercício no mesmo mês neste incremento;dedução posterior não presumida.');
if(!row.documentRef.trim()||row.legalDocument!=='yes'||row.held!=='yes'||row.integrity!=='yes')return stop('Referência,forma legal,posse e integridade do documento por completar.');
if(row.domestic==='no'){
if(row.documentType!=='customs'||!bc.importer.trim()||!bc.goodsRef.trim()||!bc.vatRef.trim()||bc.belongs!=='yes'||bc.ownImportVat!=='yes')return stop('Bilhete de despacho,importador/titularidade,bens/linha e IVA próprio devido na importação por completar;fornecedor inscrito no IVA interno não é exigido.');
if(bc.facts.exigibleInPeriod!=='yes')return stop('Exigibilidade do IVA aduaneiro no período de exercício por confirmar pelo documento21(1);não inferida só da emissão do bilhete.Dedução posterior conserva percurso próprio.');
if(!['due','exempt','zero'].includes(bc.vatBasis))return stop('IVA da importação desconhecido;não inferir montante da classificaçãoL1 ou calcular direitosL13.');
if(['exempt','zero'].includes(bc.vatBasis)&&decimalFraction(raw.vat).numerator!=='0')return stop('IVA positivo incompatível com isenção/zero declarado;resolver divergência sem substituir montante.');
}else if(!['invoice','equivalent'].includes(row.documentType)||!row.supplier.trim()||row.supplierRegistered!=='yes')return stop('Factura/documento equivalente e fornecedor interno inscrito por completar.');
if(row.domestic==='yes'&&bc.reducedSupply==='yes')return stop('IVA suportado em fornecimento17-A:exclusão20(2),Lei22/2022;distinta do destino19(2).','no_right',0);
if(row.domestic==='yes'&&bc.reducedSupply!=='no')return stop('Indique se o IVA suportado respeita a fornecimento17-A;sem inferir pela taxa ou valor.');
if(!expenseOptions.some(([id])=>id===bc.expense)||bc.expense==='other'||!bc.evidence.trim())return stop('Natureza da despesa e fundamento factual/documental por identificar.');
const f=bc.facts;
switch(bc.expense){
case 'ordinary':return stop('Elegível pelas condições declaradas18/19;sem exclusão especial20 identificada.','eligible',100);
case 'vehicle':
if(f.tourism==='')return stop('Classificação do veículo na definição20(1)(a) em falta.');
if(f.tourism==='no')return stop('Veículo fora da definição20(1)(a),segundo características/utilização documentadas.','eligible',100);
if(f.tourism!=='yes')return stop('Indique se o bem está abrangido por20(1)(a).');
if(f.saleOrExploit==='yes')return stop('Excepção20(3)(a):venda/exploração do bem constitui objecto da actividade;combustível conserva20(1)(b).','eligible',100);
return f.saleOrExploit==='no'?stop('Bem abrangido20(1)(a),sem excepção20(3)(a).','no_right',0):stop('Venda/exploração como objecto da actividade por esclarecer.');
case 'fuel':
if(f.resale==='yes')return stop('Combustível adquirido para revenda,conforme ressalva20(3)(a);fundamento específico documentado.','eligible',100);
if(f.resale!=='no')return stop('Destino de revenda do combustível por esclarecer.');
if(bc.fuelKind==='other')return stop('Combustível normalmente utilizável em viaturas,fora da excepção gasóleo20(1)(b).','no_right',0);
if(bc.fuelKind!=='diesel')return stop('Natureza do combustível por indicar.');
if(bc.dieselUse==='other')return stop('Gasóleo20(1)(b):dedução50%,não taxa de IVA nem pro rata geral.','eligible',50);
if(['heavy_passengers','public_passengers','machine','agriculture'].includes(bc.dieselUse)){
if(f.dieselScope!=='yes')return stop('Confirme o âmbito específico20(1)(b)(i–iv);não basta escolher a categoria.');
return stop('Gasóleo20(1)(b)(i–iv):dedução integral no uso específico confirmado;transporte público exclui rent a car,máquina não é veículo matriculado,tractor agrícola tem emprego exclusivo/predominante.','eligible',100);
}return stop('Utilização do gasóleo por indicar.');
case 'travel':return stop('Transportes/viagens do sujeito passivo e pessoal excluídos20(1)(c).','no_right',0);
case 'lodging_food':return f.commercialTraveller==='yes'?stop('Excepção20(3)(b):viajante comercial por conta própria,no quadro da actividade profissional.','eligible',100):f.commercialTraveller==='no'?stop('Alojamento/alimentação excluídos20(1)(d),sem excepção20(3)(b).','no_right',0):stop('Âmbito de viajante comercial por conta própria por esclarecer.');
case 'telephone':return f.ownLandline==='yes'?stop('Telefone fixo em nome do sujeito passivo:excepção expressa20(1)(e).','eligible',100):f.ownLandline==='no'?stop('Comunicações telefónicas fora da excepção20(1)(e).','no_right',0):stop('Telefone fixo e titularidade por esclarecer.');
case 'luxury':return stop('Despesa de divertimento/luxo20(1)(f),segundo natureza/montante e exploração normal declarados.','no_right',0);
case 'beverages':
if(bc.facts.beverageSpecial==='no')return stop('Bebidas/tabaco/recepção excluídos20(1)(d),fora do fundamento especial13(2).','no_right',0);
if(bc.facts.beverageSpecial==='yes'&&bc.beveragePath==='deduction'&&bc.facts.onlyDeduction==='yes'&&right.reason.startsWith('Via de dedução13(2)'))return stop(right.reason+' IVA suportado identificado neste documento;mesma parcela não segue restituiçãoL3.','eligible',100);
return stop('Via especial13(2) por delimitar:dedução ou restituiçãoL3,sem dupla contagem;tabaco/recepção não herdam a excepção de bebidas.');
}
return stop('Tratamento não implementado;rascunho preservado.');
}
@@ -6,6 +6,11 @@ export type AcquisitionRightLink={version:1;scopeId:string;operationId:string;fi
export type AcquisitionEligibility={status:'eligible'|'no_right'|'indeterminate';reason:string;source:string;operationId?:string;operationPeriod?:string;operationTreatment?:string;operationDescription?:string;operationArticle?:string;scopeId?:string};
export function validAcquisitionRightLink(value:unknown):value is AcquisitionRightLink{if(!value||typeof value!=='object'||Array.isArray(value))return false;const v=value as AcquisitionRightLink;return v.version===1&&['scopeId','operationId','fingerprint','allocationRef'].every(k=>typeof v[k as keyof AcquisitionRightLink]==='string')&&['','yes','no'].includes(v.scopeConfirmed)&&['','yes','no'].includes(v.noExcludedPayments);}
function fingerprint(row:SalesOperation,result:SalesLineResult|undefined){return JSON.stringify({version:acquisitionRightVersion,input:row,classification:result?{article:result.article,source:result.source,status:result.status}:null});}
export function linkedBeverageDeductionRight(draft:Record<string,unknown>,link:AcquisitionRightLink,notCoastalFishing:boolean):AcquisitionEligibility{
const base=linkedOperationRight(draft,link),b=salesBook(draft),row=b.rows.find(r=>r.id===link.operationId),line=b.result?.rows.find(r=>r.id===link.operationId),rule=operationTreatments.find(t=>t.id===row?.treatment);
if(link.scopeId!==acquisitionScope(draft)||!salesCurrent(draft,b)||!row||!line||!rule||link.fingerprint!==fingerprint(row,line)||row.supplyCategory!=='beverages'||!['onboard_seagoing_ship','onboard_international_aircraft','onboard_rescue_coastal_ship'].includes(row.treatment)||!row.description.trim()||!row.establishment.trim()||!row.evidence.trim()||rule.conditions.some((_,i)=>!row.confirmed.includes(i))||!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||row.sourceCurrency!=='MZN'||!/^\d{1,15}(\.\d{1,6})?$/.test(row.amount)||!['net','included'].includes(row.basis)||row.treatment==='onboard_rescue_coastal_ship'&&!notCoastalFishing)return {...base,status:'indeterminate',reason:'Correspondência própria13(2) por demonstrar em origemL1 actual:c/d/h,bebidas,condições e documento;pesca costeira conserva exclusão de provisões.'};
return {...base,status:'eligible',reason:'Via de dedução13(2) de bebidas em13(1)(c/d/h),nas condições demonstradas;20(1)(d) não se aplica para este efeito. Não concede isenção directa nem restituição.',source:base.source+';CIVA13(2–3),Lei13/2016 PDF12/p1426(270)'};
}
export function acquisitionOperationChoices(draft:Record<string,unknown>){const book=salesBook(draft);return book.rows.map(row=>({id:row.id,label:`${row.description||row.id} — ${row.period} — ${operationTreatments.find(t=>t.id===row.treatment)?.label??row.treatment}`,row,current:salesCurrent(draft,book)}));}
export function createAcquisitionRightLink(draft:Record<string,unknown>,operationId:string):AcquisitionRightLink{const b=salesBook(draft),row=b.rows.find(r=>r.id===operationId);if(!row)throw Error('Operação de origem não encontrada nesta oportunidade;dados preservados.');return{version:1,scopeId:acquisitionScope(draft),operationId,fingerprint:fingerprint(row,b.result?.rows.find(r=>r.id===operationId)),scopeConfirmed:'',allocationRef:'',noExcludedPayments:''};}
export function linkedOperationRight(draft:Record<string,unknown>,link:AcquisitionRightLink):AcquisitionEligibility{
+24 -13
View File
@@ -2,6 +2,7 @@ import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
import { restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
import { salesBook, salesCurrent as salesOperationsCurrent } from './iva-operations.ts';
import {evaluateAcquisitionBC,validAcquisitionBC,acquisitionBCSource,acquisitionBCSourceKey,acquisitionBCDetails,expenseOptions,type AcquisitionBC} from './iva-acquisition-bc.ts';
import { linkedOperationRight, validAcquisitionRightLink, acquisitionRightVersion, acquisitionRightSource, type AcquisitionRightLink, type AcquisitionEligibility } from './iva-acquisition-right.ts';
import { acquisitionTreatmentIssue, acquisitionContinuityVersion, formerExplorationSource, type AcquisitionTreatment } from './iva-acquisition-continuity.ts';
import { addFractions, decimalFraction, fractionLabel, fractionPresentation, positiveFraction, subtractFractions, type FiscalFraction } from './fiscal-rational.ts';
@@ -11,18 +12,19 @@ export const acquisitionBasis = 'CIVA arts.18(1)(a),18(2) (Lei10/2025),18(3–5)
export const acquisitionSources = 'Modelo A DOCX 62c198e9 / v1 63d45fa8; VAT Return.pdf SHA256 9c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741, p.1; CIVA republicado Lei13/2016, PDF14–16; Lei22/2022, art.20; Lei10/2025, arts.18/19, PDF13.';
type Choice = '' | 'yes' | 'no';
export type Acquisition = {
id: string; field: '08'|'09'|'10'; period: string; origin: 'projected'|'effective';
id: string; field: '08'|'09'|'10'|'11'; period: string; origin: 'projected'|'effective';
sourceMonth: number|null; sourcePeriod?:string; sourceField?:'inventory'|'asset'|'operating'; sourceAmount: string|null; sourceCurrency: string; sourceBasis: ''|'net'|'gross';
net: string; vat: string; gross: string; documentType: ''|'invoice'|'equivalent'; documentRef: string; documentDate: string; documentLine:string;
net: string; vat: string; gross: string; documentType: ''|'invoice'|'equivalent'|'customs'; documentRef: string; documentDate: string; documentLine:string;
supplier: string; supplierRegistered: Choice; legalDocument: Choice; held: Choice; domestic: Choice;
allocation: ''|'taxable_only'|'mixed'|'no_right'; exclusion: ''|'none'|'excluded'|'special';
integrity: Choice; description: string;
acquisitionTreatment?: AcquisitionTreatment;
deductionLink?: AcquisitionRightLink;
bc?:AcquisitionBC;
acquisitionTreatmentOrigin?: {id:'former_exploration_exemption';source:string};
};
export type AcquisitionResult = {id:string; net:string|null; vat:string|null; gross:string|null; deductible:string|null; reason:string; eligibility?:AcquisitionEligibility};
type PeriodInput = {period:string; complete:boolean; absent:string[]};
export type AcquisitionResult = {id:string; net:string|null; vat:string|null; gross:string|null; deductible:string|null; reason:string; eligibility?:AcquisitionEligibility;deductibleExact?:FiscalFraction;nonDeductibleExact?:FiscalFraction};
type PeriodInput = {period:string; complete:boolean; absent:string[];importsComplete?:boolean};
export type AcquisitionBook = {currency:string; rightVersion?:string; taxpayerRegistered?:Choice; rows:Acquisition[]; periods:PeriodInput[]; result?:AcquisitionCalculation;history?:AcquisitionCalculation[]};
export type AcquisitionCalculation = {version:string; revision:number; calculatedAt:string; opportunity:string; location:string; currency:string; sourceKey:string; basis:string; sources:string;
declarations?:{taxpayerRegistered:Choice;periods:PeriodInput[]}; rows:AcquisitionResult[]; inputs:Acquisition[]; periods:Array<{period:string; fields:Record<string,string|null>; exactFields?:Record<string,FiscalFraction|null>; reasons:string[]}>};
@@ -41,6 +43,7 @@ export function readAcquisitionStore(raw:unknown):Store {
const value=JSON.parse(String(raw));
if (value?.version!==1 || !value.books || typeof value.books!=='object' || Array.isArray(value.books)) throw new Error('Compras fiscais guardadas não recuperáveis; dados preservados.');
for(const book of Object.values(value.books) as AcquisitionBook[]) {
if(book?.rows?.some(row=>row.bc!==undefined&&!validAcquisitionBC(row.bc))||book?.periods?.some(p=>p.importsComplete!==undefined&&typeof p.importsComplete!=='boolean'))throw new Error('CondiçõesB/C ou cobertura de importações inválidas;originais preservados.');
if(book?.rightVersion!==undefined&&book.rightVersion!==acquisitionRightVersion)throw new Error('Versão do direito da aquisição não recuperável; original preservado.');
if(book?.rows?.some(row=>row.deductionLink!==undefined&&!validAcquisitionRightLink(row.deductionLink)))throw new Error('Vínculo da aquisição não recuperável; original preservado.');
if(book?.history!==undefined&&!Array.isArray(book.history))throw new Error('Histórico de aquisições não recuperável; originais preservados, sem substituição.');
@@ -114,6 +117,7 @@ function evaluateAcquisitionBase(row:Acquisition,currency:string,taxpayerRegiste
}
export function evaluateAcquisition(row:Acquisition,currency:string,taxpayerRegistered:Choice='',draft?:Record<string,unknown>):AcquisitionResult {
const result=evaluateAcquisitionBase(row,currency,taxpayerRegistered);
if(row.bc)return evaluateAcquisitionBC(row,currency,taxpayerRegistered,draft,result);
if(!row.deductionLink)return result;
const right=draft?linkedOperationRight(draft,row.deductionLink):{status:'indeterminate' as const,reason:'Contexto da operação em falta.',source:acquisitionRightSource};
const finish=(status:AcquisitionEligibility['status'],reason:string,deductible:string|null=null):AcquisitionResult=>({...result,deductible,reason,eligibility:{...right,status,reason,source:right.source+'; '+acquisitionBasis}});
@@ -133,7 +137,7 @@ function sourceKey(draft:Record<string,unknown>,book:AcquisitionBook):string {
// Change detection only, never an authorization or authenticity token.
let hash=14695981039346656037n;for(let i=0;i<raw.length;i++)hash=BigInt.asUintN(64,(hash^BigInt(raw.charCodeAt(i)))*1099511628211n);
const operations=(readAcquisitionStore(draft.modelo_a_snapshot) as Store & {salesOperations?:Record<string,unknown>}).salesOperations?.[acquisitionScope(draft)];
return `${raw.length}:${hash.toString(16)}${operations?`:${JSON.stringify(operations)}`:''}${book.rows.some(r=>r.acquisitionTreatment||r.acquisitionTreatmentOrigin)?`:${acquisitionContinuityVersion}`:''}${book.rightVersion||book.rows.some(r=>r.deductionLink)?`:${acquisitionRightVersion}`:''}`;
return `${raw.length}:${hash.toString(16)}${operations?`:${JSON.stringify(operations)}`:''}${book.rows.some(r=>r.acquisitionTreatment||r.acquisitionTreatmentOrigin)?`:${acquisitionContinuityVersion}`:''}${book.rightVersion||book.rows.some(r=>r.deductionLink)?`:${acquisitionRightVersion}`:''}${book.rows.some(r=>r.bc)?`:${acquisitionBCSourceKey(draft,book.rows)}`:''}`;
}
function location(draft:Record<string,unknown>):string {
const labels=draft.selection_labels as Record<string,unknown>|undefined;
@@ -147,14 +151,14 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
if(new Set(book.rows.map(r=>r.id)).size!==book.rows.length) throw new Error('Existem compras com identidade repetida.');
const documents=book.rows.filter(r=>r.documentRef.trim() && r.supplier.trim()).map(r=>JSON.stringify([r.origin,r.supplier.trim().toLowerCase(),r.documentRef.trim().toLowerCase(),r.documentDate,r.documentLine?.trim()??'']));
if(new Set(documents).size!==documents.length) throw new Error('Documento/linha repetido: discrimine linhas distintas ou remova a duplicação antes de deduzir.');
if(book.rows.some(r=>!['08','09','10'].includes(r.field) || !dates.includes(r.period))) throw new Error('Classifique cada aquisição num campo08–10 e num mês incluído na cobertura declarada das aquisições.');
if(book.rows.some(r=>!['08','09','10','11'].includes(r.field) || r.field==='11'&&!r.bc || !dates.includes(r.period))) throw new Error('Classifique cada aquisição num campo08–10 ou importaçãoB no campo11 e num mês incluído na cobertura declarada.');
const references=acquisitionPlanReferences(draft);
for(const row of book.rows.filter(r=>r.sourceMonth!==null)) {
const current=references.find(p=>p.month===row.sourceMonth && p.kind===(row.sourceField??'inventory'));
if(!current || current.amount!==row.sourceAmount || (current.kind!=='asset' && row.sourcePeriod && current.period!==row.sourcePeriod)) throw new Error('As compras ou orçamentos do plano mudaram. Resolva a divergência de origem antes de calcular; histórico preservado.');
}
if(book.rightVersion||book.rows.some(r=>r.deductionLink)){
const identities=book.rows.filter(r=>r.documentRef.trim()&&r.supplier.trim()).map(r=>JSON.stringify([r.supplier.trim().toLowerCase(),r.documentRef.trim().toLowerCase(),r.documentDate,r.documentLine.trim()]));
if(book.rightVersion||book.rows.some(r=>r.deductionLink||r.bc)){
const identities=book.rows.filter(r=>r.documentRef.trim()&&(r.supplier.trim()||r.field==='11')).map(r=>JSON.stringify([r.field==='11'?'customs':r.supplier.trim().toLowerCase(),r.documentRef.trim().toLowerCase(),r.documentDate,r.documentLine.trim()]));
if(new Set(identities).size!==identities.length)throw new Error('Documento/linha repetido entre projecção e efectivo; sem dupla dedução.');
}
const rows=book.rows.map(row=>{
@@ -191,6 +195,11 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
reasons.push(`Campo${field}: operações reduzidas/isentas/outras exigem afectar cada aquisição; não reutilizar dedução exclusiva geral como se fosse universal (L2).`);
}
}
if(book.rows.some(r=>r.bc)||input?.importsComplete!==undefined){
const imported=book.rows.filter(r=>r.period===period&&r.field==='11');
if(imported.length||input?.importsComplete)fields['11']=input?.importsComplete?sum(imported.map(r=>rows.find(x=>x.id===r.id)!.deductible)):null;
if(imported.length&&!input?.importsComplete)reasons.push('Campo11:confirme a cobertura de todas as importações do período,distinta das compras08–10.');
}
if(!input?.complete) reasons.push('Aquisições08–10: confirme que todas as compras do período foram incluídas, mesmo quando não existam.');
for(const row of book.rows.filter(r=>r.period===period)) {const r=rows.find(x=>x.id===row.id)!;if(r.deductible===null) reasons.push(`${row.description||row.id}: ${r.reason}`);}
for(const field of excluded) if(fields[field]===null) reasons.push(`Campo${field}: fora deste lote, sem declaração expressa de inexistência de operações.`);
@@ -201,9 +210,10 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
fields['17']=state===null||taxpayer===null?null:money(state>taxpayer?state-taxpayer:0n);
fields['18']=state===null||taxpayer===null?null:money(taxpayer>state?taxpayer-state:0n);
const classified=salesOperationsCurrent(draft,operations)?operations.result?.periods.find(p=>p.period===period):null;
if(classified?.exactFields) {
if(classified?.exactFields||book.rows.some(r=>r.bc)) {
const exactFields:Record<string,FiscalFraction|null>={};
for(const f of ['01','02','03','04','05','06','07','08','09','10','11','12','13'])exactFields[f]=classified.exactFields[f]??(fields[f]===null?null:decimalFraction(fields[f]!));
for(const f of ['01','02','03','04','05','06','07','08','09','10','11','12','13'])exactFields[f]=classified?.exactFields?.[f]??(fields[f]===null?null:decimalFraction(fields[f]!));
for(const f of ['08','09','10','11'])if(fields[f]!==null){const matching=book.rows.filter(r=>r.period===period&&r.field===f).map(r=>rows.find(v=>v.id===r.id)!);exactFields[f]=addFractions(matching.map(r=>r.deductibleExact??decimalFraction(r.deductible!)));}
for(const [f,parts] of [['14',['01','03','05','06','07']],['15',['08','09','10','11','12']],['16',['02','04','13']]] as const)exactFields[f]=parts.some(k=>exactFields[k]===null)?null:addFractions(parts.map(k=>exactFields[k]!));
exactFields['17']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['16'],exactFields['15'])):null;
exactFields['18']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['15'],exactFields['16'])):null;
@@ -213,7 +223,7 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
}
return {period,fields,reasons};
});
return {version:acquisitionVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:scope,location:location(draft),currency:book.currency,sourceKey:sourceKey(draft,book),basis:acquisitionBasis,sources:acquisitionSources+(book.rows.some(r=>r.acquisitionTreatment==='former_exploration_exemption'||r.acquisitionTreatmentOrigin)?`; ${formerExplorationSource}`:''),declarations:{taxpayerRegistered:book.taxpayerRegistered??'',periods:structuredClone(book.periods)},rows,inputs:structuredClone(book.rows),periods:calculated};
return {version:acquisitionVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:scope,location:location(draft),currency:book.currency,sourceKey:sourceKey(draft,book),basis:book.rows.some(r=>r.bc)?acquisitionBCSource:acquisitionBasis,sources:acquisitionSources+(book.rows.some(r=>r.acquisitionTreatment==='former_exploration_exemption'||r.acquisitionTreatmentOrigin)?`; ${formerExplorationSource}`:''),declarations:{taxpayerRegistered:book.taxpayerRegistered??'',periods:structuredClone(book.periods)},rows,inputs:structuredClone(book.rows),periods:calculated};
}
export function recordAcquisitions(draft:Record<string,unknown>,book:AcquisitionBook,now=new Date().toISOString()):AcquisitionBook {
const result=calculateAcquisitions(draft,book,now);
@@ -228,16 +238,17 @@ export function acquisitionReportRows(draft:Record<string,unknown>):Array<[strin
const out:Array<[string,string,string,string]>=[['IVA mensal','Apuramento parcial Modelo A',acquisitionCurrent(draft,book)?'Versão correspondente às entradas':'Histórico desactualizado; não aplicar à projecção actual',context],['IVA mensal','Fundamento das aquisições',result.basis,result.sources]];
for(const row of result.inputs) {
const r=result.rows.find(x=>x.id===row.id)!;
if(row.bc)out.push(['IVA mensal',`CondiçõesB/C — ${row.id}`,`${row.field==='11'?'Bilhete de despacho de importação;importador '+row.bc.importer+';bens '+row.bc.goodsRef+';IVA '+({due:'devido identificado',exempt:'isento',zero:'zero expresso',unknown:'desconhecido','':'por indicar'}[row.bc.vatBasis])+';fonte '+row.bc.vatRef:'Despesa '+(expenseOptions.find(([id])=>id===row.bc!.expense)?.[1]??'por identificar')+';IVA suportado17-A:'+({yes:'Sim',no:'Não','':'Por indicar'}[row.bc.reducedSupply])};dedutível exacto:${r.deductibleExact?fractionLabel(r.deductibleExact):'Indeterminado'};não dedutível:${r.nonDeductibleExact?fractionLabel(r.nonDeductibleExact):'Indeterminado'}`,`${row.bc.evidence};${acquisitionBCDetails(row.bc)};origem${row.bc.source?'registo de importaçãoL1 '+row.bc.source.id:'introdução directa'};${acquisitionBCSource}`]);
if(r.eligibility)out.push(['IVA mensal',`Direito da aquisição ${row.id}`,`${{eligible:'Elegível',no_right:'Sem direito',indeterminate:'Indeterminado'}[r.eligibility.status]}: ${r.eligibility.reason}`,`${r.eligibility.source}; operação ${r.eligibility.operationId??'por identificar'}; ${r.eligibility.operationPeriod??''}; ${r.eligibility.operationArticle??''}; afectação ${row.deductionLink?.allocationRef??''}`]);
const source=row.sourceMonth===null?'Introduzido pelo utilizador':`Plano (${row.sourceField==='asset'?'orçamento de activos':row.sourceField==='operating'?'orçamento operacional':'compras/encomendas'}), mês ${row.sourceMonth}, ${row.sourcePeriod??'período de origem não indicado'}, ${row.sourceAmount??'valor em falta'} ${row.sourceCurrency}; ${row.sourceBasis==='net'?'sem IVA':row.sourceBasis==='gross'?'com IVA':'base por classificar'}`;
const allocation={taxable_only:row.deductionLink?'exclusiva à operação ligada':'exclusiva a operações tributadas',mixed:'mista',no_right:'sem direito à dedução','':'por indicar'}[row.allocation];
out.push(['IVA mensal',`Aquisição ${row.id} — campo${row.field} — ${row.period}`,`${row.description}; líquido ${r.net??'Indeterminado'}; IVA ${r.vat??'Indeterminado'}; bruto ${r.gross??'Indeterminado'}; dedutível ${r.deductible??'Indeterminado'}`,`${context}; ${row.origin==='projected'?'Projecção':'Dados efectivos declarados'}; ${source}; ${row.documentType==='invoice'?'Factura':row.documentType==='equivalent'?'Documento equivalente':'Documento por indicar'} ${row.documentRef}, ${row.documentDate}, linha ${row.documentLine||'única'}; fornecedor ${row.supplier}; afectação ${allocation}; ${r.reason}`]);
out.push(['IVA mensal',`Aquisição ${row.id} — campo${row.field} — ${row.period}`,`${row.description}; líquido ${r.net??'Indeterminado'}; IVA ${r.vat??'Indeterminado'}; bruto ${r.gross??'Indeterminado'}; dedutível ${r.deductible??'Indeterminado'}`,`${context}; ${row.origin==='projected'?'Projecção':'Dados efectivos declarados'}; ${source}; ${row.documentType==='invoice'?'Factura':row.documentType==='equivalent'?'Documento equivalente':row.documentType==='customs'?'Bilhete de despacho de importação':'Documento por indicar'} ${row.documentRef}, ${row.documentDate}, linha ${row.documentLine||'única'}; fornecedor ${row.supplier}; afectação ${allocation}; ${r.reason}`]);
if(row.acquisitionTreatment==='former_exploration_exemption'||row.acquisitionTreatmentOrigin)out.push(['IVA mensal',`Continuidade14(2)(c) — ${row.id}`,row.acquisitionTreatment==='former_exploration_exemption'?'Antiga isenção eliminada; dedução indeterminada por esta referência':'Continuidade normal expressamente escolhida; montantes e documento conservados; condições de dedução da aquisição, sem aplicar antiga isenção',`${context}; ${formerExplorationSource}; origem conservada ${row.acquisitionTreatmentOrigin?.id??'former_exploration_exemption'}`]);
}
for(const period of result.periods) {
const entries=Object.entries(period.fields).sort(([a],[b])=>Number(a)-Number(b));
const declaration=result.declarations?.periods.find(p=>p.period===period.period);
const declarationOrigin=declaration?`Aquisições08–10 ${declaration.complete?'declaradas completas':'por completar'}; ausência de operações expressamente declarada nos campos ${declaration.absent.join(', ')||'nenhum'}.`:'Declarações detalhadas não recuperáveis nesta versão histórica.';
const declarationOrigin=declaration?`Aquisições08–10 ${declaration.complete?'declaradas completas':'por completar'};${declaration.importsComplete!==undefined?`importações11 ${declaration.importsComplete?'declaradas completas':'por completar'};`:''} ausência de operações expressamente declarada nos campos ${declaration.absent.join(', ')||'nenhum'}.`:'Declarações detalhadas não recuperáveis nesta versão histórica.';
out.push(['IVA mensal',`Modelo A — ${period.period} — parcelas01–13`,entries.filter(([field])=>Number(field)<=13).map(([field,value])=>`${field}: ${value??'Indeterminado'}`).join('; '),`${context}; ${declarationOrigin}`]);
out.push(['IVA mensal',`Modelo A — ${period.period} — totais14–18`,entries.filter(([field])=>Number(field)>=14).map(([field,value])=>`${field}: ${value??'Indeterminado'}`).join('; '),context]);
if(period.exactFields)out.push(['IVA mensal',`Modelo A — ${period.period} — precisão exacta`,Object.entries(period.exactFields).map(([f,v])=>`${f}: ${v?fractionLabel(v):'Indeterminado'}`).join('; '),'Fracções em MZN; apresentação decimal técnica, sem regra declarativa presumida.']);