feat: 整合門市領料日誌、API 文件存取、修改庫存與併發編號問題、供應商商品內聯編輯及日誌 UI 優化
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s

This commit is contained in:
2026-03-02 16:42:12 +08:00
parent 7dac2d1f77
commit 0a955fb993
33 changed files with 1424 additions and 853 deletions

View File

@@ -9,8 +9,17 @@ use App\Modules\Inventory\Models\Warehouse;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
class TransferService
{
protected InventoryServiceInterface $inventoryService;
public function __construct(InventoryServiceInterface $inventoryService)
{
$this->inventoryService = $inventoryService;
}
/**
* 建立調撥單草稿
*/
@@ -24,7 +33,7 @@ class TransferService
}
}
return InventoryTransferOrder::create([
$order = new InventoryTransferOrder([
'from_warehouse_id' => $fromWarehouseId,
'to_warehouse_id' => $toWarehouseId,
'transit_warehouse_id' => $transitWarehouseId,
@@ -32,6 +41,26 @@ class TransferService
'remarks' => $remarks,
'created_by' => $userId,
]);
// 手動觸發單號產生邏輯,因為 saveQuietly 繞過了 Model Events
if (empty($order->doc_no)) {
$today = date('Ymd');
$prefix = 'TRF-' . $today . '-';
$lastDoc = InventoryTransferOrder::where('doc_no', 'like', $prefix . '%')
->orderBy('doc_no', 'desc')
->first();
if ($lastDoc) {
$lastNumber = substr($lastDoc->doc_no, -2);
$nextNumber = str_pad((int)$lastNumber + 1, 2, '0', STR_PAD_LEFT);
} else {
$nextNumber = '01';
}
$order->doc_no = $prefix . $nextNumber;
}
$order->saveQuietly();
return $order;
}
/**
@@ -101,6 +130,7 @@ class TransferService
$diff['updated'][] = [
'product_name' => $item->product->name,
'unit_name' => $item->product->baseUnit?->name,
'old' => [
'quantity' => (float)$oldItem->quantity,
'position' => $oldItem->position,
@@ -114,12 +144,9 @@ class TransferService
];
}
} else {
$diff['updated'][] = [
$diff['added'][] = [
'product_name' => $item->product->name,
'old' => [
'quantity' => 0,
'notes' => null,
],
'unit_name' => $item->product->baseUnit?->name,
'new' => [
'quantity' => (float)$item->quantity,
'notes' => $item->notes,
@@ -132,6 +159,7 @@ class TransferService
if (!in_array($key, $newItemsKeys)) {
$diff['removed'][] = [
'product_name' => $oldItem->product->name,
'unit_name' => $oldItem->product->baseUnit?->name,
'old' => [
'quantity' => (float)$oldItem->quantity,
'notes' => $oldItem->notes,
@@ -169,6 +197,8 @@ class TransferService
$outType = '調撥出庫';
$inType = $hasTransit ? '在途入庫' : '調撥入庫';
$itemsDiff = [];
foreach ($order->items as $item) {
if ($item->quantity <= 0) continue;
@@ -186,70 +216,65 @@ class TransferService
]);
}
$oldSourceQty = $sourceInventory->quantity;
$newSourceQty = $oldSourceQty - $item->quantity;
$sourceBefore = (float) $sourceInventory->quantity;
// 釋放草稿階段預扣的庫存
$sourceInventory->reserved_quantity = max(0, $sourceInventory->reserved_quantity - $item->quantity);
$sourceInventory->saveQuietly();
$item->update(['snapshot_quantity' => $oldSourceQty]);
$sourceInventory->quantity = $newSourceQty;
$sourceInventory->total_value = $sourceInventory->quantity * $sourceInventory->unit_cost;
$sourceInventory->save();
$item->update(['snapshot_quantity' => $sourceBefore]);
$sourceInventory->transactions()->create([
'type' => $outType,
'quantity' => -$item->quantity,
'unit_cost' => $sourceInventory->unit_cost,
'balance_before' => $oldSourceQty,
'balance_after' => $newSourceQty,
'reason' => "調撥單 {$order->doc_no}{$targetWarehouse->name}",
'actual_time' => now(),
'user_id' => $userId,
]);
// 委託 InventoryService 處理扣庫與 Transaction
$this->inventoryService->decreaseInventoryQuantity(
$sourceInventory->id,
$item->quantity,
"調撥單 {$order->doc_no}{$targetWarehouse->name}",
InventoryTransferOrder::class,
$order->id
);
$sourceAfter = $sourceBefore - (float) $item->quantity;
// 2. 處理目的倉/在途倉 (增加)
$targetInventory = Inventory::firstOrCreate(
[
'warehouse_id' => $targetWarehouseId,
'product_id' => $item->product_id,
'batch_number' => $item->batch_number,
'location' => $hasTransit ? null : ($item->position ?? null),
],
[
'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,
]
);
if ($targetInventory->wasRecentlyCreated && $targetInventory->unit_cost == 0) {
$targetInventory->unit_cost = $sourceInventory->unit_cost;
}
// 獲取目的倉異動前的庫存數(若無則為 0
$targetInventoryBefore = Inventory::where('warehouse_id', $targetWarehouseId)
->where('product_id', $item->product_id)
->where('batch_number', $item->batch_number)
->first();
$targetBefore = $targetInventoryBefore ? (float) $targetInventoryBefore->quantity : 0;
$oldTargetQty = $targetInventory->quantity;
$newTargetQty = $oldTargetQty + $item->quantity;
$targetInventory->quantity = $newTargetQty;
$targetInventory->total_value = $targetInventory->quantity * $targetInventory->unit_cost;
$targetInventory->save();
$targetInventory->transactions()->create([
'type' => $inType,
$this->inventoryService->createInventoryRecord([
'warehouse_id' => $targetWarehouseId,
'product_id' => $item->product_id,
'quantity' => $item->quantity,
'unit_cost' => $targetInventory->unit_cost,
'balance_before' => $oldTargetQty,
'balance_after' => $newTargetQty,
'unit_cost' => $sourceInventory->unit_cost,
'batch_number' => $item->batch_number,
'expiry_date' => $sourceInventory->expiry_date,
'reason' => "調撥單 {$order->doc_no} 來自 {$fromWarehouse->name}",
'actual_time' => now(),
'user_id' => $userId,
'reference_type' => InventoryTransferOrder::class,
'reference_id' => $order->id,
'location' => $hasTransit ? null : ($item->position ?? null),
'origin_country' => $sourceInventory->origin_country,
'quality_status' => $sourceInventory->quality_status,
]);
$targetAfter = $targetBefore + (float) $item->quantity;
// 記錄異動明細供整合日誌使用
$itemsDiff[] = [
'product_name' => $item->product->name,
'batch_number' => $item->batch_number,
'quantity' => (float)$item->quantity,
'source_warehouse' => $fromWarehouse->name,
'source_before' => $sourceBefore,
'source_after' => $sourceAfter,
'target_warehouse' => $targetWarehouse->name,
'target_before' => $targetBefore,
'target_after' => $targetAfter,
];
}
$oldStatus = $order->status;
if ($hasTransit) {
$order->status = 'dispatched';
$order->dispatched_at = now();
@@ -259,7 +284,27 @@ class TransferService
$order->posted_at = now();
$order->posted_by = $userId;
}
$order->save();
$order->saveQuietly();
// 手動觸發單一合併日誌
activity()
->performedOn($order)
->causedBy(auth()->user())
->event('updated')
->withProperties([
'items_diff' => $itemsDiff,
'attributes' => [
'status' => $order->status,
'dispatched_at' => $order->dispatched_at ? $order->dispatched_at->format('Y-m-d H:i:s') : null,
'posted_at' => $order->posted_at ? $order->posted_at->format('Y-m-d H:i:s') : null,
'dispatched_by' => $order->dispatched_by,
'posted_by' => $order->posted_by,
],
'old' => [
'status' => $oldStatus,
]
])
->log($order->status == 'completed' ? 'posted' : 'dispatched');
});
}
@@ -283,6 +328,8 @@ class TransferService
$transitWarehouse = $order->transitWarehouse;
$toWarehouse = $order->toWarehouse;
$itemsDiff = [];
foreach ($order->items as $item) {
if ($item->quantity <= 0) continue;
@@ -299,71 +346,83 @@ class TransferService
]);
}
$oldTransitQty = $transitInventory->quantity;
$newTransitQty = $oldTransitQty - $item->quantity;
$transitBefore = (float) $transitInventory->quantity;
$transitInventory->quantity = $newTransitQty;
$transitInventory->total_value = $transitInventory->quantity * $transitInventory->unit_cost;
$transitInventory->save();
$transitInventory->transactions()->create([
'type' => '在途出庫',
'quantity' => -$item->quantity,
'unit_cost' => $transitInventory->unit_cost,
'balance_before' => $oldTransitQty,
'balance_after' => $newTransitQty,
'reason' => "調撥單 {$order->doc_no} 配送至 {$toWarehouse->name}",
'actual_time' => now(),
'user_id' => $userId,
]);
// 委託 InventoryService 處理扣庫與 Transaction
$this->inventoryService->decreaseInventoryQuantity(
$transitInventory->id,
$item->quantity,
"調撥單 {$order->doc_no} 配送至 {$toWarehouse->name}",
InventoryTransferOrder::class,
$order->id
);
$transitAfter = $transitBefore - (float) $item->quantity;
// 2. 目的倉增加
$targetInventory = Inventory::firstOrCreate(
[
'warehouse_id' => $order->to_warehouse_id,
'product_id' => $item->product_id,
'batch_number' => $item->batch_number,
'location' => $item->position,
],
[
'quantity' => 0,
'unit_cost' => $transitInventory->unit_cost,
'total_value' => 0,
'expiry_date' => $transitInventory->expiry_date,
'quality_status' => $transitInventory->quality_status,
'origin_country' => $transitInventory->origin_country,
]
);
$targetInventoryBefore = Inventory::where('warehouse_id', $order->to_warehouse_id)
->where('product_id', $item->product_id)
->where('batch_number', $item->batch_number)
->first();
$targetBefore = $targetInventoryBefore ? (float) $targetInventoryBefore->quantity : 0;
if ($targetInventory->wasRecentlyCreated && $targetInventory->unit_cost == 0) {
$targetInventory->unit_cost = $transitInventory->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' => '調撥入庫',
$this->inventoryService->createInventoryRecord([
'warehouse_id' => $order->to_warehouse_id,
'product_id' => $item->product_id,
'quantity' => $item->quantity,
'unit_cost' => $targetInventory->unit_cost,
'balance_before' => $oldTargetQty,
'balance_after' => $newTargetQty,
'unit_cost' => $transitInventory->unit_cost,
'batch_number' => $item->batch_number,
'expiry_date' => $transitInventory->expiry_date,
'reason' => "調撥單 {$order->doc_no} 來自 {$transitWarehouse->name}",
'actual_time' => now(),
'user_id' => $userId,
'reference_type' => InventoryTransferOrder::class,
'reference_id' => $order->id,
'location' => $item->position,
'origin_country' => $transitInventory->origin_country,
'quality_status' => $transitInventory->quality_status,
]);
$targetAfter = $targetBefore + (float) $item->quantity;
$itemsDiff[] = [
'product_name' => $item->product->name,
'batch_number' => $item->batch_number,
'quantity' => (float)$item->quantity,
'source_warehouse' => $transitWarehouse->name,
'source_before' => $transitBefore,
'source_after' => $transitAfter,
'target_warehouse' => $toWarehouse->name,
'target_before' => $targetBefore,
'target_after' => $targetAfter,
];
}
$oldStatus = $order->status;
$order->status = 'completed';
$order->posted_at = now();
$order->posted_by = $userId;
$order->received_at = now();
$order->received_by = $userId;
$order->save();
$order->saveQuietly();
// 手動觸發單一合併日誌
activity()
->performedOn($order)
->causedBy(auth()->user())
->event('updated')
->withProperties([
'items_diff' => $itemsDiff,
'attributes' => [
'status' => 'completed',
'posted_at' => $order->posted_at->format('Y-m-d H:i:s'),
'received_at' => $order->received_at->format('Y-m-d H:i:s'),
'posted_by' => $order->posted_by,
'received_by' => $order->received_by,
],
'old' => [
'status' => $oldStatus,
]
])
->log('received');
});
}
@@ -387,10 +446,24 @@ class TransferService
}
}
$order->update([
'status' => 'voided',
'updated_by' => $userId
]);
$oldStatus = $order->status;
$order->status = 'voided';
$order->updated_by = $userId;
$order->saveQuietly();
activity()
->performedOn($order)
->causedBy(auth()->user())
->event('updated')
->withProperties([
'attributes' => [
'status' => 'voided',
],
'old' => [
'status' => $oldStatus,
]
])
->log('voided');
});
}
}