commit melhorias1

This commit is contained in:
2026-11-11 15:04:01 +02:00
parent 1d11a5e57b
commit cd9897df05
145 changed files with 10950 additions and 84 deletions
+11 -8
View File
@@ -8,6 +8,7 @@ use Tempo\Finance\Payables\Payables;
final class PayableJournal
{
use LedgerCycle;
use LedgerAssets;
use LedgerSales;
use LedgerReports;
public const POLICY='QA-PAYABLE-JOURNAL-v1';
@@ -37,8 +38,9 @@ final class PayableJournal
if(!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/D',$date,$m)||!checkdate((int)$m[2],(int)$m[3],(int)$m[1]))throw new \InvalidArgumentException('Data de lançamento explícita inválida.');
$sales=in_array($kind,['sales.invoice','receivable.allocated','receivable.reversed','receivable.reduced','receivable.reduction_reversed','cash.receipt_classified','cash.expense_confirmed'],true);$cost=in_array($kind,['workshop.direct_cost.validated','workshop.direct_cost.reversal','workshop.direct_cost.replacement'],true);
$industrial=in_array($kind,['industry.cost.validated','industry.cost.reversal','industry.cost.replacement'],true);
$asset=in_array($kind,['assets.admitted','assets.depreciation','assets.writeoff','assets.depreciation.reversal','assets.depreciation.replacement'],true);
$stock=in_array($kind,['stock.receipt','stock.issue','stock.return','stock.adjustment','stock.supplier_return'],true);
$s=$sales||$cost||$industrial||$stock?($this->additionalSource!==null?($this->additionalSource)($kind,$obligation):throw new \DomainException('Origem adicional não composta neste host.')):($kind==='supplier_payable.recognised'?($this->source)($obligation):$this->cycleSource($kind,$obligation));
$s=$sales||$cost||$industrial||$stock||$asset?($this->additionalSource!==null?($this->additionalSource)($kind,$obligation):throw new \DomainException('Origem adicional não composta neste host.')):($kind==='supplier_payable.recognised'?($this->source)($obligation):$this->cycleSource($kind,$obligation));
$amount=$s['amount_minor']??$s['principal_minor']??null;
if(($s['effect_type']??'')!==$kind||!($s['compatible']??false)||($s['currency']??'')!=='MZN'||!is_int($amount)||$amount<=0||(!$sales&&($s['tax_minor']??null)!==0)||($s['history_imported']??true))throw new \DomainException('Fonte incompatível com o ensaio contabilístico.');
$period=$this->latest('periods','period_key',$periodKey);$rule=$this->latest('rules','rule_key',$ruleKey);
@@ -47,6 +49,7 @@ final class PayableJournal
if($stock){if(!$this->db->getSchemaBuilder()->hasTable('gl_stock_books')||($s['stock_movement_on']??'')!==$date)throw new \DomainException('Valorização não composta ou data física incompatível.');}
if($industrial&&($s['period_start']<$period->starts_on||$s['period_end']>$period->ends_on))throw new \DomainException('Período não contém o lote seleccionado.');
if($period->status!=='open'||$date<$period->starts_on||$date>$period->ends_on)throw new \DomainException('Período fechado, incompatível ou alterado.');
if($asset)return $this->captureAssets($s,$date,$period,$rule,$amount);
if($rule->policy!==(str_starts_with($kind,'cash.')?'QA-CASH-JOURNAL-v1':($stock?'QA-STOCK-VALUATION-v1':($sales?'QA-SALES-JOURNAL-v1':($cost?'QA-DIRECT-COST-JOURNAL-v1':($industrial?'QA-INDUSTRIAL-JOURNAL-v1':self::POLICY)))))||$rule->effect_type!==$s['effect_type']||$rule->currency!=='MZN'||$date<$rule->valid_from||$date>$rule->valid_to)throw new \DomainException('Regra contabilística ausente, incompatível ou fora de vigência.');
if(str_starts_with($kind,'cash.')&&($s['permitted_period_key']!==$periodKey||$s['cash_effect_on']!==$date))throw new \DomainException('Data ou período de Caixa incompatível.');if($sales)return $this->captureSales($s,$date,$period,$rule,$amount);
$accounts=$this->q('accounts')->whereIn('id',[$rule->debit_account_id,$rule->credit_account_id])->get()->keyBy('id');
@@ -83,7 +86,7 @@ final class PayableJournal
if($old){if(!hash_equals($old->request_hash,$hash))throw new \DomainException('Chave repetida com conteúdo diferente.');return json_decode($old->result,true,64,JSON_THROW_ON_ERROR)+['replayed'=>true];}
if(in_array($action,['validate','post'],true)){
$candidate=$this->proposal($this->id($data['proposal_id']));
if(in_array(json_decode($candidate->snapshot,true)['source']['effect_type'],['journal.reversal','journal.replacement','workshop.direct_cost.reversal','workshop.direct_cost.replacement','industry.cost.reversal','industry.cost.replacement'],true))throw new \DomainException('Use a decisão/aplicação do par de correcção completo.');
if(in_array(json_decode($candidate->snapshot,true)['source']['effect_type'],['journal.reversal','journal.replacement','workshop.direct_cost.reversal','workshop.direct_cost.replacement','industry.cost.reversal','industry.cost.replacement','assets.depreciation.reversal','assets.depreciation.replacement'],true))throw new \DomainException('Use a decisão/aplicação do par de correcção completo.');
}
if($action==='prepare')$result=$this->prepare($data,$reason);
elseif($action==='validate')$result=$this->review($data,$reason);
@@ -107,7 +110,7 @@ final class PayableJournal
{
if(!is_string($d['revision'])||!in_array($d['decision'],['approved','rejected'],true))throw new \InvalidArgumentException('Decisão/revisão explícita inválida.');
$p=$this->proposal($this->id($d['proposal_id']));
if(in_array(json_decode($p->snapshot,true)['source']['effect_type'],['journal.reversal','journal.replacement','workshop.direct_cost.reversal','workshop.direct_cost.replacement','industry.cost.reversal','industry.cost.replacement'],true))($this->authorize)('correct_validate');
if(in_array(json_decode($p->snapshot,true)['source']['effect_type'],['journal.reversal','journal.replacement','workshop.direct_cost.reversal','workshop.direct_cost.replacement','industry.cost.reversal','industry.cost.replacement','assets.depreciation.reversal','assets.depreciation.replacement'],true))($this->authorize)('correct_validate');
if((int)$p->prepared_by===$this->actor||$p->prepared_person===$this->person)throw new \DomainException('Revisão exige outra identidade e pessoa.');
$this->current($p,$d['revision']);$old=$this->q('reviews')->where('proposal_id',$p->id)->first();
if($old){if($old->decision!==$d['decision']||$old->reason!==$reason)throw new \DomainException('Decisão já registada; não pode ser alterada.');return ['state'=>$old->decision,'proposal_id'=>$p->id,'review_id'=>$old->id];}
@@ -118,7 +121,7 @@ final class PayableJournal
private function post(array $d,string $reason):array
{
if(!is_string($d['revision']))throw new \InvalidArgumentException('Revisão inválida.');$p=$this->proposal($this->id($d['proposal_id']));
if(in_array(json_decode($p->snapshot,true)['source']['effect_type'],['journal.reversal','journal.replacement','workshop.direct_cost.reversal','workshop.direct_cost.replacement','industry.cost.reversal','industry.cost.replacement'],true))($this->authorize)('correct_post');
if(in_array(json_decode($p->snapshot,true)['source']['effect_type'],['journal.reversal','journal.replacement','workshop.direct_cost.reversal','workshop.direct_cost.replacement','industry.cost.reversal','industry.cost.replacement','assets.depreciation.reversal','assets.depreciation.replacement'],true))($this->authorize)('correct_post');
if((int)$p->prepared_by!==$this->actor||$p->prepared_person!==$this->person)throw new \DomainException('Só o preparador autorizado aplica esta versão.');
$existing=$this->q('journals')->where('effect_key',$p->effect_key)->first();
if($existing){if((int)$existing->proposal_id!==(int)$p->id||!hash_equals($p->revision,$d['revision'])||$existing->reason!==$reason)throw new \DomainException('Efeito já lançado com conteúdo diferente.');return ['state'=>'posted','proposal_id'=>$p->id,'journal_id'=>$existing->id];}
@@ -153,12 +156,12 @@ final class PayableJournal
$lines=$this->q('lines')->whereIn('journal_id',$ids)->orderBy('line_no')->get();
$totals=$this->q('lines')->whereIn('journal_id',(clone $journals)->select('id'));if($account)$totals->where('account_id',$account->id);
$sum=$totals->selectRaw('coalesce(sum(debit_minor),0) AS debit_minor,coalesce(sum(credit_minor),0) AS credit_minor')->first();
$proposalQuery=$this->q('proposals');if($industrialLot)$proposalQuery->where('snapshot->source->industrial_lot_id',$industrialLot)->whereIn('snapshot->source->effect_type',['industry.cost.validated','industry.cost.reversal','industry.cost.replacement']);if($costOrder)$proposalQuery->where('snapshot->source->cost_order_id',$costOrder);if($salesInvoice)$proposalQuery->where('snapshot->source->invoice_id',$salesInvoice);if($focus)$proposalQuery->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS);$proposals=$proposalQuery->orderByDesc('id')->paginate(25,['*'],'page',$page);$proposalIds=array_map(fn($p)=>$p->id,$proposals->items());$reviews=$this->q('reviews')->whereIn('proposal_id',$proposalIds)->get();
$proposalQuery=$this->q('proposals');if($industrialLot)$proposalQuery->where('snapshot->source->industrial_lot_id',$industrialLot)->whereIn('snapshot->source->effect_type',['industry.cost.validated','industry.cost.reversal','industry.cost.replacement','assets.depreciation.reversal','assets.depreciation.replacement']);if($costOrder)$proposalQuery->where('snapshot->source->cost_order_id',$costOrder);if($salesInvoice)$proposalQuery->where('snapshot->source->invoice_id',$salesInvoice);if($focus)$proposalQuery->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS);$proposals=$proposalQuery->orderByDesc('id')->paginate(25,['*'],'page',$page);$proposalIds=array_map(fn($p)=>$p->id,$proposals->items());$reviews=$this->q('reviews')->whereIn('proposal_id',$proposalIds)->get();
$posted=$this->q('journals')->whereIn('proposal_id',$proposalIds)->pluck('id','proposal_id');foreach($proposals->items() as $p)$p->journal_id=$posted->get($p->id);
$eventPager=$this->q('events')->when($focus||$salesInvoice||$costOrder||$industrialLot,fn($q)=>$q->where(function($events)use($focus,$salesInvoice,$costOrder,$industrialLot){
$events->where(fn($e)=>$e->where('kind','journal.prepared')->whereIn('entity_id',$this->q('proposals')->when($focus,fn($q)=>$q->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS))->when($salesInvoice,fn($q)=>$q->where('snapshot->source->invoice_id',$salesInvoice))->when($costOrder,fn($q)=>$q->where('snapshot->source->cost_order_id',$costOrder))->when($industrialLot,fn($q)=>$q->where('snapshot->source->industrial_lot_id',$industrialLot)->whereIn('snapshot->source->effect_type',['industry.cost.validated','industry.cost.reversal','industry.cost.replacement']))->select('id')))
->orWhere(fn($e)=>$e->where('kind','journal.reviewed')->whereIn('entity_id',$this->q('reviews')->whereIn('proposal_id',$this->q('proposals')->when($focus,fn($q)=>$q->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS))->when($salesInvoice,fn($q)=>$q->where('snapshot->source->invoice_id',$salesInvoice))->when($costOrder,fn($q)=>$q->where('snapshot->source->cost_order_id',$costOrder))->when($industrialLot,fn($q)=>$q->where('snapshot->source->industrial_lot_id',$industrialLot)->whereIn('snapshot->source->effect_type',['industry.cost.validated','industry.cost.reversal','industry.cost.replacement']))->select('id'))->select('id')))
->orWhere(fn($e)=>$e->where('kind','journal.posted')->whereIn('entity_id',$this->q('journals')->when($focus,fn($q)=>$q->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS))->when($salesInvoice,fn($q)=>$q->where('snapshot->source->invoice_id',$salesInvoice))->when($costOrder,fn($q)=>$q->where('snapshot->source->cost_order_id',$costOrder))->when($industrialLot,fn($q)=>$q->where('snapshot->source->industrial_lot_id',$industrialLot)->whereIn('snapshot->source->effect_type',['industry.cost.validated','industry.cost.reversal','industry.cost.replacement']))->select('id')));
$events->where(fn($e)=>$e->where('kind','journal.prepared')->whereIn('entity_id',$this->q('proposals')->when($focus,fn($q)=>$q->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS))->when($salesInvoice,fn($q)=>$q->where('snapshot->source->invoice_id',$salesInvoice))->when($costOrder,fn($q)=>$q->where('snapshot->source->cost_order_id',$costOrder))->when($industrialLot,fn($q)=>$q->where('snapshot->source->industrial_lot_id',$industrialLot)->whereIn('snapshot->source->effect_type',['industry.cost.validated','industry.cost.reversal','industry.cost.replacement','assets.depreciation.reversal','assets.depreciation.replacement']))->select('id')))
->orWhere(fn($e)=>$e->where('kind','journal.reviewed')->whereIn('entity_id',$this->q('reviews')->whereIn('proposal_id',$this->q('proposals')->when($focus,fn($q)=>$q->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS))->when($salesInvoice,fn($q)=>$q->where('snapshot->source->invoice_id',$salesInvoice))->when($costOrder,fn($q)=>$q->where('snapshot->source->cost_order_id',$costOrder))->when($industrialLot,fn($q)=>$q->where('snapshot->source->industrial_lot_id',$industrialLot)->whereIn('snapshot->source->effect_type',['industry.cost.validated','industry.cost.reversal','industry.cost.replacement','assets.depreciation.reversal','assets.depreciation.replacement']))->select('id'))->select('id')))
->orWhere(fn($e)=>$e->where('kind','journal.posted')->whereIn('entity_id',$this->q('journals')->when($focus,fn($q)=>$q->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS))->when($salesInvoice,fn($q)=>$q->where('snapshot->source->invoice_id',$salesInvoice))->when($costOrder,fn($q)=>$q->where('snapshot->source->cost_order_id',$costOrder))->when($industrialLot,fn($q)=>$q->where('snapshot->source->industrial_lot_id',$industrialLot)->whereIn('snapshot->source->effect_type',['industry.cost.validated','industry.cost.reversal','industry.cost.replacement','assets.depreciation.reversal','assets.depreciation.replacement']))->select('id')));
}))->orderByDesc('id')->paginate(25,['*'],'page',$page);
return ['synthetic_only'=>true,'journals'=>$pager->items(),'lines'=>$lines->all(),'proposals'=>$proposals->items(),'reviews'=>$reviews->all(),'accounts'=>$withAccounts?$this->q('accounts')->orderBy('id')->get()->all():[],
'events'=>$eventPager->items(),'totals'=>['debit_minor'=>(int)$sum->debit_minor,'credit_minor'=>(int)$sum->credit_minor,'net_minor'=>(int)$sum->debit_minor-(int)$sum->credit_minor],