commit melhorias sgf
This commit is contained in:
@@ -23,29 +23,29 @@ final class WorkshopStock
|
||||
private function binding(array $s): array
|
||||
{
|
||||
$b=config('workshop_stock.synthetic_sources.'.implode(':',$s));
|
||||
abort_unless($b&&preg_match('/^sgf_stock_read_[a-f0-9]{12}$/D',$b['schema']??'')&&!isset($b['connection'])
|
||||
abort_unless($b&&(preg_match('/^sgf_stock_read_[a-f0-9]{12}$/D',$b['schema']??'')||(ComposedWorkshopStockBook::active()&&($b['schema']??null)==='public'))&&!isset($b['connection'])
|
||||
&&($b['lots']??true)===false&&($b['reservations']??true)===false&&($b['spare_parts']??true)===false,503,'Fonte local de consumo ainda não ligada ou fora do recorte sem lotes/reservas/peças.');return $b;
|
||||
}
|
||||
private function lock(array $s): void {DB::table('companies')->where('tenant_id',$s['tenant_id'])->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();}
|
||||
private function allocationTable(array $b): ?string
|
||||
{
|
||||
$table=$b['schema'].'.ws_allocations';
|
||||
if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=DB::getSchemaBuilder()->hasTable($table);
|
||||
if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=ComposedWorkshopStockBook::hasTable($table);
|
||||
return $this->allocationTables[$table]?$table:null;
|
||||
}
|
||||
private function returnTable(array $b): ?string
|
||||
{
|
||||
$table=$b['schema'].'.ws_returns';if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=DB::getSchemaBuilder()->hasTable($table);return $this->allocationTables[$table]?$table:null;
|
||||
$table=$b['schema'].'.ws_returns';if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=ComposedWorkshopStockBook::hasTable($table);return $this->allocationTables[$table]?$table:null;
|
||||
}
|
||||
private function closureTable(array $b): ?string
|
||||
{
|
||||
$table=$b['schema'].'.ws_closures';
|
||||
if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=DB::getSchemaBuilder()->hasTable($table);
|
||||
if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=ComposedWorkshopStockBook::hasTable($table);
|
||||
return $this->allocationTables[$table]?$table:null;
|
||||
}
|
||||
private function closure(array $s,array $b,int $id): ?object
|
||||
{
|
||||
return ($table=$this->closureTable($b))?DB::table($table)->where($s)->where('proposal_id',$id)->first():null;
|
||||
return ($table=$this->closureTable($b))?ComposedWorkshopStockBook::table($table)->where($s)->where('proposal_id',$id)->first():null;
|
||||
}
|
||||
private function proposalResult(array $s,array $b,object $p): array
|
||||
{
|
||||
@@ -61,7 +61,7 @@ final class WorkshopStock
|
||||
$right=$action==='withdrawn'?'prepare':'confirm';$s=$this->scope($right);$b=$this->binding($s);$actor=$this->context->userId();
|
||||
return DB::transaction(function()use($id,$action,$version,$sourceHash,$reason,$right,$s,$b,$actor){
|
||||
$this->lock($s);$this->scope($right);$table=$this->closureTable($b);abort_unless($table,503,'Encerramento de propostas ainda não instalado nesta fonte.');
|
||||
$p=DB::table($b['schema'].'.ws_proposals')->where($s)->where('id',$id)->lockForUpdate()->first();abort_unless($p,403);
|
||||
$p=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->where($s)->where('id',$id)->lockForUpdate()->first();abort_unless($p,403);
|
||||
abort_unless($action==='withdrawn'?(int)$p->preparer_id===$actor:(int)$p->preparer_id!==$actor,403,'Retirada exige o preparador; recusa exige outra identidade autorizada.');
|
||||
abort_unless($version===1&&hash_equals($p->source_hash,$sourceHash),409,'A proposta apresentada mudou. Consulte novamente o histórico.');
|
||||
if($old=$this->closure($s,$b,$id)){
|
||||
@@ -70,18 +70,18 @@ final class WorkshopStock
|
||||
}
|
||||
abort_if($this->proposalClaim($b,$id),409,'Tratamento já confirmado; exige correcção física própria.');
|
||||
// Closing an obsolete preview deliberately does not execute or revalidate its physical effect.
|
||||
$decision=DB::table($table)->insertGetId($s+['proposal_id'=>$id,'actor_id'=>$actor,'source_hash'=>$sourceHash,'reason'=>$reason,'before_state'=>'pending','state'=>$action,'state_version'=>2,'created_at'=>now()]);
|
||||
$decision=ComposedWorkshopStockBook::table($table)->insertGetId($s+['proposal_id'=>$id,'actor_id'=>$actor,'source_hash'=>$sourceHash,'reason'=>$reason,'before_state'=>'pending','state'=>$action,'state_version'=>2,'created_at'=>now()]);
|
||||
Audit::record('workshop_stock.'.$action,$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['area'=>'workshop_stock_proposal','record_id'=>$id,'decision_id'=>$decision,'version'=>2,'review_hash'=>$sourceHash,'reason'=>$reason]);
|
||||
return ['id'=>$id,'state'=>$action,'state_version'=>2,'decision_id'=>$decision,'replayed'=>false,'physical_effect_milli'=>'0'];
|
||||
});
|
||||
}
|
||||
private function originClaimed(array $b,string $origin): bool
|
||||
{
|
||||
return DB::table($b['schema'].'.ws_claims')->where('origin_key',$origin)->exists()||(($t=$this->allocationTable($b))&&DB::table($t)->where('origin_key',$origin)->exists())||(($t=$this->returnTable($b))&&DB::table($t)->where('origin_key',$origin)->exists());
|
||||
return ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->where('origin_key',$origin)->exists()||(($t=$this->allocationTable($b))&&ComposedWorkshopStockBook::table($t)->where('origin_key',$origin)->exists())||(($t=$this->returnTable($b))&&ComposedWorkshopStockBook::table($t)->where('origin_key',$origin)->exists());
|
||||
}
|
||||
private function proposalClaim(array $b,int $id): ?object
|
||||
{
|
||||
return DB::table($b['schema'].'.ws_claims')->where('proposal_id',$id)->first()??(($t=$this->allocationTable($b))?DB::table($t)->where('proposal_id',$id)->first():null)??(($t=$this->returnTable($b))?DB::table($t)->where('proposal_id',$id)->first():null);
|
||||
return ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->where('proposal_id',$id)->first()??(($t=$this->allocationTable($b))?ComposedWorkshopStockBook::table($t)->where('proposal_id',$id)->first():null)??(($t=$this->returnTable($b))?ComposedWorkshopStockBook::table($t)->where('proposal_id',$id)->first():null);
|
||||
}
|
||||
private function source(array $s,array $b,int $id,int $version,int $warehouse,bool $lock=false,bool $existingDelivery=false,string $event='consumed'): array
|
||||
{
|
||||
@@ -92,20 +92,20 @@ final class WorkshopStock
|
||||
abort_unless((int)$e->version===$version&&!$e->outside_authority&&!DB::table('wo_entries')->where('previous_id',$id)->exists(),409,'Origem antiga ou fora da autorização. Faça nova conferência.');
|
||||
$decision=DB::table('wo_decisions')->where(['entry_id'=>$id,'entry_version'=>$version,'decision'=>'verified'])->first();abort_unless($decision&&(int)$decision->actor_id!==(int)$e->actor_id,409,'Consumo sem verificação técnica independente.');
|
||||
$map=$b['resources'][$d['resource'].'|'.$d['unit']]??null;abort_unless($map&&in_array($warehouse,$map['warehouse_ids'],true),403,'Sem correspondência explícita de recurso/unidade/armazém.');
|
||||
$item=DB::table($b['schema'].'.stock_items')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$map['stock_item_id']])->first();
|
||||
$item=ComposedWorkshopStockBook::table($b['schema'].'.stock_items')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$map['stock_item_id']])->first();
|
||||
abort_unless($item&&$item->unit===$map['stock_unit']&&strtoupper(trim($item->unit))===$d['unit'],409,'Unidade de stock incompatível; sem conversão automática.');
|
||||
abort_unless(DB::table($b['schema'].'.warehouses')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$warehouse])->exists(),403);
|
||||
abort_unless(ComposedWorkshopStockBook::table($b['schema'].'.warehouses')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$warehouse])->exists(),403);
|
||||
try{$quantity=BigDecimal::of((string)$e->quantity)->multipliedBy(1000)->toScale(0)->toInt();}catch(\Brick\Math\Exception\MathException){abort(422,'Quantidade não representável exactamente em milésimos.');}abort_unless($quantity>0,422);
|
||||
$origin=hash('sha256',json_encode([$s,$e->order_id,$e->origin],JSON_THROW_ON_ERROR));
|
||||
// Do not deduct consumption again after an unlinked physical delivery/issue.
|
||||
$unknownQuery=DB::table($b['schema'].'.stock_movements AS m')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.stock_item_id'=>$item->id,'m.warehouse_id'=>$warehouse])->where('m.quantity_milli','<',0)
|
||||
$unknownQuery=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements AS m')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.stock_item_id'=>$item->id,'m.warehouse_id'=>$warehouse])->where('m.quantity_milli','<',0)
|
||||
->whereNotExists(fn($x)=>$x->selectRaw('1')->from($b['schema'].'.ws_claims AS c')->whereColumn('c.movement_id','m.id'));
|
||||
$accounted='0';if($t=$this->allocationTable($b))$accounted.=' + (SELECT coalesce(sum(a.quantity_milli),0) FROM '.$t.' AS a WHERE a.movement_id=m.id)';
|
||||
if($t=$this->returnTable($b))$accounted.=' + (SELECT coalesce(sum(r.quantity_milli),0) FROM '.$t.' AS r WHERE r.parent_movement_id=m.id)';
|
||||
$unknownQuery->whereRaw('('.$accounted.') < -m.quantity_milli');
|
||||
$unknown=$unknownQuery->exists();
|
||||
abort_if($unknown&&!$existingDelivery,409,'Existe saída física sem correspondência reconciliada. Não se volta a abater a entrega como consumo.');
|
||||
$book=DB::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'stock_item_id'=>$item->id,'warehouse_id'=>$warehouse])->selectRaw('coalesce(sum(quantity_milli),0)::text AS quantity,coalesce(max(id),0)::text AS revision')->first();
|
||||
$book=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'stock_item_id'=>$item->id,'warehouse_id'=>$warehouse])->selectRaw('coalesce(sum(quantity_milli),0)::text AS quantity,coalesce(max(id),0)::text AS revision')->first();
|
||||
abort_unless($existingDelivery||BigDecimal::of($book->quantity)->isGreaterThanOrEqualTo($quantity),409,'Stock físico insuficiente.');
|
||||
return ['entry'=>(array)$e,'decision'=>(array)$decision,'binding'=>$b,'origin_key'=>$origin,'stock_item_id'=>(int)$item->id,'warehouse_id'=>$warehouse,'quantity_milli'=>$quantity,'unit'=>$item->unit,'book'=>(array)$book];
|
||||
}
|
||||
@@ -118,37 +118,37 @@ final class WorkshopStock
|
||||
private function delivery(array $v,array $b,int $movementId,bool $lock=false,bool $partial=false,bool $returning=false): array
|
||||
{
|
||||
abort_unless(($b['eligible_deliveries'][$movementId]['order_id']??null)===(int)$v['entry']['order_id'],403,'Entrega sem elegibilidade explícita para esta ordem.');
|
||||
$q=DB::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$movementId,'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']]);if($lock)$q->lockForUpdate();
|
||||
$q=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$movementId,'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']]);if($lock)$q->lockForUpdate();
|
||||
$m=$q->first();abort_unless($m,403);
|
||||
$q=DB::table($b['schema'].'.stock_operations')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$m->stock_operation_id,'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']]);if($lock)$q->lockForUpdate();
|
||||
$q=ComposedWorkshopStockBook::table($b['schema'].'.stock_operations')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$m->stock_operation_id,'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']]);if($lock)$q->lockForUpdate();
|
||||
$o=$q->first();abort_unless($o&&$o->kind==='issue'&&$m->movement_type==='issue'&&$m->equipment_id===null&&$m->spare_part_id===null,409,'Movimento incompatível com entrega simples.');
|
||||
$snapshot=json_decode($m->snapshot,true,512,JSON_THROW_ON_ERROR);
|
||||
abort_unless(($snapshot['inventory_operation_hash']??null)===$this->hash((array)$o),409,'Entrega sem fotografia íntegra do escritor comum; exige conferência histórica própria.');
|
||||
abort_if(DB::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']])->whereNotIn('movement_type',['receipt_in','issue','workshop_return_in'])->exists(),409,'Livro com estornos, transferências ou ajustes fora deste ensaio simples.');
|
||||
abort_if(ComposedWorkshopStockBook::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']])->whereNotIn('movement_type',['receipt_in','issue','workshop_return_in'])->exists(),409,'Livro com estornos, transferências ou ajustes fora deste ensaio simples.');
|
||||
$returns=$this->returnTable($b);
|
||||
$q=DB::table($b['schema'].'.stock_movements AS m')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.stock_item_id'=>$v['stock_item_id'],'m.warehouse_id'=>$v['warehouse_id'],'m.movement_type'=>'workshop_return_in']);
|
||||
$q=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements AS m')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.stock_item_id'=>$v['stock_item_id'],'m.warehouse_id'=>$v['warehouse_id'],'m.movement_type'=>'workshop_return_in']);
|
||||
if($returns)$q->whereNotExists(fn($x)=>$x->selectRaw('1')->from($returns.' AS r')->whereColumn('r.movement_id','m.id'));
|
||||
abort_if($q->exists(),409,'Entrada de devolução sem decisão rastreável.');
|
||||
$total=BigDecimal::of((string)$m->quantity_milli)->negated();
|
||||
abort_unless($total->isPositive()&&$total->isEqualTo((string)$o->quantity_milli),409,'Quantidade da entrega incompatível com a operação.');
|
||||
$allocations=$this->allocationTable($b);$used=$allocations?(string)DB::table($allocations)->where('movement_id',$movementId)->sum('quantity_milli'):'0';
|
||||
$returned=$returns?(string)DB::table($returns)->where('parent_movement_id',$movementId)->sum('quantity_milli'):'0';$remaining=$total->minus($used)->minus($returned);
|
||||
$allocations=$this->allocationTable($b);$used=$allocations?(string)ComposedWorkshopStockBook::table($allocations)->where('movement_id',$movementId)->sum('quantity_milli'):'0';
|
||||
$returned=$returns?(string)ComposedWorkshopStockBook::table($returns)->where('parent_movement_id',$movementId)->sum('quantity_milli'):'0';$remaining=$total->minus($used)->minus($returned);
|
||||
if($partial||$returning){abort_unless($returning?(($b['stock_returns']??false)&&$returns):(($b['partial_delivery_links']??false)&&$allocations),503,'Tratamento de repartição/devolução não configurado neste ensaio.');abort_unless($remaining->isGreaterThanOrEqualTo($v['quantity_milli']),409,'Quantidade não associada insuficiente. Faça nova conferência.');}
|
||||
else abort_unless($total->isEqualTo($v['quantity_milli'])&&BigDecimal::of($used)->isZero()&&BigDecimal::of($returned)->isZero(),409,'Entrega e consumo devem ter a mesma quantidade integral; use conferência de repartição quando aplicável.');
|
||||
abort_unless(\Carbon\CarbonImmutable::parse($m->moved_at)->lessThanOrEqualTo(\Carbon\CarbonImmutable::parse($v['entry']['starts_at'])),409,'A entrega deve preceder o consumo observado.');
|
||||
abort_if(DB::table($b['schema'].'.ws_claims')->where('movement_id',$movementId)->exists(),409,'Entrega já ligada a outra decisão.');
|
||||
abort_if(ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->where('movement_id',$movementId)->exists(),409,'Entrega já ligada a outra decisão.');
|
||||
$v['delivery']=['movement'=>(array)$m,'operation'=>(array)$o,'partial'=>$partial,'returning'=>$returning,'allocated_milli'=>$used,'returned_milli'=>$returned,'remaining_milli'=>(string)$remaining];return $v;
|
||||
}
|
||||
public function prepareDelivery(array $d,string $key,bool $partial=false,bool $returning=false): array
|
||||
{
|
||||
$s=$this->scope('prepare');$b=$this->binding($s);$actor=$this->context->userId();$request=['mode'=>$returning?'return_existing_delivery':($partial?'allocate_existing_delivery':'link_existing_delivery')]+$d;
|
||||
return DB::transaction(function()use($s,$b,$actor,$d,$key,$request,$partial,$returning){$this->lock($s);$this->scope('prepare');
|
||||
$old=DB::table($b['schema'].'.ws_proposals')->where($s)->where(['preparer_id'=>$actor,'command_key'=>$key])->first();
|
||||
$old=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->where($s)->where(['preparer_id'=>$actor,'command_key'=>$key])->first();
|
||||
if($old){abort_unless(hash_equals($old->request_hash,$this->hash($request)),409,'Chave repetida com conteúdo diferente.');return $this->proposalResult($s,$b,$old);}
|
||||
$v=$this->source($s,$b,$d['entry_id'],$d['version'],$d['warehouse_id'],true,true,$returning?'returned':'consumed');
|
||||
abort_if($this->originClaimed($b,$v['origin_key']),409,'Origem já ligada; exige correcção própria.');
|
||||
$v=$this->delivery($v,$b,$d['movement_id'],true,$partial,$returning);
|
||||
$id=DB::table($b['schema'].'.ws_proposals')->insertGetId($s+['entry_id'=>$d['entry_id'],'entry_version'=>$d['version'],'preparer_id'=>$actor,'reason'=>$d['reason'],'evidence'=>$d['evidence'],'command_key'=>$key,'request_hash'=>$this->hash($request),'source_hash'=>$this->hash($v),'snapshot'=>json_encode($v,JSON_THROW_ON_ERROR),'created_at'=>now()]);
|
||||
$id=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->insertGetId($s+['entry_id'=>$d['entry_id'],'entry_version'=>$d['version'],'preparer_id'=>$actor,'reason'=>$d['reason'],'evidence'=>$d['evidence'],'command_key'=>$key,'request_hash'=>$this->hash($request),'source_hash'=>$this->hash($v),'snapshot'=>json_encode($v,JSON_THROW_ON_ERROR),'created_at'=>now()]);
|
||||
Audit::record($returning?'workshop_stock.return_prepared':'workshop_stock.delivery_prepared',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['proposal_id'=>$id,'entry_id'=>$d['entry_id'],'version'=>$d['version'],'movement_id'=>$d['movement_id'],'physical_effect'=>$returning?(string)$v['quantity_milli']:'0']);
|
||||
return ['id'=>$id,'impact'=>$this->impact($v),'reason'=>$d['reason'],'evidence'=>$d['evidence'],'source_hash'=>$this->hash($v),'state'=>'pending','state_version'=>1,'replayed'=>false];});
|
||||
}
|
||||
@@ -160,7 +160,7 @@ final class WorkshopStock
|
||||
->where(['e.kind'=>'material','e.outside_authority'=>false,'d.decision'=>'verified'])->whereRaw("e.data::jsonb->>'owner'='company'")->whereRaw(($b['stock_returns']??false)?"e.data::jsonb->>'event' IN ('consumed','returned')":"e.data::jsonb->>'event'='consumed'")
|
||||
->whereNotExists(fn($x)=>$x->selectRaw('1')->from('wo_entries AS n')->whereColumn('n.previous_id','e.id'))
|
||||
->select(['e.id','e.order_id','e.origin','e.version','e.quantity','e.data'])->orderBy('e.id')->paginate(25,['*'],'page',$page);
|
||||
$proposalQuery=DB::table($b['schema'].'.ws_proposals AS p')->where($s);
|
||||
$proposalQuery=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals AS p')->where($s);
|
||||
$closureTable=$this->closureTable($b);
|
||||
if(in_array($status,['withdrawn','rejected'],true)){
|
||||
if($closureTable)$proposalQuery->whereExists(fn($x)=>$x->selectRaw('1')->from($closureTable.' AS z')->whereColumn('z.proposal_id','p.id')->where('z.state',$status));
|
||||
@@ -172,14 +172,29 @@ final class WorkshopStock
|
||||
});
|
||||
if($status==='pending'&&$closureTable)$proposalQuery->whereNotExists(fn($x)=>$x->selectRaw('1')->from($closureTable.' AS z')->whereColumn('z.proposal_id','p.id'));
|
||||
$proposalPager=$proposalQuery->orderByDesc('p.id')->paginate(25,['p.id','p.entry_id','p.entry_version','p.preparer_id','p.reason','p.evidence','p.snapshot','p.source_hash','p.created_at'],'proposal_page',$proposalPage);$proposals=$proposalPager->getCollection();
|
||||
$keys=array_map(fn($e)=>$this->hash([$s,$e->order_id,$e->origin]),$q->items());$linked=DB::table($b['schema'].'.ws_claims')->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key');
|
||||
if($t=$this->allocationTable($b))$linked=$linked->merge(DB::table($t)->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key'));
|
||||
if($t=$this->returnTable($b))$linked=$linked->merge(DB::table($t)->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key'));
|
||||
foreach($q->items() as $e)$e->movement_id=$linked->get($this->hash([$s,$e->order_id,$e->origin]))?->movement_id;
|
||||
$claims=DB::table($b['schema'].'.ws_claims')->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id'])->keyBy('proposal_id');
|
||||
if($t=$this->allocationTable($b))foreach(DB::table($t)->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id']) as $a)$claims->put($a->proposal_id,$a);
|
||||
if($t=$this->returnTable($b))foreach(DB::table($t)->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id']) as $a)$claims->put($a->proposal_id,$a);
|
||||
$closed=$closureTable?DB::table($closureTable)->where($s)->whereIn('proposal_id',$proposals->pluck('id'))->get()->keyBy('proposal_id'):collect();
|
||||
$keys=array_map(fn($e)=>$this->hash([$s,$e->order_id,$e->origin]),$q->items());$linked=ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key');
|
||||
if($t=$this->allocationTable($b))$linked=$linked->merge(ComposedWorkshopStockBook::table($t)->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key'));
|
||||
if($t=$this->returnTable($b))$linked=$linked->merge(ComposedWorkshopStockBook::table($t)->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key'));
|
||||
$physicalWarehouses=collect();$physicalDeliveries=collect();
|
||||
if(ComposedWorkshopStockBook::active()){
|
||||
$warehouses=[];$articles=[];foreach($q->items() as $entry){$data=json_decode($entry->data,true);$map=$b['resources'][$data['resource'].'|'.$data['unit']]??null;if($map){$warehouses=[...$warehouses,...$map['warehouse_ids']];$articles[]=$map['stock_item_id'];}}
|
||||
$orders=array_column($q->items(),'order_id');$deliveries=[];foreach($b['eligible_deliveries'] as $id=>$known)if(in_array($known['order_id'],$orders,true))$deliveries[]=(int)$id;
|
||||
$physicalWarehouses=ComposedWorkshopStockBook::table($b['schema'].'.warehouses')->where('tenant_id',$b['source_tenant_id'])->whereIn('id',array_unique($warehouses))->orderBy('id')->get(['id','code','name']);
|
||||
$physicalDeliveries=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements as m')->join($b['schema'].'.stock_operations as o','o.id','=','m.stock_operation_id')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.movement_type'=>'issue'])->whereIn('m.stock_item_id',array_unique($articles))->whereIn('m.id',$deliveries)->whereIn('m.warehouse_id',array_unique($warehouses))->orderBy('m.id')->get(['m.id','m.stock_item_id','o.reference','m.warehouse_id','m.quantity_milli']);
|
||||
}
|
||||
foreach($q->items() as $e){
|
||||
$e->movement_id=$linked->get($this->hash([$s,$e->order_id,$e->origin]))?->movement_id;
|
||||
if(ComposedWorkshopStockBook::active()){
|
||||
$data=json_decode($e->data,true);$map=$b['resources'][$data['resource'].'|'.$data['unit']]??null;
|
||||
$e->warehouses=$physicalWarehouses->whereIn('id',$map['warehouse_ids']??[])->values()->all();
|
||||
$deliveries=[];foreach(($b['eligible_deliveries']??[]) as $id=>$known)if($known['order_id']===$e->order_id)$deliveries[]=(int)$id;
|
||||
$e->deliveries=$physicalDeliveries->where('stock_item_id',$map['stock_item_id']??0)->whereIn('id',$deliveries)->whereIn('warehouse_id',$map['warehouse_ids']??[])->values()->all();
|
||||
}
|
||||
}
|
||||
$claims=ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id'])->keyBy('proposal_id');
|
||||
if($t=$this->allocationTable($b))foreach(ComposedWorkshopStockBook::table($t)->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id']) as $a)$claims->put($a->proposal_id,$a);
|
||||
if($t=$this->returnTable($b))foreach(ComposedWorkshopStockBook::table($t)->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id']) as $a)$claims->put($a->proposal_id,$a);
|
||||
$closed=$closureTable?ComposedWorkshopStockBook::table($closureTable)->where($s)->whereIn('proposal_id',$proposals->pluck('id'))->get()->keyBy('proposal_id'):collect();
|
||||
foreach($proposals as $p){$p->impact=$this->impact(json_decode($p->snapshot,true,512,JSON_THROW_ON_ERROR));unset($p->snapshot);$p->movement_id=$claims->get($p->id)?->movement_id;$p->closure=$closed->get($p->id);$p->state=$p->closure?->state??($p->movement_id?'confirmed':'pending');$p->state_version=$p->state==='pending'?1:2;
|
||||
$p->can_withdraw=$closureTable&&$p->state==='pending'&&(int)$p->preparer_id===$this->context->userId();$p->can_review=$p->state==='pending'&&(int)$p->preparer_id!==$this->context->userId();}
|
||||
return ['sources'=>$q->items(),'pagination'=>['page'=>$q->currentPage(),'last_page'=>$q->lastPage(),'total'=>$q->total()],
|
||||
@@ -189,11 +204,11 @@ final class WorkshopStock
|
||||
{
|
||||
$s=$this->scope('prepare');$b=$this->binding($s);$actor=$this->context->userId();
|
||||
return DB::transaction(function()use($s,$b,$actor,$d,$key){$this->lock($s);$this->scope('prepare');
|
||||
$old=DB::table($b['schema'].'.ws_proposals')->where($s)->where(['preparer_id'=>$actor,'command_key'=>$key])->first();
|
||||
$old=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->where($s)->where(['preparer_id'=>$actor,'command_key'=>$key])->first();
|
||||
if($old){abort_unless(hash_equals($old->request_hash,$this->hash($d)),409,'Chave repetida com conteúdo diferente.');return $this->proposalResult($s,$b,$old);}
|
||||
$v=$this->source($s,$b,$d['entry_id'],$d['version'],$d['warehouse_id'],true);
|
||||
abort_if($this->originClaimed($b,$v['origin_key']),409,'Origem já consumida; correcções exigem compensação própria.');
|
||||
$id=DB::table($b['schema'].'.ws_proposals')->insertGetId($s+['entry_id'=>$d['entry_id'],'entry_version'=>$d['version'],'preparer_id'=>$actor,'reason'=>$d['reason'],'evidence'=>$d['evidence'],
|
||||
$id=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->insertGetId($s+['entry_id'=>$d['entry_id'],'entry_version'=>$d['version'],'preparer_id'=>$actor,'reason'=>$d['reason'],'evidence'=>$d['evidence'],
|
||||
'command_key'=>$key,'request_hash'=>$this->hash($d),'source_hash'=>$this->hash($v),'snapshot'=>json_encode($v,JSON_THROW_ON_ERROR),'created_at'=>now()]);
|
||||
Audit::record('workshop_stock.prepared',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['proposal_id'=>$id,'entry_id'=>$d['entry_id'],'version'=>$d['version']]);
|
||||
return ['id'=>$id,'impact'=>$this->impact($v),'reason'=>$d['reason'],'evidence'=>$d['evidence'],'source_hash'=>$this->hash($v),'state'=>'pending','state_version'=>1,'replayed'=>false];});
|
||||
@@ -202,7 +217,7 @@ final class WorkshopStock
|
||||
{
|
||||
$s=$this->scope('confirm');$b=$this->binding($s);$actor=$this->context->userId();
|
||||
return DB::transaction(function()use($s,$b,$actor,$id,$reason,$receivedInWarehouse){$this->lock($s);$this->scope('confirm');
|
||||
$p=DB::table($b['schema'].'.ws_proposals')->where($s)->where('id',$id)->lockForUpdate()->first();abort_unless($p,403);abort_if((int)$p->preparer_id===$actor,403,'Confirmação deve ser independente da preparação.');
|
||||
$p=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->where($s)->where('id',$id)->lockForUpdate()->first();abort_unless($p,403);abort_if((int)$p->preparer_id===$actor,403,'Confirmação deve ser independente da preparação.');
|
||||
abort_if($this->closure($s,$b,$id),409,'Proposta retirada ou recusada. Faça nova conferência; este original permanece encerrado.');
|
||||
$old=$this->proposalClaim($b,$id);if($old)return ['ok'=>true,'replayed'=>true,'movement_id'=>(int)$old->movement_id,'claim_id'=>(int)$old->id];
|
||||
$frozen=json_decode($p->snapshot,true,512,JSON_THROW_ON_ERROR);$link=isset($frozen['delivery']);$returning=$link&&($frozen['delivery']['returning']??false);$v=$this->source($s,$b,(int)$p->entry_id,(int)$p->entry_version,$frozen['warehouse_id'],true,$link,$returning?'returned':'consumed');
|
||||
@@ -213,17 +228,19 @@ final class WorkshopStock
|
||||
abort_unless(hash_equals($p->source_hash,$this->hash($v)),409,'Origem, mapa ou livro mudou. Faça nova conferência.');
|
||||
if($link&&!$returning){$operation=(int)$v['delivery']['operation']['id'];$out=['movement_id'=>(int)$v['delivery']['movement']['id']];}
|
||||
else {
|
||||
$operation=DB::table($b['schema'].'.stock_operations')->insertGetId(['tenant_id'=>$b['source_tenant_id'],'created_by'=>$actor,'responsible_id'=>$actor,'warehouse_id'=>$v['warehouse_id'],'stock_item_id'=>$v['stock_item_id'],
|
||||
$operation=ComposedWorkshopStockBook::table($b['schema'].'.stock_operations')->insertGetId(['tenant_id'=>$b['source_tenant_id'],'created_by'=>ComposedWorkshopStockBook::actor($actor),'responsible_id'=>ComposedWorkshopStockBook::actor($actor),'warehouse_id'=>$v['warehouse_id'],'stock_item_id'=>$v['stock_item_id'],
|
||||
'kind'=>$returning?'workshop_return':'issue','reference'=>'SGF-WO-'.$v['origin_key'],'idempotency_key'=>(string)Str::uuid(),'payload_hash'=>$p->source_hash,'destination'=>'workshop-origin:'.$v['origin_key'],'reason'=>$reason,
|
||||
'quantity_milli'=>$v['quantity_milli'],'theoretical_milli'=>$v['book']['quantity'],'ledger_version'=>$v['book']['revision'],'recorded_at'=>now(),
|
||||
'snapshot'=>json_encode(($returning?['delivery_movement_id'=>(int)$v['delivery']['movement']['id'],'delivery_movement_hash'=>$this->hash($v['delivery']['movement']),'received_in_warehouse'=>$receivedInWarehouse]:[])+['workshop_origin'=>$v['entry'],'verification'=>$v['decision'],'proposal_id'=>$id,'preparer_id'=>$p->preparer_id,'scope'=>$s,'evidence'=>$p->evidence],JSON_THROW_ON_ERROR)]);
|
||||
$writer=$returning?\Tempo\Inventory\DeliveryReturnWriter::class:\Tempo\Inventory\IssueMovementWriter::class;
|
||||
$w=new $writer(DB::connection(),$b['schema'],$b['source_tenant_id'],$actor,function(){$this->scope('confirm');});$out=$w->write($operation,$w->preview($operation));
|
||||
$w=new $writer(ComposedWorkshopStockBook::connection(),$b['schema'],$b['source_tenant_id'],ComposedWorkshopStockBook::actor($actor),function(){$this->scope('confirm');});$out=$w->write($operation,$w->preview($operation));
|
||||
}
|
||||
$after=$this->impact($v)['physical_after'];
|
||||
$claim=DB::table($returning?$this->returnTable($b):($partial?$this->allocationTable($b):$b['schema'].'.ws_claims'))->insertGetId($s+($returning?['quantity_milli'=>$v['quantity_milli'],'parent_movement_id'=>(int)$v['delivery']['movement']['id']]:($partial?['quantity_milli'=>$v['quantity_milli']]:[]))+['origin_key'=>$v['origin_key'],'proposal_id'=>$id,'entry_id'=>$p->entry_id,'entry_version'=>$p->entry_version,'operation_id'=>$operation,'movement_id'=>$out['movement_id'],
|
||||
$claim=ComposedWorkshopStockBook::table($returning?$this->returnTable($b):($partial?$this->allocationTable($b):$b['schema'].'.ws_claims'))->insertGetId($s+($returning?['quantity_milli'=>$v['quantity_milli'],'parent_movement_id'=>(int)$v['delivery']['movement']['id']]:($partial?['quantity_milli'=>$v['quantity_milli']]:[]))+['origin_key'=>$v['origin_key'],'proposal_id'=>$id,'entry_id'=>$p->entry_id,'entry_version'=>$p->entry_version,'operation_id'=>$operation,'movement_id'=>$out['movement_id'],
|
||||
'preparer_id'=>$p->preparer_id,'confirmer_id'=>$actor,'reason'=>$reason,'before_milli'=>$v['book']['quantity'],'after_milli'=>$after,'source_hash'=>$p->source_hash,'created_at'=>now()]);
|
||||
Audit::record($returning?'workshop_stock.return_confirmed':($partial?'workshop_stock.delivery_allocated':($link?'workshop_stock.delivery_linked':'workshop_stock.confirmed')),$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['claim_id'=>$claim,'proposal_id'=>$id,'entry_id'=>$p->entry_id,'version'=>$p->entry_version,'movement_id'=>$out['movement_id'],'linked_quantity'=>$link?(string)$v['quantity_milli']:null,'remaining_after'=>($partial||$returning)?$this->impact($v)['unallocated_after']:null,'before'=>$v['book']['quantity'],'after'=>$after,'reason'=>$reason]);
|
||||
return ['ok'=>true,'replayed'=>false,'movement_id'=>$out['movement_id'],'claim_id'=>$claim];});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user