find($poId); if (!$po || in_array($po->status, [PurchaseOrder::STATUS_CANCELED, PurchaseOrder::STATUS_RECEIVED], true)) { throw new RuntimeException('PO not receivable.'); } if (empty($received)) { throw new RuntimeException('No items to receive.'); } return DB::transaction(function () use ($po, $received, $issuedBy) { $completed = true; foreach ($received as $item) { $itemId = (int) ($item['id'] ?? 0); $qty = (int) ($item['quantity'] ?? 0); if ($itemId <= 0 || $qty <= 0) { continue; } $poItem = PurchaseOrderItem::query() ->where('purchase_order_id', $po->id) ->where('id', $itemId) ->first(); if (!$poItem) { $completed = false; continue; } $remaining = (int) $poItem->quantity - (int) $poItem->received_qty; $toReceive = min($remaining, $qty); if ($toReceive <= 0) { continue; } $poItem->received_qty = (int) $poItem->received_qty + $toReceive; $poItem->save(); // Update supply qty_on_hand (legacy support) $supply = Supply::query()->find($poItem->supply_id); if ($supply) { $supply->qty_on_hand = (int) $supply->qty_on_hand + $toReceive; $supply->save(); SupplyTransaction::query()->create([ 'supply_id' => $supply->id, 'type' => 'in', 'quantity' => $toReceive, 'ref' => 'PO ' . ($po->po_number ?? $po->id), 'issued_to' => 'Inventory', 'issued_by' => $issuedBy, 'notes' => 'Received against PO', ]); } // Create inventory movement if the PO item is linked to an inventory item $inventoryItemId = $poItem->inventory_item_id; if ($inventoryItemId) { $performedBy = is_numeric($issuedBy) ? (int) $issuedBy : null; $this->inventoryMovementService->recordMovement( itemId: (int) $inventoryItemId, qtyChange: $toReceive, type: 'in', reason: 'Purchase order received', note: 'PO ' . ($po->po_number ?? $po->id), performedBy: $performedBy ); } if ($poItem->received_qty < $poItem->quantity) { $completed = false; } } $po->status = $completed ? PurchaseOrder::STATUS_RECEIVED : PurchaseOrder::STATUS_ORDERED; $po->save(); return [ 'status' => $po->status, 'completed' => $completed, ]; }); } }