2026-01-27 13:27:28 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Modules\Inventory\Services;
|
|
|
|
|
|
|
|
|
|
use App\Modules\Inventory\Models\GoodsReceipt;
|
|
|
|
|
use App\Modules\Inventory\Models\GoodsReceiptItem;
|
|
|
|
|
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
|
|
|
|
use App\Modules\Procurement\Contracts\ProcurementServiceInterface;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
class GoodsReceiptService
|
|
|
|
|
{
|
|
|
|
|
protected $inventoryService;
|
|
|
|
|
protected $procurementService;
|
|
|
|
|
|
|
|
|
|
public function __construct(
|
|
|
|
|
InventoryServiceInterface $inventoryService,
|
|
|
|
|
ProcurementServiceInterface $procurementService
|
|
|
|
|
) {
|
|
|
|
|
$this->inventoryService = $inventoryService;
|
|
|
|
|
$this->procurementService = $procurementService;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Store a new Goods Receipt and process inventory.
|
|
|
|
|
*
|
|
|
|
|
* @param array $data
|
|
|
|
|
* @return GoodsReceipt
|
|
|
|
|
* @throws \Exception
|
|
|
|
|
*/
|
|
|
|
|
public function store(array $data)
|
|
|
|
|
{
|
|
|
|
|
return DB::transaction(function () use ($data) {
|
|
|
|
|
// 1. Generate Code
|
|
|
|
|
$data['code'] = $this->generateCode($data['received_date']);
|
|
|
|
|
$data['user_id'] = auth()->id();
|
|
|
|
|
$data['status'] = 'completed'; // Direct completion for now
|
|
|
|
|
|
|
|
|
|
// 2. Create Header
|
|
|
|
|
$goodsReceipt = GoodsReceipt::create($data);
|
|
|
|
|
|
|
|
|
|
// 3. Process Items
|
|
|
|
|
foreach ($data['items'] as $itemData) {
|
|
|
|
|
// Create GR Item
|
|
|
|
|
$grItem = new GoodsReceiptItem([
|
|
|
|
|
'product_id' => $itemData['product_id'],
|
|
|
|
|
'purchase_order_item_id' => $itemData['purchase_order_item_id'] ?? null,
|
|
|
|
|
'quantity_received' => $itemData['quantity_received'],
|
|
|
|
|
'unit_price' => $itemData['unit_price'],
|
|
|
|
|
'total_amount' => $itemData['quantity_received'] * $itemData['unit_price'],
|
|
|
|
|
'batch_number' => $itemData['batch_number'] ?? null,
|
|
|
|
|
'expiry_date' => $itemData['expiry_date'] ?? null,
|
|
|
|
|
]);
|
|
|
|
|
$goodsReceipt->items()->save($grItem);
|
|
|
|
|
|
|
|
|
|
// 4. Update Inventory
|
|
|
|
|
$reason = match($goodsReceipt->type) {
|
|
|
|
|
'standard' => '採購進貨',
|
|
|
|
|
'miscellaneous' => '雜項入庫',
|
|
|
|
|
'other' => '其他入庫',
|
|
|
|
|
default => '進貨入庫',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$this->inventoryService->createInventoryRecord([
|
|
|
|
|
'warehouse_id' => $goodsReceipt->warehouse_id,
|
|
|
|
|
'product_id' => $grItem->product_id,
|
|
|
|
|
'quantity' => $grItem->quantity_received,
|
|
|
|
|
'unit_cost' => $grItem->unit_price,
|
|
|
|
|
'batch_number' => $grItem->batch_number,
|
|
|
|
|
'expiry_date' => $grItem->expiry_date,
|
|
|
|
|
'reason' => $reason,
|
|
|
|
|
'reference_type' => GoodsReceipt::class,
|
|
|
|
|
'reference_id' => $goodsReceipt->id,
|
|
|
|
|
'source_purchase_order_id' => $goodsReceipt->purchase_order_id,
|
|
|
|
|
'arrival_date' => $goodsReceipt->received_date,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 5. Update PO if linked and type is standard
|
|
|
|
|
if ($goodsReceipt->type === 'standard' && $goodsReceipt->purchase_order_id && $grItem->purchase_order_item_id) {
|
|
|
|
|
$this->procurementService->updateReceivedQuantity(
|
|
|
|
|
$grItem->purchase_order_item_id,
|
|
|
|
|
$grItem->quantity_received
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $goodsReceipt;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function generateCode(string $date)
|
|
|
|
|
{
|
2026-02-04 13:08:05 +08:00
|
|
|
// Format: GR-YYYYMMDD-NN
|
|
|
|
|
$prefix = 'GR-' . date('Ymd', strtotime($date)) . '-';
|
2026-01-27 13:27:28 +08:00
|
|
|
|
|
|
|
|
$last = GoodsReceipt::where('code', 'like', $prefix . '%')
|
|
|
|
|
->orderBy('id', 'desc')
|
|
|
|
|
->lockForUpdate()
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($last) {
|
2026-02-04 13:08:05 +08:00
|
|
|
$seq = intval(substr($last->code, -2)) + 1;
|
2026-01-27 13:27:28 +08:00
|
|
|
} else {
|
|
|
|
|
$seq = 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 13:08:05 +08:00
|
|
|
return $prefix . str_pad($seq, 2, '0', STR_PAD_LEFT);
|
2026-01-27 13:27:28 +08:00
|
|
|
}
|
|
|
|
|
}
|