Files
star-erp/app/Modules/Inventory/Services/TransferService.php

246 lines
9.9 KiB
PHP
Raw Normal View History

<?php
namespace App\Modules\Inventory\Services;
use App\Modules\Inventory\Models\Inventory;
use App\Modules\Inventory\Models\InventoryTransferOrder;
use App\Modules\Inventory\Models\InventoryTransferItem;
use App\Modules\Inventory\Models\Warehouse;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class TransferService
{
/**
* 建立調撥單草稿
*/
public function createOrder(int $fromWarehouseId, int $toWarehouseId, ?string $remarks, int $userId): InventoryTransferOrder
{
return InventoryTransferOrder::create([
'from_warehouse_id' => $fromWarehouseId,
'to_warehouse_id' => $toWarehouseId,
'status' => 'draft',
'remarks' => $remarks,
'created_by' => $userId,
]);
}
/**
* 更新調撥單明細
*/
/**
* 更新調撥單明細 (支援精確 Diff 與自動日誌整合)
*/
public function updateItems(InventoryTransferOrder $order, array $itemsData): void
{
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 {
// 新增
$diff['added'][] = [
'product_name' => $item->product->name,
'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 的暫存屬性中
// 如果 Diff 有內容,才注入
if (!empty($diff['added']) || !empty($diff['removed']) || !empty($diff['updated'])) {
$order->activityProperties['items_diff'] = $diff;
}
});
}
/**
* 過帳 (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;
$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
]);
}
}