35 lines
17 KiB
TypeScript
35 lines
17 KiB
TypeScript
import {SimulationIrpcMrfta} from '@/components/simulation-irpc-mrfta';
|
||
import {irpcReliefRules,newIrpcReliefPreparation,prepareIrpcRelief} from '@/lib/irpc-relief-preparation';
|
||
import {SimulationFxSelection} from '@/components/simulation-fx-selection';
|
||
import {irpcFxSources,irpcFxRow,updateIrpcFx,type IrpcFxAmount} from '@/lib/irpc-fx';
|
||
import {useState} from 'react';
|
||
import {irpcBook,saveIrpcBook,reuseIrpcSources,irpcSources,newIrpcRow,recordIrpc,irpcKinds,emptyIrpcOrdinaryFacts,irpcReportRows,irpcPreparationHtml,type IrpcBook,type IrpcRow,type IrpcEvent,type IrpcPayment,type IrpcOrdinaryFacts} from '@/lib/irpc-withholdings';
|
||
export function SimulationIrpcWithholdings({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
|
||
const[error,setError]=useState('');if(draft.fiscal_component!=='irpc'&&draft.fiscal_component!=='withholding')return null;
|
||
try{const book=irpcBook(draft),sources=irpcSources(draft),save=(b:IrpcBook)=>{try{onChange('modelo_a_snapshot',saveIrpcBook(draft,b));setError('');}catch(e){setError(String(e));}},change=(id:string,p:Partial<IrpcRow>)=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...p}:r)}),uid=()=>crypto.randomUUID();
|
||
const field=(r:IrpcRow,k:keyof IrpcRow,label:string,type='text')=><label className="block">{label}<input className="block rounded border p-2" type={type} value={String(r[k]??'')} onChange={e=>change(r.id,{[k]:e.target.value})}/></label>;
|
||
const select=(r:IrpcRow,k:keyof IrpcRow,label:string,options:Array<[string,string]>)=><label className="block">{label}<select className="block rounded border p-2" value={String(r[k]??'')} onChange={e=>change(r.id,{[k]:e.target.value})}>{options.map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label>;
|
||
const yn:Array<[string,string]>=[['','Por esclarecer'],['yes','Sim'],['no','Não']];
|
||
const eventLabels:Array<[IrpcEvent['kind'],string]>=[['cost','Reconhecimento do custo'],['payment','Pagamento do rendimento'],['maturity','Vencimento, ainda que presumido'],['availability','Colocação à disposição'],['liquidation','Liquidação'],['quantification','Apuramento do quantitativo']];
|
||
const fxSources=irpcFxSources(draft);
|
||
const renderFx=(id:string,input:IrpcFxAmount,apply:(patch:Partial<IrpcFxAmount>)=>void)=><div className="space-y-2"><p>A base/comprovativo conserva a moeda e o montante originais. A conversão reutiliza o selector e a aritmética entregues; a data cambial fica distinta do evento da retenção.</p>{fxSources.map(src=><div key={src.id}><span>{src.label};data:{src.selection.applied?.date||'por confirmar'};selecção actual:{input.fxSelection?.applied?.rate||'nenhuma'}.</span><button type="button" className="rounded border p-2" onClick={()=>{if(window.confirm(`Substituir a selecção cambial desta linha (${input.fxSelection?.applied?.rate||'vazia'}) pela origem ${src.label},data ${src.selection.applied?.date||'por confirmar'}? O montante e a moeda originais são conservados;resultado anterior permanece no histórico até novo cálculo.`))apply({fxSelection:structuredClone(src.selection),fxLink:{id:src.id,fingerprint:src.fingerprint}});}}>Reutilizar selecção cambial {src.id}</button></div>)}<SimulationFxSelection row={irpcFxRow(id,input)} onChange={patch=>{if(patch.fxSelection)apply(updateIrpcFx(input,patch.fxSelection));}}/></div>;
|
||
const ordinarySelect=(r:IrpcRow,k:keyof IrpcOrdinaryFacts,label:string,options:Array<[string,string]>)=><label className="block">{label}<select value={String((r.ordinary??emptyIrpcOrdinaryFacts())[k]??'')} onChange={e=>change(r.id,{ordinary:{...(r.ordinary??emptyIrpcOrdinaryFacts()),[k]:e.target.value}})}>{options.map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label>;
|
||
const paymentChange=(id:string,p:Partial<IrpcPayment>)=>save({...book,payments:book.payments.map(v=>v.id===id?{...v,...p}:v)});
|
||
return<section aria-label="Retenções de IRPC" className="space-y-4 rounded border p-4"><h4>IRPC — retenções e preparação documental</h4><p>Reutilize documentos e contrapartes existentes. Classifique o rendimento e o evento aplicável:facturar,pagar e reconhecer o custo são factos distintos. O conjunto calcula os casos digitais de 10%, as taxas normais de 20% e as taxas especiais do artigo 62(4) nos âmbitos identificados. Dispensas dos artigos 68(d/f) e limite convencional de royalties Portugal–Moçambique são calculados nos casos documentados. Informação insuficiente condiciona apenas o respectivo rendimento.</p>{error&&<p role="alert">{error}</p>}
|
||
<button type="button" className="rounded border p-2" onClick={()=>{try{save(reuseIrpcSources(draft,book));}catch(e){setError(String(e));}}}>Reutilizar contrapartes e documentos existentes</button><button type="button" className="rounded border p-2" onClick={()=>save({...book,rows:[...book.rows,newIrpcRow(uid())]})}>Acrescentar rendimento IRPC</button>
|
||
{book.rows.map(r=><fieldset key={r.id} className="space-y-3 rounded border p-3"><legend>{r.description||r.documentRef||'Rendimento por identificar'}</legend>{r.source&&<p>Origem:{r.source.id}. Entradas deliberadamente editadas são conservadas. {sources.find(s=>s.id===r.source!.id)?.key!==r.source.key&&<><strong>Origem alterada ou indisponível.</strong>{sources.find(s=>s.id===r.source!.id)&&<><span> Base actual:{r.base};base na origem:{sources.find(s=>s.id===r.source!.id)?.amount||'desconhecida'};documento actual:{r.documentRef};origem:{sources.find(s=>s.id===r.source!.id)?.documentRef||'desconhecido'}.</span><button type="button" className="rounded border p-2" onClick={()=>{const s=sources.find(s=>s.id===r.source!.id)!;if(window.confirm('Confirma a reconciliação da origem indicada, conservando as entradas deliberadas e os resultados anteriores no histórico?'))change(r.id,{source:{...r.source!,key:s.key}});}}>Conservar edições e reconciliar origem</button></>}</>}</p>}
|
||
<div className="grid gap-3 md:grid-cols-2">{select(r,'direction','Perspectiva',[['paid','Rendimento pago — retenção a entregar'],['received','Rendimento recebido — retenção sofrida']])}{field(r,'counterparty','Contraparte')}{field(r,'documentRef','Documento / operação / linha distinta')}{field(r,'description','Descrição')}{select(r,'origin','Origem',[['projected','Projecção com pressupostos'],['effective','Factos documentados']])}{select(r,'kind','Natureza do rendimento',irpcKinds)}{field(r,'base','Base do rendimento, separada do IVA')}{field(r,'currency','Moeda da base')}{select(r,'recipientIrpc','Titular sujeito a IRPC neste rendimento',yn)}{select(r,'payerIrpc','Devedor sujeito a IRPC',yn)}{select(r,'residence','Residência do titular',[['','Por esclarecer'],['resident','Residente'],['nonresident','Não residente']])}{r.residence==='nonresident'&&<>{select(r,'permanentEstablishment','Existe estabelecimento estável em Moçambique',yn)}{r.permanentEstablishment==='yes'&&select(r,'attributable','Rendimento imputável ao estabelecimento',yn)}</>}{select(r,'mozambique','Rendimento obtido em Moçambique, conforme qualificação legal',yn)}{field(r,'qualificationRef','Suporte / pressuposto da qualificação, residência e incidência')}{select(r,'specialRelief','Convenção, isenção ou dispensa aplicável',[['','Por esclarecer'],['none','Não invocada neste caso qualificado'],['claimed','Invocada — identificar regra e condições']])}{r.specialRelief==='claimed'&&field(r,'reliefRef','Artigo, condições, período e prova invocados')}</div>
|
||
{r.specialRelief==='claimed'&&<fieldset className="space-y-2 rounded border p-2"><legend>Preparação de dispensa ou convenção</legend><p>Reutiliza contraparte, residência, estabelecimento estável e documento deste rendimento.</p><label>Enquadramento<select value={r.reliefPreparation?.kind??'dispensation'} onChange={e=>change(r.id,{reliefPreparation:{...(r.reliefPreparation??newIrpcReliefPreparation()),kind:e.target.value as 'dispensation'|'exemption'|'treaty'}})}><option value="dispensation">Dispensa de retenção</option><option value="exemption">Isenção</option><option value="treaty">Convenção</option></select></label>{(['instrument','article','country','residenceDocument','validFrom','validTo','conditions','evidence'] as const).filter(k=>!['country','residenceDocument'].includes(k)||r.reliefPreparation?.kind==='treaty').map(k=><label key={k} className="block">{({instrument:'Diploma / convenção',article:'Artigo',country:'Estado contratante',residenceDocument:'Referência do documento de residência fiscal',validFrom:'Aplicável desde',validTo:'Aplicável até',conditions:'Condições do benefício',evidence:'Referências dos documentos existentes'})[k]}<input type={k==='validFrom'||k==='validTo'?'date':'text'} value={r.reliefPreparation?.[k]??''} onChange={e=>change(r.id,{reliefPreparation:{...(r.reliefPreparation??newIrpcReliefPreparation()),[k]:e.target.value}})}/></label>)}<label>Regra aplicável<select value={r.reliefPreparation?.rule??''} onChange={e=>change(r.id,{reliefPreparation:{...(r.reliefPreparation??newIrpcReliefPreparation()),rule:e.target.value as NonNullable<IrpcRow['reliefPreparation']>['rule']}})}>{irpcReliefRules.map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label>{(r.reliefPreparation?.rule==='ptMz12'?(['treatyResident','beneficialOwner','payerResidentMz','armLength','royaltyDefinition'] as const):r.reliefPreparation?.rule==='cirpc68f'?(['subjectToIrpc','notTransparent'] as const):r.reliefPreparation?.rule==='cirpc68d'?(['subjectToIrpc'] as const):[]).map(k=><label key={k} className="block">{({subjectToIrpc:'Titular sujeito a IRPC relativamente a este rendimento (ainda que isento)',notTransparent:'Sociedade fora da transparência fiscal — 6(1)c',treatyResident:'Residência em Portugal nos termos do artigo 4 documentada',beneficialOwner:'Titular é beneficiário efectivo das royalties',payerResidentMz:'Devedor residente em Moçambique; origem do rendimento conforme 12(5)',armLength:'Montante sem excesso por relações especiais — 12(6)',royaltyDefinition:'Rendimento corresponde à definição de royalties — 12(3), versão 2009'})[k]}<select value={r.reliefPreparation?.facts?.[k]??''} onChange={e=>change(r.id,{reliefPreparation:{...(r.reliefPreparation??newIrpcReliefPreparation()),facts:{...r.reliefPreparation?.facts,[k]:e.target.value as ''|'yes'|'no'}}})}>{yn.map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label>)}<p>{prepareIrpcRelief(r,r.reliefPreparation??newIrpcReliefPreparation()).missing.length?'Faltam elementos documentais; confira artigo, período e provas.':'Documentos registados; calcule para conferir as condições da regra seleccionada.'}</p></fieldset>}
|
||
{r.currency!=='MZN'&&renderFx(r.id,r,patch=>change(r.id,patch))}
|
||
{r.kind&&!['digital_goods','digital_services','emoney_commission','other'].includes(r.kind)&&<fieldset className="space-y-2 rounded border p-2"><legend>Factos específicos do rendimento</legend>{ordinarySelect(r,'nonDigital','Rendimento fora do âmbito digital/moeda electrónica do artigo62(5)',yn)}{r.kind==='capital'&&ordinarySelect(r,'capitalType','Categoria dos capitais',[['','Por esclarecer'],['unlisted_interest','Juros não cotados, fora das permutas de liquidez'],['unlisted_dividend','Dividendos não cotados'],['listed_debt_mz','Juros de bilhetes do Tesouro/títulos de dívida cotados em bolsa —62(3)a'],['listed_other_mz','Outros títulos cotados na Bolsa de Valores de Moçambique —62(4)d'],['liquidity_repo','Juros de permutas de liquidez em bancos,com ou sem garantia —62(3)b'],['other','Outra categoria']])}{r.kind==='capital'&&r.residence==='resident'&&['listed_debt_mz','liquidity_repo'].includes(r.ordinary?.capitalType??'')&&ordinarySelect(r,'capitalSeatAndManagement','Titular com sede e direcção efectiva nacionais —62(3)',yn)}{r.kind==='rent'&&ordinarySelect(r,'rentOwnPropertyManagement','Titular é sociedade com objecto de gestão de imóveis próprios',yn)}{(['telecom','international_transport','connected_installation','aircraft_maintenance_charter','rural_public_electrification','maritime_fishing_cabotage'].includes(r.kind)||r.kind==='capital'&&r.ordinary?.capitalType==='listed_other_mz')&&ordinarySelect(r,'specialFacts','Factos exactos da categoria62(4) identificados no suporte — não apenas actividade semelhante',yn)}<label className="block">Suporte ou pressuposto específico da classificação<input value={r.ordinary?.reference??''} onChange={e=>change(r.id,{ordinary:{...(r.ordinary??emptyIrpcOrdinaryFacts()),reference:e.target.value}})}/></label></fieldset>}
|
||
<h5>Eventos aplicáveis — artigo67(5), versão2026</h5>{r.events.map((e,n)=><div key={n} className="grid gap-2 md:grid-cols-3"><label>Evento<select value={e.kind} onChange={ev=>change(r.id,{events:r.events.map((v,j)=>j===n?{...v,kind:ev.target.value as IrpcEvent['kind']}:v)})}>{eventLabels.map(([k,l])=><option key={k} value={k}>{l}</option>)}</select></label><label>Data<input type="date" value={e.date} onChange={ev=>change(r.id,{events:r.events.map((v,j)=>j===n?{...v,date:ev.target.value}:v)})}/></label><label>Suporte / pressuposto<input value={e.reference} onChange={ev=>change(r.id,{events:r.events.map((v,j)=>j===n?{...v,reference:ev.target.value}:v)})}/></label></div>)}<button type="button" onClick={()=>change(r.id,{events:[...r.events,{kind:'cost',date:'',reference:''}]})}>Acrescentar evento aplicável</button>{select(r,'eventsComplete','Todos os eventos aplicáveis identificados, incluindo custo quando aplicável',yn)}
|
||
{select(r,'settlement','Reconciliação da entrega / prova de retenção sofrida',[['','Por esclarecer'],['none','Ainda sem entrega / prova'],['complete','Pagamentos / provas associados completos']])}{field(r,'settlementRef','Referência da situação declarada')}
|
||
{r.allocations.map((a,n)=><div key={n}><label>Pagamento / prova existente<select value={a.paymentId} onChange={e=>change(r.id,{allocations:r.allocations.map((v,j)=>j===n?{...v,paymentId:e.target.value}:v)})}><option value="">Seleccione</option>{book.payments.map(p=><option key={p.id} value={p.id}>{p.reference} — {p.amount} {p.currency} — {p.kind}</option>)}</select></label><label>Parcela usada em MZN<input value={a.amount} onChange={e=>change(r.id,{allocations:r.allocations.map((v,j)=>j===n?{...v,amount:e.target.value}:v)})}/></label></div>)}{r.settlement==='complete'&&<button type="button" onClick={()=>change(r.id,{allocations:[...r.allocations,{paymentId:'',amount:''}]})}>Associar pagamento/prova existente</button>}
|
||
</fieldset>)}
|
||
<h5>Pagamentos e comprovativos reutilizáveis neste livro</h5><p>Uma referência é registada uma vez e repartida entre rendimentos sem exceder o montante. Pagamento ao fornecedor não é entrega à AT; pagamentos de IVA conservam o seu vínculo anterior e não são automaticamente usados em IRPC.</p>{book.payments.map(p=><fieldset key={p.id} className="grid gap-2 rounded border p-2 md:grid-cols-3"><label>Natureza<select value={p.kind} onChange={e=>paymentChange(p.id,{kind:e.target.value as IrpcPayment['kind']})}><option value="remittance">Entrega IRPC à AT</option><option value="withholding_proof">Comprovativo de retenção sofrida</option><option value="beneficiary">Pagamento ao beneficiário</option></select></label>{(['reference','date','amount','currency']as const).map(k=><label key={k}>{({reference:'Referência',date:'Data',amount:'Montante',currency:'Moeda'})[k]}<input type={k==='date'?'date':'text'} value={p[k]} onChange={e=>paymentChange(p.id,{[k]:e.target.value})}/></label>)}{p.currency!=='MZN'&&(p.kind==='withholding_proof'?renderFx(p.id,p,patch=>paymentChange(p.id,patch)):<p>A entrega IRPC à AT requer o montante MZN do comprovativo; converter o pagamento ao beneficiário não prova entrega do imposto.</p>)}</fieldset>)}<button type="button" onClick={()=>save({...book,payments:[...book.payments,{id:uid(),kind:'remittance',reference:'',date:'',amount:'',currency:'MZN'}]})}>Registar pagamento/prova para reutilização</button>
|
||
<div className="space-x-3"><button type="button" className="rounded border p-2" onClick={()=>{try{save(recordIrpc(draft,book));}catch(e){setError(String(e));}}}>Calcular e guardar retenções IRPC</button><button type="button" className="rounded border p-2" onClick={()=>{try{const html=irpcPreparationHtml(draft),u=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'})),a=document.createElement('a');a.href=u;a.download='IRPC-L5-preparacao.html';a.click();URL.revokeObjectURL(u);setError('');}catch(e){setError(String(e));}}}>Exportar preparação IRPC</button></div>
|
||
<SimulationIrpcMrfta draft={draft} book={book} save={save}/>
|
||
{irpcReportRows(draft).slice(0,book.result?.rows.length??0).map((r,n)=><p key={n}><strong>{r[1]}</strong>: {r[2]}<small className="block">{r[3]}</small></p>)}<p>Formalidades externas:MRFTA19/39 e guia M/39 preservados. CER/rubricas digitais e remissão67(7)→62(6) por corresponder. Exportar não submete nem obtém aceitação AT. L5 continua aberto;ISPC e IRPS conservam os seus limites anteriores.</p></section>;
|
||
}catch(e){return<p role="alert">{String(e)}</p>;}
|
||
}
|