feat(inventory): 完善庫存盤調更新與日誌邏輯,新增「無需盤調」狀態判定
1. 修正 AdjustDocController 缺失 update 方法導致的錯誤。 2. 修正 ActivityDetailDialog 前端 map 渲染 undefined 的 TypeError。 3. 優化盤調單「過帳」日誌,現在會同步包含當時的商品明細快照。 4. 實作盤點單「無需盤調」(no_adjust) 自動判定邏輯: - 當盤點數量與庫存完全一致時,自動標記為 no_adjust 結案。 - 更新前端標籤樣式與操作按鈕對應邏輯。 - 限制 no_adjust 單據不可重複建立盤調單。 5. 統一盤點單與盤調單的日誌配置,優化 ID 轉名稱顯示。
This commit is contained in:
@@ -60,6 +60,21 @@ class AdjustService
|
||||
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) {
|
||||
@@ -71,13 +86,60 @@ class AdjustService
|
||||
|
||||
$qtyBefore = $inventory ? $inventory->quantity : 0;
|
||||
|
||||
$doc->items()->create([
|
||||
$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');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -88,13 +150,11 @@ class AdjustService
|
||||
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 存在 (且理論上不能變負? 視策略而定,這裡假設允許變負或由 InventoryService 控管)
|
||||
// 若是增加庫存,若不存在需建立
|
||||
|
||||
$inventory = Inventory::firstOrNew([
|
||||
'warehouse_id' => $doc->warehouse_id,
|
||||
'product_id' => $item->product_id,
|
||||
@@ -103,7 +163,6 @@ class AdjustService
|
||||
|
||||
// 如果是新建立的 object (id 為空),需要初始化 default
|
||||
if (!$inventory->exists) {
|
||||
// 繼承 Product 成本或預設 0 (簡化處理)
|
||||
$inventory->unit_cost = $item->product->cost ?? 0;
|
||||
$inventory->quantity = 0;
|
||||
}
|
||||
@@ -112,7 +171,6 @@ class AdjustService
|
||||
$newQty = $oldQty + $item->adjust_qty;
|
||||
|
||||
$inventory->quantity = $newQty;
|
||||
// 用最新的數量 * 單位成本 (簡化成本計算,不採用移動加權)
|
||||
$inventory->total_value = $newQty * $inventory->unit_cost;
|
||||
$inventory->save();
|
||||
|
||||
@@ -129,17 +187,53 @@ class AdjustService
|
||||
]);
|
||||
}
|
||||
|
||||
$doc->update([
|
||||
'status' => 'posted',
|
||||
'posted_at' => now(),
|
||||
'posted_by' => $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) {
|
||||
InventoryCountDoc::where('id', $doc->count_doc_id)->update([
|
||||
'status' => 'adjusted'
|
||||
]);
|
||||
$countDoc = InventoryCountDoc::find($doc->count_doc_id);
|
||||
if ($countDoc) {
|
||||
$countDoc->status = 'adjusted';
|
||||
$countDoc->saveQuietly(); // 盤點單也靜默更新
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -152,9 +246,20 @@ class AdjustService
|
||||
if ($doc->status !== 'draft') {
|
||||
throw new \Exception('只能作廢草稿狀態的單據');
|
||||
}
|
||||
$doc->update([
|
||||
'status' => 'voided',
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
|
||||
$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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,16 +145,20 @@ class CountService
|
||||
$newDocAttributesLog = [];
|
||||
|
||||
if ($isAllCounted) {
|
||||
if ($doc->status !== 'completed') {
|
||||
$doc->status = 'completed';
|
||||
// 檢查是否有任何差異
|
||||
$hasDiff = $doc->items()->where('diff_qty', '!=', 0)->exists();
|
||||
$targetStatus = $hasDiff ? 'completed' : 'no_adjust';
|
||||
|
||||
if ($doc->status !== $targetStatus) {
|
||||
$doc->status = $targetStatus;
|
||||
$doc->completed_at = now();
|
||||
$doc->completed_by = auth()->id();
|
||||
$doc->saveQuietly();
|
||||
|
||||
$doc->refresh(); // 獲取更新後的屬性 (如時間)
|
||||
$doc->refresh();
|
||||
|
||||
$newDocAttributesLog = [
|
||||
'status' => 'completed',
|
||||
'status' => $targetStatus,
|
||||
'completed_at' => $doc->completed_at->format('Y-m-d H:i:s'),
|
||||
'completed_by' => $doc->completed_by,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user