e0dfc3ec82
API CI/CD / Validate (composer + pint) (push) Successful in 3m15s
API CI/CD / Test (PHPUnit) (push) Failing after 5m4s
API CI/CD / Build frontend assets (push) Successful in 1m3s
API CI/CD / Security audit (push) Failing after 49s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
109 lines
3.8 KiB
PHP
109 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\PurchaseOrders;
|
|
|
|
use App\Models\PurchaseOrder;
|
|
use App\Models\PurchaseOrderItem;
|
|
use App\Models\Supply;
|
|
use App\Models\SupplyTransaction;
|
|
use App\Services\Inventory\InventoryMovementService;
|
|
use Illuminate\Support\Facades\DB;
|
|
use RuntimeException;
|
|
|
|
class PurchaseOrderReceiveService
|
|
{
|
|
private InventoryMovementService $inventoryMovementService;
|
|
|
|
public function __construct(?InventoryMovementService $inventoryMovementService = null)
|
|
{
|
|
$this->inventoryMovementService = $inventoryMovementService ?? app(InventoryMovementService::class);
|
|
}
|
|
|
|
public function receive(int $poId, array $received, string $issuedBy): array
|
|
{
|
|
$po = PurchaseOrder::query()->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,
|
|
];
|
|
});
|
|
}
|
|
}
|