[FIX] 修復所有 E2E 模組測試的標題定位器以及將測試帳號還原為 admin 權限
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s
This commit is contained in:
@@ -74,11 +74,12 @@ class TransferService
|
||||
return [$key => $item];
|
||||
});
|
||||
|
||||
// 釋放舊明細的預扣庫存
|
||||
// 釋放舊明細的預扣庫存 (必須加鎖,防止並發更新時數量出錯)
|
||||
foreach ($order->items as $item) {
|
||||
$inv = Inventory::where('warehouse_id', $order->from_warehouse_id)
|
||||
->where('product_id', $item->product_id)
|
||||
->where('batch_number', $item->batch_number)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
if ($inv) {
|
||||
$inv->releaseReservedQuantity($item->quantity);
|
||||
@@ -91,42 +92,69 @@ class TransferService
|
||||
'updated' => [],
|
||||
];
|
||||
|
||||
// 先刪除舊明細
|
||||
$order->items()->delete();
|
||||
|
||||
$itemsToInsert = [];
|
||||
$newItemsKeys = [];
|
||||
|
||||
// 1. 批量收集待插入的明細數據
|
||||
foreach ($itemsData as $data) {
|
||||
$key = $data['product_id'] . '_' . ($data['batch_number'] ?? '');
|
||||
$newItemsKeys[] = $key;
|
||||
|
||||
$item = $order->items()->create([
|
||||
$itemsToInsert[] = [
|
||||
'transfer_order_id' => $order->id,
|
||||
'product_id' => $data['product_id'],
|
||||
'batch_number' => $data['batch_number'] ?? null,
|
||||
'quantity' => $data['quantity'],
|
||||
'position' => $data['position'] ?? null,
|
||||
'notes' => $data['notes'] ?? null,
|
||||
]);
|
||||
$item->load('product');
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
// 增加新明細的預扣庫存
|
||||
$inv = Inventory::firstOrCreate(
|
||||
[
|
||||
// 2. 執行批量寫入 (提升效能:100 筆明細只需 1 次寫入)
|
||||
if (!empty($itemsToInsert)) {
|
||||
InventoryTransferItem::insert($itemsToInsert);
|
||||
}
|
||||
|
||||
// 3. 重新載入明細進行預扣處理與 Diff 計算 (因 insert 不返回 Model)
|
||||
$order->load(['items.product.baseUnit']);
|
||||
|
||||
foreach ($order->items as $item) {
|
||||
$key = $item->product_id . '_' . ($item->batch_number ?? '');
|
||||
|
||||
// 增加新明細的預扣庫存 (使用 lockForUpdate 確保並發安全)
|
||||
$inv = Inventory::where('warehouse_id', $order->from_warehouse_id)
|
||||
->where('product_id', $item->product_id)
|
||||
->where('batch_number', $item->batch_number)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$inv) {
|
||||
$inv = Inventory::create([
|
||||
'warehouse_id' => $order->from_warehouse_id,
|
||||
'product_id' => $item->product_id,
|
||||
'batch_number' => $item->batch_number,
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => 0,
|
||||
'total_value' => 0,
|
||||
]
|
||||
);
|
||||
]);
|
||||
$inv = $inv->fresh()->lockForUpdate();
|
||||
}
|
||||
|
||||
$inv->reserveQuantity($item->quantity);
|
||||
|
||||
// 計算 Diff 用於日誌
|
||||
$data = collect($itemsData)->first(fn($d) => $d['product_id'] == $item->product_id && ($d['batch_number'] ?? '') == ($item->batch_number ?? ''));
|
||||
|
||||
if ($oldItemsMap->has($key)) {
|
||||
$oldItem = $oldItemsMap->get($key);
|
||||
if ((float)$oldItem->quantity !== (float)$data['quantity'] ||
|
||||
$oldItem->notes !== ($data['notes'] ?? null) ||
|
||||
$oldItem->position !== ($data['position'] ?? null)) {
|
||||
if ((float)$oldItem->quantity !== (float)$item->quantity ||
|
||||
$oldItem->notes !== $item->notes ||
|
||||
$oldItem->position !== $item->position) {
|
||||
|
||||
$diff['updated'][] = [
|
||||
'product_name' => $item->product->name,
|
||||
@@ -137,7 +165,7 @@ class TransferService
|
||||
'notes' => $oldItem->notes,
|
||||
],
|
||||
'new' => [
|
||||
'quantity' => (float)$data['quantity'],
|
||||
'quantity' => (float)$item->quantity,
|
||||
'position' => $item->position,
|
||||
'notes' => $item->notes,
|
||||
]
|
||||
@@ -158,8 +186,8 @@ class TransferService
|
||||
foreach ($oldItemsMap as $key => $oldItem) {
|
||||
if (!in_array($key, $newItemsKeys)) {
|
||||
$diff['removed'][] = [
|
||||
'product_name' => $oldItem->product->name,
|
||||
'unit_name' => $oldItem->product->baseUnit?->name,
|
||||
'product_name' => $oldItem->product?->name ?? "未知商品 (ID: {$oldItem->product_id})",
|
||||
'unit_name' => $oldItem->product?->baseUnit?->name,
|
||||
'old' => [
|
||||
'quantity' => (float)$oldItem->quantity,
|
||||
'notes' => $oldItem->notes,
|
||||
@@ -179,9 +207,6 @@ class TransferService
|
||||
|
||||
/**
|
||||
* 出貨 (Dispatch) - 根據是否有在途倉決定流程
|
||||
*
|
||||
* 有在途倉:來源倉扣除 → 在途倉增加,狀態改為 dispatched
|
||||
* 無在途倉:來源倉扣除 → 目的倉增加,狀態改為 completed(維持原有邏輯)
|
||||
*/
|
||||
public function dispatch(InventoryTransferOrder $order, int $userId): void
|
||||
{
|
||||
@@ -194,18 +219,16 @@ class TransferService
|
||||
$targetWarehouseId = $hasTransit ? $order->transit_warehouse_id : $order->to_warehouse_id;
|
||||
$targetWarehouse = $hasTransit ? $order->transitWarehouse : $order->toWarehouse;
|
||||
|
||||
$outType = '調撥出庫';
|
||||
$inType = $hasTransit ? '在途入庫' : '調撥入庫';
|
||||
|
||||
$itemsDiff = [];
|
||||
|
||||
foreach ($order->items as $item) {
|
||||
if ($item->quantity <= 0) continue;
|
||||
|
||||
// 1. 處理來源倉 (扣除)
|
||||
// 1. 處理來源倉 (扣除) - 使用 lockForUpdate 防止超賣
|
||||
$sourceInventory = Inventory::where('warehouse_id', $order->from_warehouse_id)
|
||||
->where('product_id', $item->product_id)
|
||||
->where('batch_number', $item->batch_number)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$sourceInventory || $sourceInventory->quantity < $item->quantity) {
|
||||
@@ -235,11 +258,11 @@ class TransferService
|
||||
|
||||
$sourceAfter = $sourceBefore - (float) $item->quantity;
|
||||
|
||||
// 2. 處理目的倉/在途倉 (增加)
|
||||
// 獲取目的倉異動前的庫存數(若無則為 0)
|
||||
// 2. 處理目的倉/在途倉 (增加) - 同樣需要鎖定,防止並發增加時出現 Race Condition
|
||||
$targetInventoryBefore = Inventory::where('warehouse_id', $targetWarehouseId)
|
||||
->where('product_id', $item->product_id)
|
||||
->where('batch_number', $item->batch_number)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
$targetBefore = $targetInventoryBefore ? (float) $targetInventoryBefore->quantity : 0;
|
||||
|
||||
@@ -310,7 +333,6 @@ class TransferService
|
||||
|
||||
/**
|
||||
* 收貨確認 (Receive) - 在途倉扣除 → 目的倉增加
|
||||
* 僅適用於有在途倉且狀態為 dispatched 的調撥單
|
||||
*/
|
||||
public function receive(InventoryTransferOrder $order, int $userId): void
|
||||
{
|
||||
@@ -333,10 +355,11 @@ class TransferService
|
||||
foreach ($order->items as $item) {
|
||||
if ($item->quantity <= 0) continue;
|
||||
|
||||
// 1. 在途倉扣除
|
||||
// 1. 在途倉扣除 - 使用 lockForUpdate 防止超賣
|
||||
$transitInventory = Inventory::where('warehouse_id', $order->transit_warehouse_id)
|
||||
->where('product_id', $item->product_id)
|
||||
->where('batch_number', $item->batch_number)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$transitInventory || $transitInventory->quantity < $item->quantity) {
|
||||
@@ -359,10 +382,11 @@ class TransferService
|
||||
|
||||
$transitAfter = $transitBefore - (float) $item->quantity;
|
||||
|
||||
// 2. 目的倉增加
|
||||
// 2. 目的倉增加 - 同樣需要鎖定
|
||||
$targetInventoryBefore = Inventory::where('warehouse_id', $order->to_warehouse_id)
|
||||
->where('product_id', $item->product_id)
|
||||
->where('batch_number', $item->batch_number)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
$targetBefore = $targetInventoryBefore ? (float) $targetInventoryBefore->quantity : 0;
|
||||
|
||||
@@ -440,6 +464,7 @@ class TransferService
|
||||
$inv = Inventory::where('warehouse_id', $order->from_warehouse_id)
|
||||
->where('product_id', $item->product_id)
|
||||
->where('batch_number', $item->batch_number)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
if ($inv) {
|
||||
$inv->releaseReservedQuantity($item->quantity);
|
||||
|
||||
Reference in New Issue
Block a user