inventoryService = $inventoryService; $this->procurementService = $procurementService; } /** * Store a new Goods Receipt (Draft state). * * @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'] = GoodsReceipt::STATUS_DRAFT; // 預設草稿 // 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); } return $goodsReceipt; }); } /** * Update an existing Goods Receipt. * * @param GoodsReceipt $goodsReceipt * @param array $data * @return GoodsReceipt * @throws \Exception */ public function update(GoodsReceipt $goodsReceipt, array $data) { if (!in_array($goodsReceipt->status, [GoodsReceipt::STATUS_DRAFT, GoodsReceipt::STATUS_REJECTED])) { throw new \Exception('只有草稿或被退回的進貨單可以修改。'); } return DB::transaction(function () use ($goodsReceipt, $data) { $goodsReceipt->update([ 'vendor_id' => $data['vendor_id'] ?? $goodsReceipt->vendor_id, 'received_date' => $data['received_date'] ?? $goodsReceipt->received_date, 'remarks' => $data['remarks'] ?? $goodsReceipt->remarks, ]); if (isset($data['items'])) { // Simple strategy: delete existing items and recreate $goodsReceipt->items()->delete(); foreach ($data['items'] as $itemData) { $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); } } return $goodsReceipt->fresh('items'); }); } /** * Submit for audit (Confirm receipt by warehouse staff). * This will increase inventory and update PO. * * @param GoodsReceipt $goodsReceipt * @return GoodsReceipt * @throws \Exception */ public function submit(GoodsReceipt $goodsReceipt) { if (!in_array($goodsReceipt->status, [GoodsReceipt::STATUS_DRAFT, GoodsReceipt::STATUS_REJECTED])) { throw new \Exception('只有草稿或被退回的進貨單可以確認點收。'); } return DB::transaction(function () use ($goodsReceipt) { $goodsReceipt->status = GoodsReceipt::STATUS_COMPLETED; $goodsReceipt->save(); // Process Inventory and PO updates foreach ($goodsReceipt->items as $grItem) { // 1. 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, ]); // 2. 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 ); } } // Fire event to let Finance module create AP event(new GoodsReceiptApprovedEvent($goodsReceipt->id)); return $goodsReceipt; }); } private function generateCode(string $date) { // Format: GR-YYYYMMDD-NN $prefix = 'GR-' . date('Ymd', strtotime($date)) . '-'; $last = GoodsReceipt::where('code', 'like', $prefix . '%') ->orderBy('id', 'desc') ->lockForUpdate() ->first(); if ($last) { $seq = intval(substr($last->code, -2)) + 1; } else { $seq = 1; } return $prefix . str_pad($seq, 2, '0', STR_PAD_LEFT); } /** * 獲取指定的進貨單資訊 (實作 GoodsReceiptServiceInterface) * * @param int $goodsReceiptId * @return array|null */ public function getGoodsReceiptData(int $goodsReceiptId): ?array { $receipt = GoodsReceipt::with('items')->find($goodsReceiptId); if (!$receipt) { return null; } // 以陣列形式回傳資料,避免外部模組產生 Model 依賴 return $receipt->toArray(); } }