Files
2026-11-04 09:37:20 +02:00

48 lines
18 KiB
PHP

@php
$c=$data['commercial'];$inv=$c['invoice'];$id=$inv['id']??null;$p=$c['position']??null;$ob=$c['obligation_id']??null;
$money=static fn($n)=>($n<0?'-':'').number_format(intdiv(abs($n),100),0,',',' ').','.str_pad((string)(abs($n)%100),2,'0',STR_PAD_LEFT).' MZN';
$fp=$c['finance_permissions']??[];
$periodClosed=(collect($data['periods'])->where('period_key','QA-2026-10')->sortByDesc('version')->first()?->status)==='closed';
if($periodClosed){$fp=array_fill_keys(array_keys($fp),false);$data['permissions']=array_fill_keys(array_keys($data['permissions']),false);}
@endphp
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Comercial, recebíveis e contabilidade — QA</title><style>body{font:17px system-ui;background:#f3f6f5;color:#142b28}main{max-width:1100px;margin:auto;padding:24px}section,header{background:white;border:1px solid #d5e0dc;border-radius:12px;padding:24px;margin:18px 0}article{border-top:1px solid #ddd;padding:18px 0}label{display:block;margin:12px 0}input,select,textarea,button{font:inherit;padding:10px;max-width:95%}textarea{display:block;width:90%}.notice{padding:16px;background:#fff2cc}pre{white-space:pre-wrap;overflow-wrap:anywhere}code{overflow-wrap:anywhere}a{color:#125a4b}</style></head><body><main>
<header><a href="/general-ledger">Diário e extracto</a> · <a href="/">Navegação SGF</a>@if(session('sales_demo')) · <a href="/qa/sales-demo">Trocar função sintética</a>@endif<h1>Comercial → Recebíveis → Contabilidade</h1><p class="notice">TESTE — SEM VALIDADE FISCAL. Sem movimentação de dinheiro. Contas e período artificiais, sem reabertura. As decisões da emissão e da dívida permanecem separadas do lançamento.</p><p>Empresa #{{ $c['scope']['company_id'] }} / Unidade #{{ $c['scope']['unit_id'] }}. As operações financeiras abaixo seleccionam explicitamente Financeiro, usam os seus comandos existentes e regressam a Contabilidade. Cada envio exige fundamento; mudança de contexto invalida separadores antigos.</p></header><p id="error" role="alert"></p>
<section><h2>Escolher documento emitido</h2><form method="get"><label>Factura de teste<select name="invoice_id" required><option value="">Escolha uma origem</option>@foreach($c['invoices'] as $i)<option value="{{ $i->id }}" @selected($i->id===$id)>#{{ $i->id }} · {{ $i->series_code }}/{{ $i->number }}</option>@endforeach</select></label><button>Conferir esta origem</button></form></section>
@if($periodClosed)<p class="notice">Período QA encerrado. Este percurso conserva a consulta e os comprovativos; não propõe novos efeitos para o período fechado. Não existe reabertura ou reposição automática do exemplo.</p>@endif
@if($inv)
<section><h2>1. Documento original e cálculo conservado</h2><p>Factura #{{ $id }} · Base {{ $inv['snapshot']['calculation']['base_exact'] }} · Imposto documental {{ $inv['snapshot']['calculation']['vat_document_proposed'] }} · Total {{ $inv['snapshot']['calculation']['total_document_proposed'] }} MZN.</p><p>IVA exacto de teste {{ $inv['snapshot']['calculation']['vat_exact'] }}; diferença conservada {{ $inv['snapshot']['calculation']['discarded_difference'] }}. Não se recalcula o imposto ao contabilizar.</p><button id="pdf">Seleccionar Comercial e obter PDF original de teste</button><a id="pdf-link" hidden target="_blank" rel="noopener">Abrir PDF de teste obtido</a><details><summary>Fotografia de emissão, pressupostos e versões</summary><pre>{{ json_encode($inv['snapshot'],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details></section>
<section><h2>2. Obrigação e recebimentos sintéticos</h2>
@if($p)<p>Obrigação #{{ $ob }} · Principal {{ $p['original'] }} MZN · Saldo {{ $p['balance'] }} MZN · Recebimentos por afectar {{ $p['available'] }} MZN · revisão {{ $p['revision'] }}</p><p>Vencimento {{ $p['due_date']??'desconhecido' }}. As posições vêm do núcleo financeiro existente; um lançamento não abate o saldo novamente.</p>
@if(!$p['opened'])<p role="alert">Esta é uma proposta de abertura ainda sem confirmação. O montante proposto não constitui dívida aprovada; o reconhecimento contabilístico fica bloqueado.</p>@endif
@else<p>Nenhuma obrigação confirmada. Primeiro preparar a abertura e obter decisão independente; o documento sozinho não aprova a dívida.</p>@endif
@if(($fp['prepare']??false)&&!$ob)<form data-finance="propose"><input type="hidden" name="kind" value="obligation"><input type="hidden" name="revision" value="0"><label>Fundamento da abertura<textarea name="reason" required minlength="3" maxlength="1000"></textarea></label><button>Seleccionar Financeiro e preparar abertura1000 — sem confirmar</button></form>@endif
@if(($fp['prepare']??false)&&($p['opened']??false))<form data-finance="propose"><input type="hidden" name="kind" value="receipt"><input type="hidden" name="obligation_id" value="{{ $ob }}"><input type="hidden" name="revision" value="{{ $p['revision'] }}"><label>Montante do recebimento<input name="amount" required pattern="[0-9]+([.][0-9]{1,2})?" placeholder="400.00 ou 600.00"></label><label>Data explícita<input name="date" type="date" value="2026-10-29" required></label><label>Evidência sintética<textarea name="evidence" required minlength="3" maxlength="1000"></textarea></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="1000"></textarea></label><button>Seleccionar Financeiro e propor recebimento — sem confirmar</button></form>@endif
@foreach($c['records'] as $r)<article><h3>Registo financeiro #{{ $r->id }} · {{ $r->kind }} · {{ $r->amount }} MZN</h3><p>Autor #{{ $r->actor_id }} · {{ $r->created_at }} · {{ $r->decision_kind??'Decisão pendente' }}</p><details><summary>Origem, fundamento e versão</summary><pre>{{ $r->data }}</pre></details>
@if(!$r->decision_kind&&($r->kind==='correction'?($fp['correction_approve']??false):($fp['confirm']??false))&&$r->actor_id!==auth()->id())<form data-finance="decision" data-id="{{ $r->id }}"><input type="hidden" name="version" value="1"><input type="hidden" name="revision" value="{{ $p['revision']??0 }}"><label>Decisão independente<select name="decision" required><option value="">Escolha</option><option value="approved">Aprovar</option><option value="refused">Recusar</option></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="1000"></textarea></label><button>Seleccionar Financeiro e registar decisão exacta</button></form>@endif
@if($r->kind==='receipt'&&($fp['allocate']??false)&&(($p['receipts'][$r->id]['state']??'')==='confirmed'))<p>Disponível deste recebimento: {{ $p['receipts'][$r->id]['available'] }} MZN.</p><form data-finance="allocate" data-id="{{ $r->id }}"><input type="hidden" name="version" value="1"><input type="hidden" name="revision" value="{{ $p['revision'] }}"><label>Montante explícito a afectar<input name="amount" required placeholder="400.00"></label><button>Seleccionar Financeiro e afectar este montante</button></form>@endif
</article>@endforeach</section>
<section><h2>3. Fontes contabilísticas e reversão financeira</h2>
@if(($p['opened']??false)&&$data['permissions']['prepare'])<form data-ledger="effect-prepare"><input type="hidden" name="effect_type" value="sales.invoice"><input type="hidden" name="source_id" value="{{ $id }}"><input type="hidden" name="rule_key" value="QA-SALES"><label>Data explícita<input name="journal_on" type="date" value="2026-10-29" required></label><label>Período<input name="period_key" value="QA-2026-10" required></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar o reconhecimento da factura em três linhas — sem lançar</button></form>@endif
@foreach($c['effects'] as $e)@if(in_array($e->kind,['allocate','reverse_allocation']))<article><h3>Efeito #{{ $e->id }} · {{ $e->kind }} · {{ $e->amount }} MZN</h3><p>Registo #{{ $e->record_id }} · autor #{{ $e->actor_id }} · {{ $e->created_at }}</p>
@if($data['permissions']['prepare'])<form data-ledger="effect-prepare"><input type="hidden" name="effect_type" value="{{ $e->kind==='allocate'?'receivable.allocated':'receivable.reversed' }}"><input type="hidden" name="source_id" value="{{ $e->id }}"><input type="hidden" name="rule_key" value="{{ $e->kind==='allocate'?'QA-RECEIVABLE':'QA-RECEIVABLE-REVERSE' }}"><label>Data explícita<input name="journal_on" type="date" value="2026-10-29" required></label><label>Período<input name="period_key" value="QA-2026-10" required></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar este efeito confirmado — sem lançar</button></form>@endif
@if($e->kind==='allocate'&&($fp['correction_request']??false))<form data-finance="propose"><input type="hidden" name="kind" value="correction"><input type="hidden" name="action" value="reverse_allocation"><input type="hidden" name="target_effect_id" value="{{ $e->id }}"><input type="hidden" name="obligation_id" value="{{ $ob }}"><input type="hidden" name="revision" value="{{ $p['revision'] }}"><label>Fundamento da reversão integral<textarea name="reason" required minlength="3" maxlength="1000"></textarea></label><button>Seleccionar Financeiro e propor reversão — sem aplicar</button></form>@endif</article>@endif @endforeach</section>
<section><h2>4. Conferência e aplicação contabilística</h2>@foreach($data['book']['proposals'] as $proposal)@php $snap=json_decode($proposal->snapshot,true);$review=collect($data['book']['reviews'])->firstWhere('proposal_id',$proposal->id); @endphp<article><h3>Proposta #{{ $proposal->id }} · {{ $snap['source']['effect_type'] }}</h3><p>Autor #{{ $proposal->prepared_by }} · revisão exacta <code>{{ $proposal->revision }}</code></p><p>Data {{ $snap['journal_on'] }} · regra {{ $snap['rule']['rule_key'] }} v{{ $snap['rule']['version'] }} · período v{{ $snap['period']['version'] }}.</p><ul>@foreach($snap['lines'] as $line)<li>{{ $line['account_code'] }} · D {{ $money($line['debit_minor']) }} / C {{ $money($line['credit_minor']) }}</li>@endforeach</ul><p>Impacto D/C {{ $money($snap['impact']['debit_minor']) }}; sem nova dívida ou alteração do saldo financeiro.</p><details><summary>Fotografia exacta da fonte, regra e período</summary><pre>{{ json_encode($snap,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details><p>{{ $review?'Decisão '.$review->decision.' por #'.$review->reviewed_by:'Revisão independente pendente' }} · {{ $proposal->journal_id?'Lançamento #'.$proposal->journal_id:'' }}</p>
@if($data['permissions']['validate']&&!$review&&$proposal->prepared_by!==auth()->id())<form data-ledger="validate"><input type="hidden" name="proposal_id" value="{{ $proposal->id }}"><input type="hidden" name="revision" value="{{ $proposal->revision }}"><label>Decisão<select name="decision" required><option value="">Escolha</option><option value="approved">Aprovar</option><option value="rejected">Recusar</option></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Rever esta fotografia — sem lançar</button></form>@endif
@if($data['permissions']['post']&&$review&&$review->decision==='approved'&&!$proposal->journal_id&&$proposal->prepared_by===auth()->id())<form data-ledger="post"><input type="hidden" name="proposal_id" value="{{ $proposal->id }}"><input type="hidden" name="revision" value="{{ $proposal->revision }}"><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e aplicar uma vez</button></form>@endif</article>@endforeach</section>
<section><h2>5. Fecho QA, extracto e evidências</h2><p>Totais brutos desta factura: D {{ $money($data['book']['totals']['debit_minor']) }} / C {{ $money($data['book']['totals']['credit_minor']) }}. Não representam saldo por receber.</p><p>O fecho abaixo confere todos os efeitos reconhecidos da empresa/unidade, incluindo fornecedores quando existirem; não apenas a factura seleccionada.</p>@if($data['permissions']['period_prepare'])<form data-ledger="period-prepare"><label>Período<input name="period_key" value="QA-2026-10" required></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar conferência completa do período</button></form>@endif
@if($data['client_account'])<p>Conta de clientes desta factura: débito menos crédito {{ $money($data['client_account']['net_minor']) }}. Compare com a posição financeira acima; efeitos confirmados ainda não lançados continuam pendentes de conferência.</p>@endif
@foreach($data['period_checks'] as $check)@php $periodReview=collect($data['period_reviews'])->firstWhere('check_id',$check->id);$closed=collect($data['period_closures'])->firstWhere('check_id',$check->id); @endphp<article><h3>Conferência #{{ $check->id }} · {{ $check->period_key }}</h3><pre>{{ $check->snapshot }}</pre><p>{{ $closed?'Fecho #'.$closed->id:($periodReview?'Revisão '.$periodReview->decision:'Revisão pendente') }}</p>
@if($data['permissions']['period_validate']&&!$periodReview&&!$closed)<form data-ledger="period-validate"><input type="hidden" name="check_id" value="{{ $check->id }}"><input type="hidden" name="revision" value="{{ $check->revision }}"><label>Decisão<select name="decision" required><option value="">Escolha</option><option value="approved">Aprovar</option><option value="rejected">Recusar</option></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Conferir período por outra pessoa</button></form>@endif
@if($data['permissions']['period_close']&&$periodReview&&$periodReview->decision==='approved'&&!$closed)<form data-ledger="period-close"><input type="hidden" name="check_id" value="{{ $check->id }}"><input type="hidden" name="revision" value="{{ $check->revision }}"><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e fechar o período artificial</button></form>@endif</article>@endforeach
@foreach($data['book']['journals'] as $j)<details><summary>Lançamento #{{ $j->id }} · {{ $j->journal_on }} · D/C {{ $money((int)$j->debit_minor) }}</summary><p>Proposta #{{ $j->proposal_id }} · revisão #{{ $j->review_id }} · autor #{{ $j->posted_by }} · {{ $j->reason }}</p><pre>{{ $j->snapshot }}</pre></details>@endforeach
@foreach($data['book']['events'] as $event)<details><summary>Evidência #{{ $event->id }} · {{ $event->kind }} · autor #{{ $event->actor_id }} · {{ $event->created_at }}</summary><p>{{ $event->reason }}</p><pre>{{ $event->previous }}</pre><pre>{{ $event->subsequent }}</pre></details>@endforeach</section>
@endif
@php $pagers=$data['pagination']+['documents'=>$c['pagination']]+($id?['financial_records'=>$c['records_pagination'],'financial_effects'=>$c['effects_pagination']]+$data['book']['evidence_pagination']:[]);$current=$c['pagination']['page'];$max=max(array_column($pagers,'last_page')); @endphp<nav aria-label="Paginação das evidências"><p>Até25 registos por lista. Página {{ $current }}; maior página disponível {{ $max }}.</p>@foreach([-1=>'Anterior',1=>'Seguinte'] as $delta=>$label)@php $target=$current+$delta; @endphp @if($target>=1&&$target<=$max)<a href="?{{ http_build_query(array_filter(['invoice_id'=>$id,'page'=>$target],fn($v)=>$v!==null)) }}">{{ $label }}</a>@endif @endforeach</nav>
@include('accounting.receipt-recovery')
</main><script>
const scope=@json($c['scope']),invoice=@json($id),actor=@json(auth()->id()),csrf=@json(csrf_token());let context=@json(session('company_context.token'));
const select=async module=>{const current=await fetch('/api/session',{credentials:'same-origin',cache:'no-store',headers:{Accept:'application/json'}});const snapshot=await current.json();if(!current.ok||snapshot.user?.id!==actor||snapshot.context?.token!==context||snapshot.context?.company?.id!==scope.company_id||snapshot.context?.company?.tenant_id!==scope.tenant_id||snapshot.context?.unit?.id!==scope.unit_id)throw Error('O contexto mudou. Reabra o percurso antes de enviar esta decisão.');const r=await fetch('/api/context',{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf},body:JSON.stringify({...scope,module_id:module})});const d=await r.json();if(!r.ok)throw Error(d.message||'Módulo indisponível.');context=d.context.token;};
const pdf=document.querySelector('#pdf');if(pdf)pdf.onclick=async()=>{try{await select('commercial');const r=await fetch('/api/service-invoices/'+invoice+'/pdf',{credentials:'same-origin',headers:{'X-SGF-Context':context}});if(!r.ok)throw Error('PDF indisponivel; confira os acessos.');const blob=await r.blob();await select('accounting');const link=document.querySelector('#pdf-link');if(link.dataset.url)URL.revokeObjectURL(link.dataset.url);link.dataset.url=URL.createObjectURL(blob);link.href=link.dataset.url;link.hidden=false;}catch(e){document.querySelector('#error').textContent=e.message;try{await select('accounting');}catch{}}};
for(const form of document.querySelectorAll('[data-ledger],[data-finance]'))form.onsubmit=async e=>{e.preventDefault();let body=Object.fromEntries(new FormData(form));for(const k of ['obligation_id','source_id','proposal_id','check_id','revision','target_effect_id','version'])if(k in body&&(k!=='revision'||form.dataset.finance))body[k]=Number(body[k]);const finance=form.dataset.finance;const signature=JSON.stringify(body);if(form.dataset.signature!==signature){form.dataset.signature=signature;form.dataset.key=crypto.randomUUID();}let url='/api/general-ledger/'+form.dataset.ledger;if(finance){if(finance==='propose'){body={...body,invoice_id:invoice,currency:'MZN',source:'TEST-MANUAL',external_id:form.dataset.key};url='/api/service-receivables/proposals';}else url='/api/service-receivables/'+form.dataset.id+'/'+(finance==='decision'?'decision':'allocate');}const button=form.querySelector('button');button.disabled=true;try{if(finance)await select('finance');const r=await fetch(url,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,'Idempotency-Key':form.dataset.key},body:JSON.stringify(body)});const d=await r.json();if(!r.ok)throw Error(d.message||'Estado alterado; confira a evidência.');if(finance)await select('accounting');location.reload();}catch(e){document.querySelector('#error').textContent=e.message+' Consulte o histórico antes de repetir.';if(finance){try{await select('accounting');}catch{}}}finally{button.disabled=false;}};
</script></body></html>