Specify AT interest and CER questions and label simulation assumptions
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-25 21:04:53 +02:00
parent acf8ec3cb5
commit 7a11c4ef8f
8 changed files with 77 additions and 8 deletions
@@ -16,14 +16,14 @@ export function SimulationIrpcMrfta({draft,book,save}:{draft:Record<string,unkno
return <fieldset aria-label="Preparação MRFTA e juros" className="space-y-3 rounded border p-4"><legend>MRFTA — preenchimento e juros compensatórios</legend><p>Reutiliza o cálculo guardado, documentos e entregas IRPC. Um mês/ano por declaração. A retenção sofrida e as dispensas ficam separadas. Quadros 8/9 reservados à assinatura e aos serviços; não há submissão ou aceitação AT.</p>
{identities.map(([scope,b])=><button key={scope} type="button" onClick={()=>{const p:Partial<MrftaInput>={};for(const key of ['nuit','name','commercialName','taxArea','address','contact']as const)if(!i[key].trim())p[key]=b.mrfta![key];change(p);}}>Reutilizar identidade fiscal {b.mrfta!.name} ({scope}) nos campos vazios</button>)}
<div className="grid gap-3 md:grid-cols-2">{field('period','Quadro 2 — mês/ano','month')}<label>Período existente<select value="" onChange={e=>{if(e.target.value)change({period:e.target.value});}}><option value="">Seleccione</option>{periods.map(p=><option key={p} value={p}>{p}</option>)}</select></label>{select('declaration','Quadro 1 — retenção na fonte',[['','Por identificar'],['initial','Inicial'],['replacement','Substituição']])}{select('timing','Quadro 2 — situação do prazo',[['','Por esclarecer'],['within','Dentro do prazo'],['late','Fora do prazo']])}{field('nuit','Quadro 3 — NUIT')}{field('taxArea','Quadro 3 — área fiscal')}{field('name','Quadro 4 — nome do sujeito passivo')}{field('commercialName','Quadro 5 — nome comercial')}{field('address','Quadro 6 — domicílio fiscal')}{field('contact','Quadro 6 — contacto')}{select('completeness','Retenções do sujeito passivo neste período conferidas, incluindo demais livros',[['','Por esclarecer'],['yes','Sim'],['no','Ainda não']])}{field('completenessRef','Referência da reconciliação do período')}{select('otherRubrics','IRPS, tributação autónoma e outras rubricas fora deste recorte',[['','Por esclarecer'],['none','Ausência de outros valores devidos documentada'],['pending','Existem ou estão por apurar']])}{field('otherRubricsRef','Documento da situação das outras rubricas')}</div>
<h5>Quadro 7 — CER das rubricas correspondidas</h5><p>Base, taxa e imposto vêm do cálculo guardado. O CER é transcrito da referência aplicável; os números de campos da guia antiga não são automaticamente códigos de receita.</p>
<h5>Quadro 7 — CER das rubricas correspondidas</h5><p>Base, taxa e imposto vêm do cálculo guardado. O CER é transcrito da referência aplicável; os números de campos da guia antiga não são automaticamente códigos de receita. Códigos introduzidos não são validados por catálogo oficial; códigos provisórios ou de simulação não são correspondências oficiais.</p>
{book.result?.inputs.filter(r=>r.direction==='paid'&&book.result?.rows.some(v=>v.id===r.id&&v.period===i.period&&v.status==='calculated'&&v.treatment!=='dispensed')).map(r=>{const code=i.codes.find(c=>c.rowId===r.id)??{rowId:r.id,cer:'',reference:''};const update=(p:Partial<typeof code>)=>change({codes:[...i.codes.filter(c=>c.rowId!==r.id),{...code,...p}]});return <div key={r.id} className="grid gap-2 md:grid-cols-3"><span>{r.documentRef} — {r.counterparty}</span><label>CER<input value={code.cer} onChange={e=>update({cer:e.target.value})}/></label><label>Documento / referência do CER<input value={code.reference} onChange={e=>update({reference:e.target.value})}/></label></div>;})}
<h5>Juros compensatórios — separados do principal</h5><p>Regulamento IRPC, artigos 25/35: a taxa refere MAIBOR de 12 meses + 2 pontos. O Banco de Moçambique documenta a descontinuidade da MAIBOR. O portal não adopta MIMO ou Prime como substituto nem calcula juros de mora.</p>
<label>Tratamento dos juros<select value={interestMode} onChange={e=>interest(e.target.value==='automatic'?{mode:'',calculation:'automatic'}:{mode:e.target.value as MrftaInput['interest']['mode'],calculation:undefined})}><option value="">Informação insuficiente / cálculo automático por resolver</option><option value="assessment">Transcrever liquidação documentada de juros compensatórios</option><option value="automatic">Cálculo com taxa e regra documentadas — sem parâmetro pré-definido</option><option value="timely">Conferir entrega integral dentro do prazo documentado</option></select></label>
{interestMode&&<><label>Período documentado dos juros / entregas<input type="month" value={i.interest.period} onChange={e=>interest({period:e.target.value})}/></label><label>Documento da liquidação / reconciliação<input value={i.interest.reference} onChange={e=>interest({reference:e.target.value})}/></label></>}
{['assessment','automatic'].includes(interestMode)&&<>{i.interest.mode==='assessment'&&<label>Montante dos juros em MZN<input value={i.interest.amount} onChange={e=>interest({amount:e.target.value})}/></label>}<label>Data da liquidação<input type="date" value={i.interest.date} onChange={e=>interest({date:e.target.value})}/></label></>}
{['timely','automatic'].includes(interestMode)&&<><label>Prazo aplicável documentado<input type="date" value={i.interest.dueDate} onChange={e=>interest({dueDate:e.target.value})}/></label><label>Fonte do prazo, incluindo eventual ajuste de calendário<input value={i.interest.dueReference} onChange={e=>interest({dueReference:e.target.value})}/></label></>}
{interestMode==='automatic'&&<fieldset className="space-y-2 rounded border p-2"><legend>Parâmetros documentados do cálculo — sem taxa fiscal de2026 pré-definida</legend><p>Preencha apenas a regra aplicável ao caso. Sem parâmetro completo, os juros continuam indeterminados. A taxa bancária actual não é adoptada por substituição. Imposto por conta não retido e juros de mora mantêm regra própria.</p>{(['annualRate','rateSource','fiscalBasis','validFrom','validTo','dayBasisRef','roundingRef','occurrenceRef']as const).map(k=><label key={k} className="block">{({annualRate:'Taxa anual final (%) documentada, incluindo acréscimos aplicáveis',rateSource:'Documento e citação da taxa',fiscalBasis:'Fundamento da aplicação fiscal ao caso',validFrom:'Parâmetro aplicável desde',validTo:'Parâmetro aplicável até',dayBasisRef:'Documento da base anual e contagem diária',roundingRef:'Documento do arredondamento do total',occurrenceRef:'Documento da retenção e ocorrência'})[k]}<input type={k==='validFrom'||k==='validTo'?'date':'text'} value={i.interest.automatic?.[k]??''} onChange={e=>interest({automatic:{...(i.interest.automatic??newIrpcInterestRule()),[k]:e.target.value}})}/></label>)}{(['dayBasis','rounding','retention']as const).map(k=><label key={k} className="block">{({dayBasis:'Base anual conforme documento',rounding:'Arredondamento do total conforme documento',retention:'Retenção efectuada'})[k]}<select value={i.interest.automatic?.[k]??''} onChange={e=>interest({automatic:{...(i.interest.automatic??newIrpcInterestRule()),[k]:e.target.value} as IrpcInterestRule})}>{(k==='dayBasis'?[['','Por identificar'],['365','365 dias'],['366','366 dias'],['actual_year','Dias efectivos de cada ano']]:k==='rounding'?[['','Por identificar'],['total_cent_half_up','Total aos cêntimos — metade por excesso'],['total_cent_down','Total aos cêntimos — por defeito']]:[['','Por identificar'],['withheld','Sim, documentada'],['not_withheld','Não efectuada, documentado']]).map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label>)}</fieldset>}
{interestMode==='automatic'&&<fieldset className="space-y-2 rounded border p-2"><legend>Parâmetros documentados do cálculo — sem taxa fiscal de2026 pré-definida</legend><p>Simulação pelos pressupostos introduzidos, sem confirmação fiscal dos juros. Preencha apenas a regra aplicável ao caso. Sem parâmetro completo, os juros continuam indeterminados. A taxa bancária actual não é adoptada por substituição. Imposto por conta não retido e juros de mora mantêm regra própria.</p>{(['annualRate','rateSource','fiscalBasis','validFrom','validTo','dayBasisRef','roundingRef','occurrenceRef']as const).map(k=><label key={k} className="block">{({annualRate:'Taxa anual final (%) documentada, incluindo acréscimos aplicáveis',rateSource:'Documento e citação da taxa',fiscalBasis:'Fundamento da aplicação fiscal ao caso',validFrom:'Parâmetro aplicável desde',validTo:'Parâmetro aplicável até',dayBasisRef:'Documento da base anual e contagem diária',roundingRef:'Documento do arredondamento do total',occurrenceRef:'Documento da retenção e ocorrência'})[k]}<input type={k==='validFrom'||k==='validTo'?'date':'text'} value={i.interest.automatic?.[k]??''} onChange={e=>interest({automatic:{...(i.interest.automatic??newIrpcInterestRule()),[k]:e.target.value}})}/></label>)}{(['dayBasis','rounding','retention']as const).map(k=><label key={k} className="block">{({dayBasis:'Base anual conforme documento',rounding:'Arredondamento do total conforme documento',retention:'Retenção efectuada'})[k]}<select value={i.interest.automatic?.[k]??''} onChange={e=>interest({automatic:{...(i.interest.automatic??newIrpcInterestRule()),[k]:e.target.value} as IrpcInterestRule})}>{(k==='dayBasis'?[['','Por identificar'],['365','365 dias'],['366','366 dias'],['actual_year','Dias efectivos de cada ano']]:k==='rounding'?[['','Por identificar'],['total_cent_half_up','Total aos cêntimos — metade por excesso'],['total_cent_down','Total aos cêntimos — por defeito']]:[['','Por identificar'],['withheld','Sim, documentada'],['not_withheld','Não efectuada, documentado']]).map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label>)}</fieldset>}
<button type="button" className="rounded border p-2" onClick={()=>{try{save(recordMrfta(book,irpcCurrent(draft,book)));setError('');}catch(e){setError(String(e));}}}>Preparar e guardar MRFTA</button><button type="button" className="rounded border p-2" onClick={()=>{try{const html=mrftaHtml(book,irpcCurrent(draft,book)),u=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'})),a=document.createElement('a');a.href=u;a.download='MRFTA-'+(i.period||'periodo')+'-preparacao.html';a.click();URL.revokeObjectURL(u);setError('');}catch(e){setError(String(e));}}}>Exportar preenchimento MRFTA</button>
{book.mrftaResult&&<fieldset className="space-y-2 rounded border p-2"><legend>Casos de trabalho separados por período, rubrica, taxa e CER</legend><p>Estas fichas conservam as pendências do período. Não são declarações autónomas, anexos oficiais ou uma autorização para fundir taxas.</p>{mrftaCases(book.mrftaResult.model).map(c=><div key={c.key}><span>{c.period}; {c.rubric}; {c.rate}%; CER:{c.cer||'por corresponder'}; documentos:{c.documents.join('; ')}.</span><button type="button" onClick={()=>{try{if(!irpcCurrent(draft,book)||!mrftaCurrent(book,true))throw Error('Reprepare as entradas actuais antes de exportar o caso.');const html=mrftaCaseHtml(book.mrftaResult!.model,c.key),u=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'})),a=document.createElement('a');a.href=u;a.download='MRFTA-caso-'+c.key+'.html';a.click();URL.revokeObjectURL(u);setError('');}catch(e){setError(String(e));}}}>Exportar ficha de trabalho {c.rate}% / {c.cer||'CER pendente'}</button></div>)}</fieldset>}
{error&&<p role="alert">{error}</p>}{mrftaReportRows(book,irpcCurrent(draft,book)).map((r,n)=><p key={n}><strong>{r[1]}</strong>: {r[2]}<small>{r[3]}</small></p>)}
+1 -1
View File
@@ -7,7 +7,7 @@ export function mrftaCases(m:MrftaModel){return m.lines.map(line=>({
key:closureFingerprint([m.period,line.rubric,line.rate,line.cer]),period:m.period,
rubric:line.rubric,rate:line.rate,cer:line.cer,ids:[...line.ids],documents:[...line.documents],references:[...line.references],
base:structuredClone(line.base),tax:structuredClone(line.tax),paid:structuredClone(line.paid),
pending:!line.cer||!/^\d+$/.test(line.cer)||!line.ids.every(id=>m.input.codes.find(c=>c.rowId===id)?.reference.trim())?'CER ou referência por corresponder;sem preenchimento presumido.':'CER transcrito com referência do caso;sem correspondência automática a catálogo integral.',
pending:!line.cer||!/^\d+$/.test(line.cer)||!line.ids.every(id=>m.input.codes.find(c=>c.rowId===id)?.reference.trim())?'CER ou referência por corresponder;sem preenchimento presumido.':'CER transcrito com referência do caso;sem validação por catálogo oficial. Código provisório ou de simulação não é correspondência oficial.',
officialSeparation:'A correspondência de linhas/anexos/declaração oficial permanece por resolver quando há taxas/CER distintos na mesma rubrica.',
}));}
export function mrftaCaseHtml(m:MrftaModel,key:string):string{
+3 -3
View File
@@ -75,7 +75,7 @@ export function prepareMrfta(b:IrpcBook&MrftaStore,current:boolean):MrftaModel{
let automatic:IrpcInterestCalculation|undefined;
if(i.interest.mode===''&&i.interest.calculation==='automatic'){
if(!i.interest.automatic||i.interest.period!==i.period||!i.interest.reference.trim())interestReason='Cálculo automático sem parâmetros/ocorrência e período documentados;juros continuam indeterminados.';
else{automatic=calculateIrpcInterest(b,current,i.period,i.interest.dueDate,i.interest.dueReference,i.interest.date,i.interest.automatic);interest=automatic.total;interestReason=automatic.pending.length?automatic.pending.join(' | '):'Juros calculados por parâmetros documentados deste caso;sem taxa fiscal2026 pré-definida. Taxa anual:'+automatic.rule.annualRate+'%;fonte:'+automatic.rule.rateSource+';fundamento:'+automatic.rule.fiscalBasis+';vigência:'+automatic.rule.validFrom+'–'+automatic.rule.validTo+';base anual:'+(automatic.rule.dayBasis==='actual_year'?'dias efectivos de cada ano':automatic.rule.dayBasis+' dias')+';fonte:'+automatic.rule.dayBasisRef+';arredondamento do total aos cêntimos:'+(automatic.rule.rounding==='total_cent_half_up'?'metade por excesso':'por defeito')+';fonte:'+automatic.rule.roundingRef+';ocorrência:'+automatic.rule.occurrenceRef+';'+automatic.rows.map(r=>r.document+':'+r.segments.map(s=>s.from+'–'+s.to+':'+s.days+'dias/'+s.denominator+';principal '+money(s.principal)+';juros '+money(s.interest)).join(',')).join(' | ');}
else{automatic=calculateIrpcInterest(b,current,i.period,i.interest.dueDate,i.interest.dueReference,i.interest.date,i.interest.automatic);interest=automatic.total;interestReason=automatic.pending.length?automatic.pending.join(' | '):'Juros calculados por parâmetros documentados deste caso — simulação pelos pressupostos introduzidos, sem confirmação fiscal;sem taxa fiscal2026 pré-definida. Taxa anual:'+automatic.rule.annualRate+'%;fonte:'+automatic.rule.rateSource+';fundamento:'+automatic.rule.fiscalBasis+';vigência:'+automatic.rule.validFrom+'–'+automatic.rule.validTo+';base anual:'+(automatic.rule.dayBasis==='actual_year'?'dias efectivos de cada ano':automatic.rule.dayBasis+' dias')+';fonte:'+automatic.rule.dayBasisRef+';arredondamento do total aos cêntimos:'+(automatic.rule.rounding==='total_cent_half_up'?'metade por excesso':'por defeito')+';fonte:'+automatic.rule.roundingRef+';ocorrência:'+automatic.rule.occurrenceRef+';'+automatic.rows.map(r=>r.document+':'+r.segments.map(s=>s.from+'–'+s.to+':'+s.days+'dias/'+s.denominator+';principal '+money(s.principal)+';juros '+money(s.interest)).join(',')).join(' | ');}
}
if(!interest)pending.push(interestReason);
let total:FiscalFraction|null=null;
@@ -87,10 +87,10 @@ export function recordMrfta(b:IrpcBook&MrftaStore,current:boolean,now=new Date()
}
const money=(f:FiscalFraction|null)=>{if(!f)return'Indeterminado';const p=fractionPresentation(f);return(p.approximate?'≈':'')+p.decimal+' MZN';};
export function mrftaReportRows(b:IrpcBook&MrftaStore,current:boolean):Array<[string,string,string,string]>{
const r=b.mrftaResult;if(!r)return[];const m=r.model;return[['IRPC — retenções','MRFTA — '+m.period,`Subtotal das retenções correspondidas:${money(m.principal)};juros:${money(m.interest)};valor já pago:${money(m.paid)};total a pagar:${money(m.total)};${m.interestReason};pendências:${m.pending.join(' | ')}`,`${mrftaCurrent(b,current)?'Preparação correspondente':'Histórico;preparação desactualizada'};${r.createdAt};completude:${m.input.completenessRef};outras rubricas:${m.input.otherRubricsRef};${m.source}`],...m.lines.map(l=>['IRPC — retenções',l.rubric,`${l.documents.join(';')};base:${money(l.base)};taxa:${l.rate}%;imposto:${money(l.tax)};CER:${l.cer||'por corresponder'}`,`Período:${m.period};apenas rendimento pago;sem assinatura ou uso dos serviços;documentos:${l.ids.join(',')};suporte:${l.references.join(' | ')}`] as [string,string,string,string])];
const r=b.mrftaResult;if(!r)return[];const m=r.model;return[['IRPC — retenções','MRFTA — '+m.period,`Subtotal das retenções correspondidas:${money(m.principal)};juros:${money(m.interest)};valor já pago:${money(m.paid)};total a pagar:${money(m.total)};${m.interestReason};pendências:${m.pending.join(' | ')}`,`${mrftaCurrent(b,current)?'Preparação correspondente':'Histórico;preparação desactualizada'};${r.createdAt};completude:${m.input.completenessRef};outras rubricas:${m.input.otherRubricsRef};${m.source}`],...m.lines.map(l=>['IRPC — retenções',l.rubric,`${l.documents.join(';')};base:${money(l.base)};taxa:${l.rate}%;imposto:${money(l.tax)};CER:${l.cer||'por corresponder'};código introduzido,sem validação por catálogo oficial`,`Período:${m.period};apenas rendimento pago;sem assinatura ou uso dos serviços;documentos:${l.ids.join(',')};suporte:${l.references.join(' | ')}`] as [string,string,string,string])];
}
export function mrftaHtml(b:IrpcBook&MrftaStore,current:boolean):string{
if(!current||!mrftaCurrent(b,current))throw Error('Calcule as retenções e prepare o MRFTA com as entradas actuais antes de exportar.');
const m=b.mrftaResult!.model,esc=(v:unknown)=>String(v).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));
return `<!doctype html><html lang="pt"><meta charset="utf-8"><title>MRFTA — preparação</title><body><h1>MRFTA — preenchimento de preparação</h1><p>Quadro1:retenção na fonte;${esc(m.input.declaration==='initial'?'Inicial':m.input.declaration==='replacement'?'Substituição':'Tipo por identificar')}. Quadro2:${esc(m.period)};${esc(m.input.timing==='within'?'Dentro do prazo':m.input.timing==='late'?'Fora do prazo':'Prazo por identificar')}.</p><p>Quadro3 NUIT:${esc(m.input.nuit)};área fiscal:${esc(m.input.taxArea)}. Quadro4:${esc(m.input.name)}. Quadro5:${esc(m.input.commercialName)}. Quadro6:${esc(m.input.address)};${esc(m.input.contact)}.</p><p>Conferência do período:${esc(m.input.completenessRef)}. Outras rubricas:${esc(m.input.otherRubricsRef)}.</p><h2>Quadro7</h2><table><thead><tr><th>CER</th><th>Rubrica</th><th>Base</th><th>Taxa</th><th>Imposto</th><th>Documentos</th></tr></thead><tbody>${m.lines.map(l=>`<tr><td>${esc(l.cer||'Por corresponder')}</td><td>${esc(l.rubric)}</td><td>${esc(money(l.base))}</td><td>${l.rate}%</td><td>${esc(money(l.tax))}</td><td>${esc(l.documents.join(';')+';'+l.references.join(' | '))}</td></tr>`).join('')}</tbody></table><p>Subtotal correspondido:${esc(money(m.principal))};JUROS COMPENSATÓRIOS:${esc(money(m.interest))};VALOR JÁ PAGO:${esc(money(m.paid))};TOTAL A PAGAR:${esc(money(m.total))}.</p><p>${esc(m.interestReason)}</p><h2>Casos pendentes</h2><ul>${m.pending.map(p=>'<li>'+esc(p)+'</li>').join('')}</ul><h2>Casos separados</h2><ul>${m.excluded.map(p=>'<li>'+esc(p.id+':'+p.reason)+'</li>').join('')}</ul><p>Quadros8/9 reservados,sem assinatura,carimbo,número de entrada ou aceitação AT. Tributação autónoma e juros de mora não apurados. Este documento de preparação não substitui o original oficial nem submete uma declaração.</p><small>${esc(m.source)}</small></body></html>`;
return `<!doctype html><html lang="pt"><meta charset="utf-8"><title>MRFTA — preparação</title><body><h1>MRFTA — preenchimento de preparação</h1><p>Quadro1:retenção na fonte;${esc(m.input.declaration==='initial'?'Inicial':m.input.declaration==='replacement'?'Substituição':'Tipo por identificar')}. Quadro2:${esc(m.period)};${esc(m.input.timing==='within'?'Dentro do prazo':m.input.timing==='late'?'Fora do prazo':'Prazo por identificar')}.</p><p>Quadro3 NUIT:${esc(m.input.nuit)};área fiscal:${esc(m.input.taxArea)}. Quadro4:${esc(m.input.name)}. Quadro5:${esc(m.input.commercialName)}. Quadro6:${esc(m.input.address)};${esc(m.input.contact)}.</p><p>Conferência do período:${esc(m.input.completenessRef)}. Outras rubricas:${esc(m.input.otherRubricsRef)}.</p><p>CER introduzidos com referência do caso; sem validação automática por catálogo oficial. Códigos provisórios ou de simulação não são correspondências oficiais.</p><h2>Quadro7</h2><table><thead><tr><th>CER</th><th>Rubrica</th><th>Base</th><th>Taxa</th><th>Imposto</th><th>Documentos</th></tr></thead><tbody>${m.lines.map(l=>`<tr><td>${esc(l.cer||'Por corresponder')}</td><td>${esc(l.rubric)}</td><td>${esc(money(l.base))}</td><td>${l.rate}%</td><td>${esc(money(l.tax))}</td><td>${esc(l.documents.join(';')+';'+l.references.join(' | '))}</td></tr>`).join('')}</tbody></table><p>Subtotal correspondido:${esc(money(m.principal))};JUROS COMPENSATÓRIOS:${esc(money(m.interest))};VALOR JÁ PAGO:${esc(money(m.paid))};TOTAL A PAGAR:${esc(money(m.total))}.</p><p>${esc(m.interestReason)}</p><h2>Casos pendentes</h2><ul>${m.pending.map(p=>'<li>'+esc(p)+'</li>').join('')}</ul><h2>Casos separados</h2><ul>${m.excluded.map(p=>'<li>'+esc(p.id+':'+p.reason)+'</li>').join('')}</ul><p>Quadros8/9 reservados,sem assinatura,carimbo,número de entrada ou aceitação AT. Tributação autónoma e juros de mora não apurados. Este documento de preparação não substitui o original oficial nem submete uma declaração.</p><small>${esc(m.source)}</small></body></html>`;
}