Files
SGF/packages/general-ledger/ui/cycle.php
T
2026-11-04 09:37:20 +02:00

32 lines
13 KiB
PHP

<?php
$e=static fn($v)=>htmlspecialchars((string)$v,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');
$money=static fn($v)=>($v<0?'-':'').number_format(intdiv(abs($v),100),0,',',' ').','.str_pad((string)(abs($v)%100),2,'0',STR_PAD_LEFT).' MZN';
$reason=static function(){echo '<label>Fundamento individual<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label>';};
$dates=static function(){echo '<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>';};
$reports=class_exists(\Tempo\Finance\GeneralLedger\ReportsWorkspace::class)?'<p><a href="/general-ledger/trial-balance?from_on=2026-10-01&amp;to_on=2026-10-31">Balancete e razão por conta — Outubro de 2026 (QA)</a></p>':'';
$hidden=static function($name,$value)use($e){echo '<input type="hidden" name="'.$e($name).'" value="'.$e($value).'">';};
?><!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ciclo contabilístico sintético</title><style>body{font:17px system-ui;background:#f3f6f5;color:#142b28}main{max-width:1100px;margin:auto;padding:24px}header,section{background:white;padding:24px;border:1px solid #d5e0dc;border-radius:12px;margin:18px 0}article{border-top:1px solid #ddd;padding:16px 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}a{color:#125a4b}</style></head><body><main>
<header><a href="<?= $e($settings['return_url']) ?>">Voltar à aplicação</a><h1><?= $e($settings['host']) ?> — ciclo contabilístico QA</h1><p class="notice">Exclusivamente sintético: principal60, afectação20, reversão integral e correcção técnica sem alterar a dívida. Contas e período artificiais. Sem execução monetária, fiscalidade real ou reabertura.</p><button id="finance">Consultar/preparar liquidações financeiras</button><?php if(session('ledger_demo')): ?><p><a href="/qa/ledger-demo">Trocar função sintética</a></p><?php endif ?><p>Preparação, revisão independente e aplicação são decisões separadas. Os totais contabilísticos incluem os estornos; saldo financeiro não é soma bruta dos lançamentos.</p></header><p id="error" role="alert"></p>
<?= $reports ?><section><h2>1. Reconhecimento contabilístico da obrigação</h2><p>Obrigações financeiras reconhecidas desta página; preparar não volta a reconhecer a dívida.</p><?php if($data['permissions']['prepare']): ?><form data-action="prepare"><label>Obrigação<select name="obligation_id" required><option value="">Escolha uma obrigação</option><?php foreach($data['obligations'] as $o): ?><option value="<?= $o->id ?>">#<?= $o->id ?> · <?= $money((int)$o->principal_minor) ?></option><?php endforeach ?></select></label><?php $dates();$hidden('rule_key','QA-PAYABLE-JOURNAL');$reason(); ?><button>Capturar reconhecimento e impacto</button></form><?php endif ?></section>
<section><h2>2. Afectações e reversões financeiras confirmadas</h2><p>Um pagamento sem afectação confirmada não é origem deste lançamento. O principal da obrigação e o montante afectado permanecem distintos.</p><?php foreach(['allocations'=>['allocation.confirmed','QA-ALLOCATION'],'reversals'=>['allocation.reversed','QA-REVERSAL']] as $name=>[$type,$rule]): ?><?php foreach($data[$name] as $fact): ?><article><h3><?= $name==='allocations'?'Afectação':'Reversão' ?> #<?= $fact->id ?> · Obrigação #<?= $fact->obligation_id ?> · <?= $money((int)$fact->amount_minor) ?></h3><details><summary>Evidência financeira confirmada</summary><pre><?= $e(json_encode($fact,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)) ?></pre></details><?php if($data['permissions']['prepare']): ?><form data-action="effect-prepare"><?php $hidden('effect_type',$type);$hidden('source_id',$fact->id);$hidden('rule_key',$rule);$dates();$reason(); ?><button>Capturar este efeito — sem lançar</button></form><?php endif ?></article><?php endforeach; endforeach ?></section>
<section><h2>3. Propostas, impacto e revisão exacta</h2><?php foreach($data['book']['proposals'] as $p): $snapshot=json_decode($p->snapshot,true);$kind=$snapshot['source']['effect_type'];$correct=in_array($kind,['journal.reversal','journal.replacement','workshop.direct_cost.reversal','workshop.direct_cost.replacement','industry.cost.reversal','industry.cost.replacement'],true);$review=null;foreach($data['book']['reviews'] as $r)if($r->proposal_id===$p->id)$review=$r; ?><article><h3>Proposta #<?= $p->id ?> · <?= $e($kind) ?> · Obrigação #<?= $p->obligation_id ?></h3><p>Impacto <?= $money($snapshot['impact']['debit_minor']) ?> débito/crédito · versão <?= $e($p->revision) ?> · autor #<?= $p->prepared_by ?></p><ul><?php foreach($snapshot['lines'] as $line): ?><li><?= $e($line['account_code']) ?> D <?= $money($line['debit_minor']) ?> / C <?= $money($line['credit_minor']) ?></li><?php endforeach ?></ul><details><summary>Fotografia original, regra e período</summary><pre><?= $e(json_encode($snapshot,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)) ?></pre></details><p><?= $e($p->reason) ?> · <?= $review?$e($review->decision):'Revisão pendente' ?><?= $p->journal_id?' · Aplicada no lançamento #'.$p->journal_id:'' ?></p>
<?php if(!$correct&&$data['permissions']['validate']&&!$review): ?><form data-action="validate"><?php $hidden('proposal_id',$p->id);$hidden('revision',$p->revision); ?><label>Decisão<select name="decision"><option value="">Escolha</option><option value="approved">Aprovar</option><option value="rejected">Recusar</option></select></label><?php $reason(); ?><button>Rever versão exacta</button></form><?php endif ?>
<?php if(!$correct&&$data['permissions']['post']&&$review&&$review->decision==='approved'&&!$p->journal_id): ?><form data-action="post"><?php $hidden('proposal_id',$p->id);$hidden('revision',$p->revision);$reason(); ?><button>Revalidar e aplicar uma vez</button></form><?php endif ?><?php if($correct): ?><p>Esta proposta só pode ser aplicada com o par de correcção abaixo.</p><?php endif ?></article><?php endforeach ?></section>
<section><h2>4. Correcção técnica por par indivisível</h2><p>O original mantém-se. Estorno60 e substituição60 são revistos e aplicados juntos; não alteram principal, imposto ou moeda.</p><?php if($data['permissions']['correct_prepare']): ?><form data-action="correct-prepare"><label>ID do lançamento original<input name="journal_id" type="number" min="1" required></label><?php $dates();$reason(); ?><button>Capturar os dois impactos</button></form><?php endif ?>
<?php foreach($data['corrections'] as $c): $result=null;foreach($data['correction_results'] as $r)if($r->correction_id===$c->id)$result=$r; ?><article><h3>Correcção #<?= $c->id ?> do original #<?= $c->original_journal_id ?></h3><p>Propostas #<?= $c->reverse_proposal_id ?> e #<?= $c->replacement_proposal_id ?> · revisão <?= $e($c->revision) ?></p><p><?= $e($c->reason) ?></p><details><summary>Impacto exacto dos dois lançamentos</summary><pre><?= $e($c->reverse_snapshot) ?></pre><pre><?= $e($c->replacement_snapshot) ?></pre></details><?php if($result): ?><p>Par aplicado: lançamentos #<?= $result->reverse_journal_id ?> / #<?= $result->replacement_journal_id ?>.</p><?php else: ?>
<?php if($data['permissions']['correct_validate']): ?><form data-action="correct-validate"><?php $hidden('correction_id',$c->id);$hidden('revision',$c->revision); ?><label>Decisão do par<select name="decision" required><option value="">Escolha</option><option value="approved">Aprovar ambos</option><option value="rejected">Recusar ambos</option></select></label><?php $reason(); ?><button>Rever o par exacto</button></form><?php endif ?>
<?php if($data['permissions']['correct_post']): ?><form data-action="correct-post"><?php $hidden('correction_id',$c->id);$hidden('revision',$c->revision);$reason(); ?><button>Revalidar e aplicar o par atómico</button></form><?php endif ?><?php endif ?></article><?php endforeach ?></section>
<section><h2>5. Conferência e fecho do período QA</h2><p>Reconhecimentos, afectações e reversões confirmados sem contabilização ou propostas necessárias não resolvidas impedem o fecho. Mudança de evidência exige nova conferência. Não existe comando de reabertura.</p><?php foreach($data['periods'] as $p): ?><p><?= $e($p->period_key) ?> v<?= $p->version ?> · <?= $e($p->status) ?> · <?= $e($p->reason) ?></p><?php endforeach ?>
<?php if($data['permissions']['period_prepare']): ?><form data-action="period-prepare"><label>Período<input name="period_key" value="QA-2026-10" required></label><?php $reason(); ?><button>Capturar evidência e totais para fecho</button></form><?php endif ?>
<?php foreach($data['period_checks'] as $c): $closed=null;foreach($data['period_closures'] as $r)if($r->check_id===$c->id)$closed=$r; ?><article><h3>Conferência #<?= $c->id ?> · <?= $e($c->period_key) ?></h3><p>Versão <?= $e($c->revision) ?> · autor #<?= $c->prepared_by ?> · <?= $e($c->reason) ?></p><pre><?= $e($c->snapshot) ?></pre><?php foreach($data['period_reviews'] as $r)if($r->check_id===$c->id): ?><p><?= $e($r->decision) ?> por #<?= $r->actor_id ?> · <?= $e($r->reason) ?></p><?php endif ?>
<?php if($closed): ?><p>Fecho #<?= $closed->id ?> · nova versão <?= $closed->period_version ?> · <?= $e($closed->created_at) ?></p><?php else: ?>
<?php if($data['permissions']['period_validate']): ?><form data-action="period-validate"><?php $hidden('check_id',$c->id);$hidden('revision',$c->revision); ?><label>Decisão<select name="decision" required><option value="">Escolha</option><option value="approved">Aprovar</option><option value="rejected">Recusar</option></select></label><?php $reason(); ?><button>Conferir fotografia do período</button></form><?php endif ?>
<?php if($data['permissions']['period_close']): ?><form data-action="period-close"><?php $hidden('check_id',$c->id);$hidden('revision',$c->revision);$reason(); ?><button>Revalidar e fechar este período</button></form><?php endif ?><?php endif ?></article><?php endforeach ?></section>
<section><h2>Diário, extracto e histórico</h2><p>Débitos <?= $money($data['book']['totals']['debit_minor']) ?> · Créditos <?= $money($data['book']['totals']['credit_minor']) ?>.</p><?php foreach($data['book']['journals'] as $j): ?><details><summary>Lançamento #<?= $j->id ?> · Obrigação #<?= $j->obligation_id ?> · D/C <?= $money((int)$j->debit_minor) ?></summary><p>Preparação #<?= $j->proposal_id ?> · revisão #<?= $j->review_id ?> · aplicador #<?= $j->posted_by ?> · <?= $e($j->reason) ?> · <?= $e($j->created_at) ?></p><pre><?= $e($j->snapshot) ?></pre></details><?php endforeach ?><?php foreach($data['book']['events'] as $event): ?><details><summary>#<?= $event->id ?> · <?= $e($event->kind) ?> · autor #<?= $event->actor_id ?> · <?= $e($event->created_at) ?></summary><p><?= $e($event->reason) ?></p><pre><?= $e($event->previous) ?></pre><pre><?= $e($event->subsequent) ?></pre></details><?php endforeach ?></section>
<nav aria-label="Páginas do ciclo"><p>Cada lista contém até25 registos. Totais por lista:</p><?php $max=max(array_column($data['book']['evidence_pagination'],'last_page'));foreach($data['pagination'] as $name=>$p){$max=max($max,$p['last_page']);echo '<p>'.$e($name).': '.$p['total'].' · página '.$p['page'].' de '.$p['last_page'].'</p>';}$current=$data['pagination']['obligations']['page'];foreach([-1=>'Anterior',1=>'Seguinte'] as $delta=>$label){$target=$current+$delta;if($target>=1&&$target<=max($max,$data['book']['pagination']['last_page']))echo '<a href="'.$e($settings['page_url'].'?page='.$target).'">'.$label.'</a> ';} ?></nav>
</main><script>const settings=<?= json_encode($settings,JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_THROW_ON_ERROR) ?>;
document.querySelector('#finance').onclick=async()=>{try{if(settings.scope){const r=await fetch('/api/context',{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':settings.csrf},body:JSON.stringify({tenant_id:settings.scope.tenant_id,company_id:settings.scope.company_id,unit_id:settings.scope.unit_id,module_id:'purchasing'})});if(!r.ok)throw Error('Módulo financeiro indisponível.');}location.assign(settings.financial_url);}catch(e){document.querySelector('#error').textContent=e.message;}};
for(const form of document.querySelectorAll('[data-action]'))form.onsubmit=async e=>{e.preventDefault();const body=Object.fromEntries(new FormData(form));for(const k of ['obligation_id','source_id','proposal_id','journal_id','correction_id','check_id'])if(k in body)body[k]=Number(body[k]);const payload=JSON.stringify(body);if(form.dataset.payload!==payload){form.dataset.payload=payload;form.dataset.key=crypto.randomUUID();}const button=form.querySelector('button');button.disabled=true;try{const r=await fetch('/api/general-ledger/'+form.dataset.action,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':settings.csrf,'X-SGF-Context':settings.context??'','Idempotency-Key':form.dataset.key},body:payload});const data=await r.json();if(!r.ok)throw Error(data.message||'Decisão recusada; confira o estado actual.');location.reload();}catch(e){document.querySelector('#error').textContent=e.message+' Consulte o histórico antes de repetir.';}finally{button.disabled=false;}};
</script></body></html>