commit de compras

This commit is contained in:
2026-11-04 09:37:20 +02:00
parent fcf31e8d48
commit 0b05a1192c
741 changed files with 114127 additions and 116 deletions
@@ -0,0 +1,28 @@
<?php
namespace App\Modules\Purchasing;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\{Crypt,DB,Gate,Validator};
use Tempo\Purchasing\Embedded\Models\{GoodsReceipt,PurchaseOrder,StockItem,Warehouse};
use Tempo\Purchasing\Embedded\Services\ReceiptWorkflow;
final class ReceiptController
{
private function canConfirm():bool{$ctx=app(\App\Shared\Context\CompanyContext::class);$s=$ctx->identifiers();return app(\App\Modules\Identity\Services\Permissions::class)->has($ctx->userId(),$s['tenant_id'],$s['company_id'],'purchasing_receipts.confirm',$ctx->unitId());}
private function order(int $id,array $binding):PurchaseOrder{$order=PurchaseOrder::where('id',$id)->where(fn($q)=>$q->whereIn('id',$binding['order_ids'])->orWhereIn('requisition_id',RequisitionAccess::ids($binding)))->first();abort_unless($order,403);Gate::authorize('viewOrder',[GoodsReceipt::class,$order]);return $order;}
private function revision(PurchaseOrder $order,array $binding):string{return hash('sha256',json_encode([$order->getAttributes(),app(ReceiptWorkflow::class)->progress($order),$binding],JSON_THROW_ON_ERROR));}
private function result(GoodsReceipt $receipt,bool $replayed):array{return ['receipt'=>$receipt->load('items')->toArray(),'replayed'=>$replayed,'movements'=>DB::table('stock_movements')->where('tenant_id',$receipt->tenant_id)->whereIn('goods_receipt_item_id',$receipt->items->pluck('id'))->orderBy('id')->get(['id','goods_receipt_item_id','quantity_milli','warehouse_id','stock_item_id','movement_type'])->all()];}
public function catalogue(int $id,int $line,Request $r,ReceiptRuntime $runtime) {
$d=$r->validate(['page'=>'sometimes|integer|min:1|max:10000','per_page'=>'sometimes|integer|in:25,100','search'=>'sometimes|nullable|string|max:100']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);
return $runtime->run('purchasing_receipts.confirm',function($actor,$binding)use($id,$line,$d){$order=$this->order($id,$binding);Gate::authorize('create',[GoodsReceipt::class,$order]);$source=$order->items()->find($line);abort_unless($source,403,'Linha fora da ordem autorizada.');$catalogue=app(ReceiptCatalogue::class);$q=$catalogue->query($order,$source);if(isset($d['search'])&&$d['search']!=='')$q->where(fn($w)=>$w->whereRaw('position(lower(?) in lower(code))>0',[$d['search']])->orWhereRaw('position(lower(?) in lower(name))>0',[$d['search']]));$page=$q->orderBy('code')->orderBy('id')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1));$page->through(fn($item)=>$item->only(['id','code','name','unit'])+['selection_revision'=>$catalogue->revision($item)]);return response()->json($page)->header('Cache-Control','no-store');});
}
public function history(int $id,Request $r,ReceiptRuntime $runtime) {
$d=$r->validate(['page'=>'sometimes|integer|min:1|max:10000']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);return $runtime->run('purchasing_receipts.view',function($actor,$binding)use($id,$d){$order=$this->order($id,$binding);return response()->json(GoodsReceipt::where('purchase_order_id',$order->id)->orderByDesc('id')->paginate(25,['id','number','status','received_on','received_by'],'page',(int)($d['page']??1)))->header('Cache-Control','no-store');});
}
public function screen(Request $r,ReceiptRuntime $runtime){$runtime->run('purchasing_receipts.view',fn()=>true);return response()->view('receipt-workspace',['context'=>$r->session()->get('company_context.token'),'csrf'=>csrf_token()]);}
public function index(Request $r,ReceiptRuntime $runtime){$d=$r->validate(['page'=>'sometimes|integer|min:1|max:10000','per_page'=>'sometimes|integer|in:25,100']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);return $runtime->run('purchasing_receipts.view',function($actor,$binding)use($d){Gate::authorize('viewAny',GoodsReceipt::class);$orders=PurchaseOrder::where(fn($q)=>$q->whereIn('id',$binding['order_ids'])->orWhereIn('requisition_id',RequisitionAccess::ids($binding)))->orderBy('id')->paginate((int)($d['per_page']??25),['id','number','status'], 'page',(int)($d['page']??1));return response()->json($orders);});}
public function show(int $id,Request $r,ReceiptRuntime $runtime){$d=$r->validate(['page'=>'sometimes|integer|min:1|max:10000']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);$can=$this->canConfirm();return $runtime->run('purchasing_receipts.view',function($actor,$binding)use($id,$d,$can){$order=$this->order($id,$binding);return response()->json(['order'=>$order->only(['id','number','status']),'progress'=>app(ReceiptWorkflow::class)->progress($order),'revision'=>$this->revision($order,$binding),'can_confirm'=>$can&&Gate::allows('create',[GoodsReceipt::class,$order]),'warehouses'=>Warehouse::whereIn('id',$binding['warehouse_ids'])->orderBy('id')->get(['id','code','name']),'items'=>StockItem::orderBy('id')->limit(25)->get(['id','code','name','unit']),'history'=>GoodsReceipt::where('purchase_order_id',$id)->orderByDesc('id')->paginate(25,['id','number','status','received_on','received_by'],'page',(int)($d['page']??1))]);});}
public function preview(int $id,Request $r,ReceiptRuntime $runtime){return $runtime->run('purchasing_receipts.confirm',function($actor,$binding,$fingerprint)use($id,$r){$order=$this->order($id,$binding);Gate::authorize('create',[GoodsReceipt::class,$order]);$input=$r->all();$revision=$input['revision']??null;$selected=$input['item_revisions']??null;unset($input['revision'],$input['item_revisions']);abort_if(array_diff(array_keys($input),['idempotency_key','delivery_note','received_on','warehouse_id','items']),422);$rules=(new \Tempo\Purchasing\Embedded\Http\Requests\ConfirmReceiptRequest)->setUserResolver(fn()=>$actor)->rules();$data=Validator::make($input,$rules)->validate();abort_unless(in_array((int)($data['warehouse_id']??0),$binding['warehouse_ids'],true),403);abort_unless(is_string($revision)&&hash_equals($this->revision($order,$binding),$revision),409,'A origem mudou. Actualize e confira novamente.');$items=app(ReceiptCatalogue::class)->selected($data);if($selected!==null){Validator::make(['selected'=>$selected],['selected'=>'array|max:50','selected.*'=>'required|string|size:64'])->validate();foreach($selected as$key=>$value)abort_unless(isset($items[(string)$key])&&hash_equals($items[(string)$key],$value),409,'O artigo mudou desde a escolha. Pesquise novamente.');} $cut=['item_revisions'=>$items,'actor'=>$actor->id,'fingerprint'=>$fingerprint,'order'=>$id,'revision'=>$revision,'data'=>$data,'observed'=>time()];return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>['document'=>'Recepção confirmada, com histórico imutável','stock'=>'Apenas quantidades físicas aceites geram entrada, uma única vez.','payments'=>false,'accounting'=>false],'data'=>$data]);});}
public function confirm(int $id,Request $r,ReceiptRuntime $runtime){$d=$r->validate(['preview'=>'required|string|max:100000']);abort_if(array_diff(array_keys($r->all()),['preview']),422);try{$cut=json_decode(Crypt::decryptString($d['preview']),true,128,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida. Confira novamente.');}return $runtime->run('purchasing_receipts.confirm',function($actor,$binding,$fingerprint,$origin)use($id,$cut){$order=$this->order($id,$binding);abort_unless(($cut['actor']??null)===$actor->id&&($cut['order']??null)===$id&&($cut['fingerprint']??null)===$fingerprint&&is_int($cut['observed']??null)&&$cut['observed']<=time()&&$cut['observed']>=time()-600,409,'A prévia ou o contexto mudou. Confira novamente.');$existing=GoodsReceipt::where('purchase_order_id',$id)->where('idempotency_key',$cut['data']['idempotency_key'])->first();abort_unless($existing||hash_equals($this->revision($order,$binding),$cut['revision']),409,'Recepção alterada por outra operação. Confira novamente.');if(!$existing){$current=app(ReceiptCatalogue::class)->selected($cut['data']);abort_unless(($cut['item_revisions']??null)===$current,409,'O artigo mudou desde a previa. Confira novamente.');}$receipt=app(ReceiptWorkflow::class)->confirm($actor,$order,$cut['data']);if(!$existing)app(\Tempo\Purchasing\Embedded\Services\RequisitionWorkflow::class)->audit($actor,'sgf.receipt.confirmed',\Tempo\Purchasing\Embedded\Models\Requisition::findOrFail($order->requisition_id),null,$origin+['receipt_id'=>$receipt->id,'binding_revision'=>$fingerprint]);return response()->json($this->result($receipt,(bool)$existing),$existing?200:201);});}
public function receipt(int $id,ReceiptRuntime $runtime){$can=$this->canConfirm();return $runtime->run('purchasing_receipts.view',function($actor,$binding)use($id,$can){$receipt=GoodsReceipt::findOrFail($id);$this->order($receipt->purchase_order_id,$binding);Gate::authorize('view',$receipt);return response()->json($this->result($receipt,true)+['documents_enabled'=>config('receipt_qa.receipt_documents_enabled')===true,'document_revision'=>hash('sha256',\Tempo\Finance\Payables\Payables::canonical([$receipt->getAttributes()])),'can_attach'=>$can&&config('receipt_qa.receipt_documents_enabled')===true&&app(\Tempo\Purchasing\Embedded\Services\ReceiptDocuments::class)->canAdd($actor,$receipt)]);});}
}