$warehouseId, 'count_doc_id' => $countDocId, 'status' => 'draft', 'reason' => $reason, 'remarks' => $remarks, 'created_by' => $userId, ]); } /** * 從盤點單建立盤調單 */ public function createFromCountDoc(InventoryCountDoc $countDoc, int $userId): InventoryAdjustDoc { return DB::transaction(function () use ($countDoc, $userId) { // 1. 建立盤調單頭 $adjDoc = $this->createDoc( $countDoc->warehouse_id, "盤點調整: " . $countDoc->doc_no, "由盤點單 {$countDoc->doc_no} 自動生成", $userId, $countDoc->id ); // 2. 抓取有差異的明細 (diff_qty != 0) foreach ($countDoc->items as $item) { if (abs($item->diff_qty) < 0.0001) continue; $adjDoc->items()->create([ 'product_id' => $item->product_id, 'batch_number' => $item->batch_number, 'qty_before' => $item->system_qty, 'adjust_qty' => $item->diff_qty, 'notes' => "盤點差異: " . $item->diff_qty, ]); } return $adjDoc; }); } /** * 更新盤調單內容 (Items) * 此處採用 "全量更新" 方式處理 items (先刪後加),簡單可靠 */ public function updateItems(InventoryAdjustDoc $doc, array $itemsData): void { DB::transaction(function () use ($doc, $itemsData) { $updatedItems = []; $oldItems = $doc->items()->with('product')->get(); // 記錄舊品項狀態 (用於標註異動) foreach ($oldItems as $oldItem) { $updatedItems[] = [ 'product_name' => $oldItem->product->name, 'old' => [ 'adjust_qty' => (float)$oldItem->adjust_qty, 'notes' => $oldItem->notes, ], 'new' => null // 標記為刪除或待更新 ]; } $doc->items()->delete(); foreach ($itemsData as $data) { // 取得當前庫存作為 qty_before 參考 (僅參考,實際扣減以過帳當下為準) $inventory = Inventory::where('warehouse_id', $doc->warehouse_id) ->where('product_id', $data['product_id']) ->where('batch_number', $data['batch_number'] ?? null) ->first(); $qtyBefore = $inventory ? $inventory->quantity : 0; $newItem = $doc->items()->create([ 'product_id' => $data['product_id'], 'batch_number' => $data['batch_number'] ?? null, 'qty_before' => $qtyBefore, 'adjust_qty' => $data['adjust_qty'], 'notes' => $data['notes'] ?? null, ]); // 更新日誌中的品項列表 $productName = \App\Modules\Inventory\Models\Product::find($data['product_id'])?->name; $found = false; foreach ($updatedItems as $idx => $ui) { if ($ui['product_name'] === $productName && $ui['new'] === null) { $updatedItems[$idx]['new'] = [ 'adjust_qty' => (float)$data['adjust_qty'], 'notes' => $data['notes'] ?? null, ]; $found = true; break; } } if (!$found) { $updatedItems[] = [ 'product_name' => $productName, 'old' => null, 'new' => [ 'adjust_qty' => (float)$data['adjust_qty'], 'notes' => $data['notes'] ?? null, ] ]; } } // 清理沒被更新到的舊品項 (即真正被刪除的) $finalUpdatedItems = []; foreach ($updatedItems as $ui) { if ($ui['old'] === null && $ui['new'] === null) continue; // 比對是否有實質變動 if ($ui['old'] != $ui['new']) { $finalUpdatedItems[] = $ui; } } if (!empty($finalUpdatedItems)) { activity() ->performedOn($doc) ->causedBy(auth()->user()) ->event('updated') ->withProperties([ 'items_diff' => [ 'updated' => $finalUpdatedItems, ] ]) ->log('updated'); } }); } /** * 過帳 (Post) - 生效庫存異動 */ public function post(InventoryAdjustDoc $doc, int $userId): void { DB::transaction(function () use ($doc, $userId) { $oldStatus = $doc->status; foreach ($doc->items as $item) { if ($item->adjust_qty == 0) continue; $inventory = Inventory::firstOrNew([ 'warehouse_id' => $doc->warehouse_id, 'product_id' => $item->product_id, 'batch_number' => $item->batch_number, ]); // 如果是新建立的 object (id 為空),需要初始化 default if (!$inventory->exists) { $inventory->unit_cost = $item->product->cost ?? 0; $inventory->quantity = 0; } $oldQty = $inventory->quantity; $newQty = $oldQty + $item->adjust_qty; $inventory->quantity = $newQty; $inventory->total_value = $newQty * $inventory->unit_cost; $inventory->save(); // 建立 Transaction $inventory->transactions()->create([ 'type' => '庫存調整', 'quantity' => $item->adjust_qty, 'unit_cost' => $inventory->unit_cost, 'balance_before' => $oldQty, 'balance_after' => $newQty, 'reason' => "盤調單 {$doc->doc_no}: " . ($doc->reason ?? '手動調整'), 'actual_time' => now(), 'user_id' => $userId, ]); } // 使用 saveQuietly 避免重複產生自動日誌 $doc->status = 'posted'; $doc->posted_at = now(); $doc->posted_by = $userId; $doc->saveQuietly(); // 準備品項快照供日誌使用 $itemsSnapshot = $doc->items->map(function($item) { return [ 'product_name' => $item->product->name, 'old' => null, // 過帳視為整單生效,不顯示個別欄位差異 'new' => [ 'adjust_qty' => (float)$item->adjust_qty, 'notes' => $item->notes, ] ]; })->toArray(); // 手動產生過帳日誌 activity() ->performedOn($doc) ->causedBy(auth()->user()) ->event('updated') ->withProperties([ 'attributes' => [ 'status' => 'posted', 'posted_at' => $doc->posted_at->format('Y-m-d H:i:s'), 'posted_by' => $userId, ], 'old' => [ 'status' => $oldStatus, 'posted_at' => null, 'posted_by' => null, ], 'items_diff' => [ 'updated' => $itemsSnapshot, ] ]) ->log('posted'); // 4. 若關聯盤點單,連動更新盤點單狀態 if ($doc->count_doc_id) { $countDoc = InventoryCountDoc::find($doc->count_doc_id); if ($countDoc) { $countDoc->status = 'adjusted'; $countDoc->saveQuietly(); // 盤點單也靜默更新 } } }); } /** * 作廢 (Void) */ public function void(InventoryAdjustDoc $doc, int $userId): void { if ($doc->status !== 'draft') { throw new \Exception('只能作廢草稿狀態的單據'); } $oldStatus = $doc->status; $doc->status = 'voided'; $doc->updated_by = $userId; $doc->saveQuietly(); activity() ->performedOn($doc) ->causedBy(auth()->user()) ->event('updated') ->withProperties([ 'attributes' => ['status' => 'voided'], 'old' => ['status' => $oldStatus] ]) ->log('voided'); } }