feat(iva): add own-production exemptions and dated tax options in QA

This commit is contained in:
2026-10-24 15:31:27 +02:00
parent 7696af6814
commit fe69a16eea
9 changed files with 225 additions and 8 deletions
@@ -1,8 +1,10 @@
import { useState } from 'react';
import { emptyProductionTaxOption, type ProductionTaxOption } from '@/lib/iva-production-option';
import { modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
export function SimulationOperations({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
const [error,setError]=useState('');
const [reuseOption,setReuseOption]=useState<{rowId:string;sourceId:string}|null>(null);
let book:SalesBook;
try{book=salesBook(draft);}catch(cause){return <p role="alert">{String(cause)}</p>;}
const run=(action:()=>void)=>{try{action();setError('');}catch(cause){setError(cause instanceof Error?cause.message:String(cause));}};
@@ -26,6 +28,14 @@ export function SimulationOperations({draft,onChange}:{draft:Record<string,unkno
<label>Representação do preço<select className="block rounded border p-2" value={row.basis} onChange={e=>change(row.id,{basis:e.target.value as SalesOperation['basis']})}><option value="">Ainda desconhecida</option><option value="net">Sem IVA</option><option value="included">IVA incluído</option></select></label>
<label>Tratamento da operação<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,{treatment:e.target.value,confirmed:[]})}>{operationTreatments.map(t=><option value={t.id} key={t.id}>{t.label}</option>)}</select></label>
</div>
{row.treatment.startsWith('production_')&&row.treatment.endsWith('_opted')&&<fieldset className="space-y-2 rounded border p-2"><legend>Opção de tributação da produção — artigo11</legend>
<p>Complete apenas as datas em falta. A opção não cessa automaticamente ao fim de cinco anos. Uma declaração de regresso deve respeitar o prazo mínimo e produz efeitos no ano seguinte.</p>
{book.rows.some(source=>source.id!==row.id&&source.taxOption)&&<label className="block">Reutilizar opção já preenchida neste dossier<select value={reuseOption?.rowId===row.id?reuseOption.sourceId:''} onChange={e=>setReuseOption(e.target.value?{rowId:row.id,sourceId:e.target.value}:null)}><option value="">Escolha a origem, se aplicável ao mesmo sujeito passivo</option>{book.rows.filter(source=>source.id!==row.id&&source.taxOption).map(source=><option key={source.id} value={source.id}>{source.description} — {source.establishment}</option>)}</select></label>}
{reuseOption?.rowId===row.id&&<div><p>Ao confirmar, substituirá o enquadramento e as datas da opção desta linha pelos da origem escolhida, conservando a data desta operação. Confirme que se aplicam ao mesmo sujeito passivo; o resultado anterior permanece no histórico.</p><button type="button" onClick={()=>run(()=>{const source=book.rows.find(r=>r.id===reuseOption.sourceId);if(!source?.taxOption)throw new Error('Origem da opção já não disponível; dados conservados.');change(row.id,{taxOption:{...structuredClone(source.taxOption),operationDate:row.taxOption?.operationDate??'',origin:{rowId:source.id,description:source.description,establishment:source.establishment}}});setReuseOption(null);})}>Confirmar reutilização da opção escolhida</button><button type="button" onClick={()=>setReuseOption(null)}>Conservar os dados desta linha</button></div>}
<label className="block">Tipo de declaração de opção<select value={row.taxOption?.mode??''} onChange={e=>change(row.id,{taxOption:{...emptyProductionTaxOption(),...row.taxOption,mode:e.target.value as ProductionTaxOption['mode']}})}><option value="">Por indicar</option><option value="annual">Opção com efeitos no ano seguinte</option><option value="startup">Opção na declaração de início da actividade</option></select></label>
<label className="block">Declaração de regresso à isenção<select value={row.taxOption?.returnStatus??''} onChange={e=>change(row.id,{taxOption:{...emptyProductionTaxOption(),...row.taxOption,returnStatus:e.target.value as ProductionTaxOption['returnStatus']}})}><option value="">Ainda desconhecido</option><option value="none">Não existe declaração de regresso</option><option value="declared">Existe declaração de regresso</option></select></label>
{([['declarationDate','Data da declaração de opção'],...(row.taxOption?.mode==='startup'?[['startupDate','Data de início da actividade']]:[]),['operationDate','Data da operação nesta linha'],...(row.taxOption?.returnStatus==='declared'||row.taxOption?.returnDeclarationDate?[['returnDeclarationDate','Data da declaração de regresso à isenção']]:[])] as Array<['declarationDate'|'startupDate'|'operationDate'|'returnDeclarationDate',string]>).map(([field,label])=><label className="block" key={field}>{label}<input type="date" value={row.taxOption?.[field]??''} onChange={e=>change(row.id,{taxOption:{...emptyProductionTaxOption(),...row.taxOption,[field]:e.target.value}})}/></label>)}
</fieldset>}
<p>{rule?.article}; {rule?.source}. Aplicação nesta versão: Janeiro–Dezembro2026, sujeita às condições seguintes.</p>
{rule?.conditions.map((condition,index)=><label className="block" key={index}><input type="checkbox" checked={row.confirmed.includes(index)} onChange={e=>change(row.id,{confirmed:e.target.checked?[...row.confirmed,index]:row.confirmed.filter(i=>i!==index)})}/> {condition}</label>)}
<label className="block">Documento ou pressuposto que sustenta estas condições<textarea className="block w-full rounded border p-2" value={row.evidence} onChange={e=>change(row.id,{evidence:e.target.value})}/></label>
+13 -3
View File
@@ -1,4 +1,5 @@
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
import { productionOptionCheck, productionOptionVersion, type ProductionTaxOption } from './iva-production-option.ts';
import { monthlySalesFromDraft, readMonthlyIvaSettings } from './monthly-iva.ts';
import { addFractions, decimalFraction, fraction, fractionLabel, fractionPresentation, type FiscalFraction } from './fiscal-rational.ts';
@@ -12,7 +13,7 @@ const source2025 = 'Lei10/2025, BR248, suplemento1, PDF14–15 (2180(14)–(15))
const healthGoodsSource = 'CIVA republicado Lei13/2016, PDF9, p.1426(267), art.9(1)(b–f); PDF15, p.1426(273), art.19(1); Lei22/2022 art.1, PDF1, p.2252(25), mantém9(1)(b–f). Âmbito técnico2026/MZN';
const internalOperation = 'Operação interna localizada em Moçambique; não é importação nem operação localizada no estrangeiro';
export const modeloA06Correspondence = 'Exemplar recebido:06 é base de operações isentas, com remissão18(1)(b); nessa lei18(1)(b) trata IVA de importações, enquanto19(1)(b) enumera operações com direito. O mesmo exemplar já tem05 para vendas isentas com direito. O formulário antigo da AT usa03 para a remissão e06 para existências. Não há correspondência unívoca nem instrução aplicável que separe05/06 no acervo conferido: não atribuir06 automaticamente a operações no estrangeiro ou importar instruções antigas por número.';
export type OperationTreatment = {id:string; label:string; field:string|null; rate:number|null; article:string; source:string; conditions:string[]};
export type OperationTreatment = {id:string; label:string; field:string|null; rate:number|null; article:string; source:string; conditions:string[];logicVersion?:string};
const nonprofitConditions = [
'Não distribui nem disponibiliza lucros; os corpos gerentes não têm interesse directo ou indirecto nos resultados, por si ou interposta pessoa',
'Escrituração abrange todas as actividades e está à disposição dos serviços fiscais para comprovação',
@@ -53,6 +54,13 @@ const listedGoodsTreatments:OperationTreatment[] = [
['insecticides','Insecticidas','Bem transmitido identificado como insecticida; não qualquer produto químico'],
].map(([id,label,condition])=>({id:`listed_${id}`,label:`Lista legal — ${label}`,field:'05',rate:0,article:'CIVA arts.9(10),19(1)(b)(v), redacção Lei22/2022',source:'Lei22/2022 art.1, PDF2, p.2252(26),9(10); CIVA republicado Lei13/2016, PDF15, p.1426(273),19(1)(b)(v). Âmbito técnico2026/MZN',conditions:[internalOperation,'Transmissão do próprio bem enumerado, identificado; não um serviço autónomo nem extensão por sector, marca ou destino',condition]}));
export const operationTreatments:OperationTreatment[] = [
...(['agriculture','forestry','livestock','fishing'] as const).flatMap((kind,index)=>{
const label=['Produção agrícola, avicultura e apicultura','Produção silvícola','Produção pecuária','Pesca'][index],letter=['a','b','c','d'][index];
const conditions=[internalOperation,'Operação no âmbito da própria produção identificada, utilizando os próprios recursos; não mera revenda/comércio','Se houver transformação, é acessória, pelo próprio produtor sobre produtos da sua produção, por meios geralmente utilizados nas respectivas explorações'];
const source=`CIVA republicado Lei13/2016, PDF10, p.1426(268),9(7)(${letter}); PDF11, p.1426(269),11; PDF15, p.1426(273),19(1)(b)(v); Lei22/2022 art.1, PDF2, p.2252(26), mantém9(7). Âmbito técnico2026/MZN`;
const exempt:OperationTreatment={id:`production_${kind}`,label:`${label} — isenção`,field:kind==='agriculture'?'05':'07',rate:0,article:`CIVA arts.9(7)(${letter}),19(1)${kind==='agriculture'?'(b)(v)':',11'}`,source,conditions:[...conditions,...(kind==='agriculture'?[]:['Regime de isenção aplicável nesta operação: não existe opção de tributação eficaz do art.11 no período; fundamento/histórico identificado'])]};
return kind==='agriculture'?[exempt]:[exempt,{id:`production_${kind}_opted`,logicVersion:productionOptionVersion,label:`${label} — opção de tributação eficaz`,field:'01',rate:16,article:`CIVA arts.9(7)(${letter}),11,15(1),17(1),19(1)(a),34`,source:`${source}; ${source2022}; ${source2025}`,conditions:[...conditions,'Opção declarada à Direcção da Área Fiscal competente, na declaração adequada; se opção no início, consta da declaração de início da actividade','Operação no regime normal por opção eficaz; contraprestação completa, sem base especial do art.15(2) ou outro tratamento próprio']}];
}),
...listedGoodsTreatments,
{...socialTreatment('public_security','Bens e serviços ligados à segurança por entidade pública','b',['Transmissão de bens ou prestação ligada à segurança identificada e efectuada pela própria entidade pública; não presumir segurança social nem extensão a prestador privado contratado'],false),source:'CIVA republicado Lei13/2016, PDF9, p.1426(267),9(2)(b); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF1, p.2252(25), mantém9(2)(b); PDF2, p.2252(26),10(1). Âmbito técnico2026/MZN'},
{id:'official_military_supplies',label:'Fornecimentos oficiais militares/paramilitares elegíveis',field:'07',rate:0,article:'CIVA arts.9(11),19(1)',source:'CIVA republicado Lei13/2016, PDF10, p.1426(268),9(11); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF2, p.2252(26), mantém9(11). Âmbito técnico2026/MZN',conditions:[internalOperation,'Bem/serviço no fornecimento de material de guerra e de aquartelamento ou fardamento militar/paramilitar, identificado','Destino de utilização oficial das Forças de Defesa e Segurança Nacional; actividade efectuada exclusivamente para aqueles serviços','Estabelecimento reconhecido pelo Ministério competente, com reconhecimento identificado; não basta o cliente ser militar']},
@@ -100,7 +108,7 @@ export const operationTreatments:OperationTreatment[] = [
{id:'foreign_operation',label:'Operação no estrangeiro — analisar territorialidade',field:null,rate:null,article:'CIVA art.6: localização por natureza da operação; art.19(1)(b)(ii) não determina sozinho a caixa06 do formulário',source:source2016,conditions:[]},
{id:'other',label:'Outra operação / enquadramento ainda desconhecido',field:null,rate:null,article:'Fundamento específico ainda necessário; não presumir taxa ou isenção',source:'Sem fonte operacional determinada para esta linha',conditions:[]},
];
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string;taxOption?:ProductionTaxOption; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
export type SalesBook = {currency:string;rows:SalesOperation[];periods:Array<{period:string;complete:boolean}>; result?:SalesResult; history?:SalesResult[]};
export type SalesLineResult = {id:string;field:string|null;net:string|null;vat:string|null;gross:string|null;rate:number|null;status:'calculated'|'exempt'|'indeterminate';reason:string;article:string;source:string;exact?:{net:FiscalFraction;vat:FiscalFraction;gross:FiscalFraction}};
export type SalesResult = {version:string;revision:number;calculatedAt:string;opportunity:string;location:string;currency:string;key:string;inputs:SalesOperation[];rows:SalesLineResult[];periods:Array<{period:string;fields:Record<string,string|null>;exactFields?:Record<string,FiscalFraction|null>;reasons:string[]}>;rounding:string};
@@ -148,6 +156,8 @@ export function evaluateSalesOperation(row:SalesOperation,currency:string):Sales
if(!row.description.trim()||!row.establishment.trim())return stop('Indique a operação e o estabelecimento a que pertence; não inferidos da localização do investimento.');
if(rule.conditions.some((_,i)=>!row.confirmed.includes(i))||!row.evidence.trim())return stop('Condições da operação ou fundamento/pressuposto em falta; o sector não prova o tratamento fiscal.');
if(row.sourceAmount!==null&&amount(row.sourceAmount)!==n)return stop('Divergência com o valor reutilizado: conserve a origem e prepare a repartição explicitamente; não substituir silenciosamente.');
const option=row.treatment.startsWith('production_')&&row.treatment.endsWith('_opted')?productionOptionCheck(row.taxOption,row.period):{};
if(option.error)return stop(option.error);
if(row.basis==='included'&&rule.rate===5){
const exact={net:fraction(n*20n,21000000n),vat:fraction(n,21000000n),gross:fraction(n,1000000n)};
return {...result,net:fractionPresentation(exact.net).decimal,vat:fractionPresentation(exact.vat).decimal,gross:money(n),exact,status:'calculated',reason:'Base e IVA conservados como fracções exactas (preço×20/21 e preço/21). Decimal a6casas é apresentação técnica, metade para cima; não é regra de arredondamento da declaração nem imposto final a pagar.'};
@@ -155,7 +165,7 @@ export function evaluateSalesOperation(row:SalesOperation,currency:string):Sales
let net=n,vat=0n;
if(rule.rate===16&&row.basis==='included') {net=((n*100n+58000000n)/116000000n)*1000000n;vat=n-net;if(vat<0n)return stop('Arredondamento à unidade produz IVA negativo para este montante; reveja a linha e a documentação.');}
else vat=(n*BigInt(rule.rate)+50n)/100n;
return {...result,net:money(net),vat:money(vat),gross:money(row.basis==='included'?n:net+vat),status:rule.rate===0?'exempt':'calculated',reason:rule.rate===0?(rule.field==='05'?'Isenção com direito à dedução nas condições legais; não é taxa zero nem dedução automática de compras.':'Isenção sem direito à dedução para esta operação; não é isenção geral da entidade.'):'IVA da operação projectada; não é imposto final a pagar nem declaração submetida.'};
return {...result,net:money(net),vat:money(vat),gross:money(row.basis==='included'?n:net+vat),status:rule.rate===0?'exempt':'calculated',reason:(rule.rate===0?(rule.field==='05'?'Isenção com direito à dedução nas condições legais; não é taxa zero nem dedução automática de compras.':'Isenção sem direito à dedução para esta operação; não é isenção geral da entidade.'):'IVA da operação projectada; não é imposto final a pagar nem declaração submetida.')+(option.summary?' '+option.summary:'')};
}
function key(draft:Record<string,unknown>,book:SalesBook,catalog=operationTreatments.filter(t=>book.rows.some(r=>r.treatment===t.id))):string {return JSON.stringify([operationVersion,catalog,acquisitionScope(draft),draft.currency,draft.province_id,draft.district_id,draft.business_plan_snapshot,draft.cashflow_snapshot,draft.monthly_iva_settings,book.currency,book.rows,book.periods]);}
export function salesCurrent(draft:Record<string,unknown>,book:SalesBook):boolean {return !!book.result&&book.result.version===operationVersion&&book.result.opportunity===acquisitionScope(draft)&&(book.result.key===key(draft,book)||book.result.key===key(draft,book,operationTreatments.filter(t=>legacyV2TreatmentIds.includes(t.id))));}
+26
View File
@@ -0,0 +1,26 @@
export type ProductionTaxOption = {mode:''|'annual'|'startup';declarationDate:string;startupDate:string;returnStatus:''|'none'|'declared';returnDeclarationDate:string;operationDate:string;origin?:{rowId:string;description:string;establishment:string}};
export const productionOptionVersion='production-option/1';
export const emptyProductionTaxOption = ():ProductionTaxOption=>({mode:'',declarationDate:'',startupDate:'',returnStatus:'',returnDeclarationDate:'',operationDate:''});
export function productionOptionCheck(option:ProductionTaxOption|undefined,period:string):{error?:string;summary?:string} {
const valid=(value:unknown):value is string=>typeof value==='string'&&/^\d{4}-\d{2}-\d{2}$/.test(value)&&Number(value.slice(0,4))>=1&&Number.isFinite(new Date(value+'T00:00:00Z').valueOf())&&new Date(value+'T00:00:00Z').toISOString().slice(0,10)===value;
if(!option||!['annual','startup'].includes(option.mode)||!valid(option.declarationDate)||!valid(option.operationDate))return {error:'Indique o tipo/data da declaração de opção e a data da operação; não se presume renúncia eficaz.'};
if(option.operationDate.slice(0,7)!==period)return {error:'A data da operação deve pertencer ao período desta linha; não repartir montantes automaticamente.'};
let effective=String(Number(option.declarationDate.slice(0,4))+1).padStart(4,'0')+'-01-01';
if(option.mode==='startup'){
if(!valid(option.startupDate)||option.declarationDate>option.startupDate)return {error:'Na opção constante da declaração de início, indique a data de início e uma declaração que não lhe seja posterior.'};
effective=option.startupDate;
}
if(option.operationDate<effective)return {error:`Opção ainda sem efeitos nesta operação; início de efeitos ${effective}.`};
if(!['none','declared'].includes(option.returnStatus))return {error:'Indique expressamente se existe declaração de regresso à isenção; informação desconhecida não significa ausência de declaração.'};
if(option.returnStatus==='none'&&option.returnDeclarationDate)return {error:'Foi indicada ausência de declaração de regresso, mas existe uma data guardada; resolva a divergência explicitamente.'};
let end='';
if(option.returnStatus==='declared'){
if(!valid(option.returnDeclarationDate))return {error:'Data da declaração de regresso à isenção em falta ou inválida; não presumir uma data.'};
const anniversary=String(Number(effective.slice(0,4))+5).padStart(4,'0')+effective.slice(4);
if(effective.endsWith('-02-29')&&option.returnDeclarationDate===anniversary.slice(0,4)+'-02-28')return {error:'Regresso no aniversário de cinco anos de início em29/02: o acervo conferido não define este caso de calendário; não presumir a data limite.'};
if(option.returnDeclarationDate<anniversary)return {error:'A declaração de regresso indicada antecede o período mínimo de cinco anos; reveja o histórico, sem presumir regresso eficaz.'};
end=String(Number(option.returnDeclarationDate.slice(0,4))+1).padStart(4,'0')+'-01-01';
if(option.operationDate>=end)return {error:`Opção já cessou na data desta operação; regresso à isenção com efeitos ${end}. Reveja o tratamento.`};
}
return {summary:`Art.11 (${productionOptionVersion}): declaração ${option.declarationDate} (${option.mode==='startup'?'opção no início da actividade':'opção anual'}); efeitos ${effective}; operação ${option.operationDate}; ${end?`declaração de regresso ${option.returnDeclarationDate}, efeitos ${end}`:'ausência de declaração de regresso expressamente indicada; tributação continua, sem caducidade automática ao fim de cinco anos'}.${option.origin?` Opção reutilizada de ${option.origin.description} (${option.origin.rowId}), estabelecimento ${option.origin.establishment}; escolha confirmada nesta linha.`:''}`};
}