$fromWarehouseId, 'to_warehouse_id' => $toWarehouseId, 'status' => 'draft', 'remarks' => $remarks, 'created_by' => $userId, ]); } /** * 更新調撥單明細 */ /** * 更新調撥單明細 (支援精確 Diff 與自動日誌整合) */ public function updateItems(InventoryTransferOrder $order, array $itemsData): bool { return DB::transaction(function () use ($order, $itemsData) { // 1. 準備舊資料索引 (Key: product_id . '_' . batch_number) $oldItemsMap = $order->items->mapWithKeys(function ($item) { $key = $item->product_id . '_' . ($item->batch_number ?? ''); return [$key => $item]; }); $diff = [ 'added' => [], 'removed' => [], 'updated' => [], ]; // 2. 處理新資料 (Deleted and Re-inserted currently for simplicity, but logic simulates update) // 為了保持 ID 當作外鍵的穩定性,最佳做法是 update 存在的,create 新的,delete 舊的。 // 但考量現有邏輯是 delete all -> create all,我們維持原策略但優化 Diff 計算。 // 由於採用全刪重建,我們必須手動計算 Diff $order->items()->delete(); $newItemsKeys = []; foreach ($itemsData as $data) { $key = $data['product_id'] . '_' . ($data['batch_number'] ?? ''); $newItemsKeys[] = $key; $item = $order->items()->create([ 'product_id' => $data['product_id'], 'batch_number' => $data['batch_number'] ?? null, 'quantity' => $data['quantity'], 'notes' => $data['notes'] ?? null, ]); // Eager load product for name $item->load('product'); // 比對邏輯 if ($oldItemsMap->has($key)) { $oldItem = $oldItemsMap->get($key); // 檢查數值是否有變動 if ((float)$oldItem->quantity !== (float)$data['quantity'] || $oldItem->notes !== ($data['notes'] ?? null)) { $diff['updated'][] = [ 'product_name' => $item->product->name, 'old' => [ 'quantity' => (float)$oldItem->quantity, 'notes' => $oldItem->notes, ], 'new' => [ 'quantity' => (float)$data['quantity'], 'notes' => $item->notes, ] ]; } } else { // 新增 (使用者需求:顯示為更新,從 0 -> X) $diff['updated'][] = [ 'product_name' => $item->product->name, 'old' => [ 'quantity' => 0, 'notes' => null, ], 'new' => [ 'quantity' => (float)$item->quantity, 'notes' => $item->notes, ] ]; } } // 3. 處理被移除的項目 foreach ($oldItemsMap as $key => $oldItem) { if (!in_array($key, $newItemsKeys)) { $diff['removed'][] = [ 'product_name' => $oldItem->product->name, 'old' => [ 'quantity' => (float)$oldItem->quantity, 'notes' => $oldItem->notes, ] ]; } } // 4. 將 Diff 注入到 Model 的暫存屬性中 $hasChanged = !empty($diff['added']) || !empty($diff['removed']) || !empty($diff['updated']); if ($hasChanged) { $order->activityProperties['items_diff'] = $diff; } return $hasChanged; }); } /** * 過帳 (Post) - 執行調撥 (直接扣除來源,增加目的) */ public function post(InventoryTransferOrder $order, int $userId): void { // [IMPORTANT] 強制重新載入品項,因為在 Controller 中可能剛執行過 updateItems,導致記憶體中快取的 items 是舊的或空的 $order->load('items.product'); DB::transaction(function () use ($order, $userId) { $fromWarehouse = $order->fromWarehouse; $toWarehouse = $order->toWarehouse; foreach ($order->items as $item) { if ($item->quantity <= 0) continue; // 1. 處理來源倉 (扣除) $sourceInventory = Inventory::where('warehouse_id', $order->from_warehouse_id) ->where('product_id', $item->product_id) ->where('batch_number', $item->batch_number) ->first(); if (!$sourceInventory || $sourceInventory->quantity < $item->quantity) { throw ValidationException::withMessages([ 'items' => ["商品 {$item->product->name} (批號: {$item->batch_number}) 在來源倉庫存不足"], ]); } $oldSourceQty = $sourceInventory->quantity; $newSourceQty = $oldSourceQty - $item->quantity; // 儲存庫存快照 $item->update(['snapshot_quantity' => $oldSourceQty]); $sourceInventory->quantity = $newSourceQty; // 更新總值 (假設成本不變) $sourceInventory->total_value = $sourceInventory->quantity * $sourceInventory->unit_cost; $sourceInventory->save(); // 記錄來源交易 $sourceInventory->transactions()->create([ 'type' => '調撥出庫', 'quantity' => -$item->quantity, 'unit_cost' => $sourceInventory->unit_cost, 'balance_before' => $oldSourceQty, 'balance_after' => $newSourceQty, 'reason' => "調撥單 {$order->doc_no} 至 {$toWarehouse->name}", 'actual_time' => now(), 'user_id' => $userId, ]); // 2. 處理目的倉 (增加) $targetInventory = Inventory::firstOrCreate( [ 'warehouse_id' => $order->to_warehouse_id, 'product_id' => $item->product_id, 'batch_number' => $item->batch_number, ], [ 'quantity' => 0, 'unit_cost' => $sourceInventory->unit_cost, // 繼承成本 'total_value' => 0, // 繼承其他屬性 'expiry_date' => $sourceInventory->expiry_date, 'quality_status' => $sourceInventory->quality_status, 'origin_country' => $sourceInventory->origin_country, ] ); // 若是新建立的,且成本為0,確保繼承成本 if ($targetInventory->wasRecentlyCreated && $targetInventory->unit_cost == 0) { $targetInventory->unit_cost = $sourceInventory->unit_cost; } $oldTargetQty = $targetInventory->quantity; $newTargetQty = $oldTargetQty + $item->quantity; $targetInventory->quantity = $newTargetQty; $targetInventory->total_value = $targetInventory->quantity * $targetInventory->unit_cost; $targetInventory->save(); // 記錄目的交易 $targetInventory->transactions()->create([ 'type' => '調撥入庫', 'quantity' => $item->quantity, 'unit_cost' => $targetInventory->unit_cost, 'balance_before' => $oldTargetQty, 'balance_after' => $newTargetQty, 'reason' => "調撥單 {$order->doc_no} 來自 {$fromWarehouse->name}", 'actual_time' => now(), 'user_id' => $userId, ]); } // 準備品項快照供日誌使用 $itemsSnapshot = $order->items->map(function($item) { return [ 'product_name' => $item->product->name, 'old' => [ 'quantity' => (float)$item->quantity, 'notes' => $item->notes, ], 'new' => [ 'quantity' => (float)$item->quantity, 'notes' => $item->notes, ] ]; })->toArray(); $order->status = 'completed'; $order->posted_at = now(); $order->posted_by = $userId; $order->save(); // 觸發自動日誌 }); } public function void(InventoryTransferOrder $order, int $userId): void { if ($order->status !== 'draft') { throw new \Exception('只能作廢草稿狀態的單據'); } $order->update([ 'status' => 'voided', 'updated_by' => $userId ]); } }