feat: 整合門市領料日誌、API 文件存取、修改庫存與併發編號問題、供應商商品內聯編輯及日誌 UI 優化
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s
This commit is contained in:
@@ -5,10 +5,17 @@ use App\Modules\Inventory\Models\Inventory;
|
||||
use App\Modules\Inventory\Models\InventoryCountDoc;
|
||||
use App\Modules\Inventory\Models\InventoryAdjustDoc;
|
||||
use App\Modules\Inventory\Models\InventoryAdjustItem;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdjustService
|
||||
{
|
||||
protected InventoryServiceInterface $inventoryService;
|
||||
|
||||
public function __construct(InventoryServiceInterface $inventoryService)
|
||||
{
|
||||
$this->inventoryService = $inventoryService;
|
||||
}
|
||||
public function createDoc(string $warehouseId, string $reason, ?string $remarks = null, int $userId, ?int $countDocId = null): InventoryAdjustDoc
|
||||
{
|
||||
return InventoryAdjustDoc::create([
|
||||
@@ -161,29 +168,20 @@ class AdjustService
|
||||
'batch_number' => $item->batch_number,
|
||||
]);
|
||||
|
||||
// 如果是新建立的 object (id 為空),需要初始化 default
|
||||
// 如果是新建立的 object (id 為空),需要初始化 default 並先行儲存
|
||||
if (!$inventory->exists) {
|
||||
$inventory->unit_cost = $item->product->cost ?? 0;
|
||||
$inventory->quantity = 0;
|
||||
$inventory->total_value = 0;
|
||||
$inventory->saveQuietly();
|
||||
}
|
||||
|
||||
$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' => '庫存調整',
|
||||
$this->inventoryService->adjustInventory($inventory, [
|
||||
'operation' => 'add',
|
||||
'quantity' => $item->adjust_qty,
|
||||
'unit_cost' => $inventory->unit_cost,
|
||||
'balance_before' => $oldQty,
|
||||
'balance_after' => $newQty,
|
||||
'type' => 'adjustment',
|
||||
'reason' => "盤調單 {$doc->doc_no}: " . ($doc->reason ?? '手動調整'),
|
||||
'actual_time' => now(),
|
||||
'user_id' => $userId,
|
||||
'notes' => $item->notes,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,14 +60,6 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing Goods Receipt.
|
||||
*
|
||||
* @param GoodsReceipt $goodsReceipt
|
||||
* @param array $data
|
||||
* @return GoodsReceipt
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function update(GoodsReceipt $goodsReceipt, array $data)
|
||||
{
|
||||
if (!in_array($goodsReceipt->status, [GoodsReceipt::STATUS_DRAFT, GoodsReceipt::STATUS_REJECTED])) {
|
||||
@@ -75,14 +67,42 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($goodsReceipt, $data) {
|
||||
$goodsReceipt->update([
|
||||
$goodsReceipt->fill([
|
||||
'vendor_id' => $data['vendor_id'] ?? $goodsReceipt->vendor_id,
|
||||
'received_date' => $data['received_date'] ?? $goodsReceipt->received_date,
|
||||
'remarks' => $data['remarks'] ?? $goodsReceipt->remarks,
|
||||
]);
|
||||
|
||||
$dirty = $goodsReceipt->getDirty();
|
||||
$oldAttributes = [];
|
||||
$newAttributes = [];
|
||||
|
||||
foreach ($dirty as $key => $value) {
|
||||
$oldAttributes[$key] = $goodsReceipt->getOriginal($key);
|
||||
$newAttributes[$key] = $value;
|
||||
}
|
||||
|
||||
// 儲存但不觸發事件,以避免重複記錄
|
||||
$goodsReceipt->saveQuietly();
|
||||
|
||||
// 捕捉包含商品名稱的舊項目以進行比對
|
||||
$oldItemsCollection = $goodsReceipt->items()->get();
|
||||
$oldProductIds = $oldItemsCollection->pluck('product_id')->unique()->toArray();
|
||||
$oldProducts = $this->inventoryService->getProductsByIds($oldProductIds)->keyBy('id');
|
||||
|
||||
$oldItems = $oldItemsCollection->map(function($item) use ($oldProducts) {
|
||||
$product = $oldProducts->get($item->product_id);
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'product_id' => $item->product_id,
|
||||
'product_name' => $product?->name ?? 'Unknown',
|
||||
'quantity_received' => (float) $item->quantity_received,
|
||||
'unit_price' => (float) $item->unit_price,
|
||||
'total_amount' => (float) $item->total_amount,
|
||||
];
|
||||
})->keyBy('product_id');
|
||||
|
||||
if (isset($data['items'])) {
|
||||
// Simple strategy: delete existing items and recreate
|
||||
$goodsReceipt->items()->delete();
|
||||
|
||||
foreach ($data['items'] as $itemData) {
|
||||
@@ -99,6 +119,75 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
|
||||
}
|
||||
}
|
||||
|
||||
// 計算項目差異
|
||||
$itemDiffs = [
|
||||
'added' => [],
|
||||
'removed' => [],
|
||||
'updated' => [],
|
||||
];
|
||||
|
||||
$newItemsCollection = $goodsReceipt->items()->get();
|
||||
$newProductIds = $newItemsCollection->pluck('product_id')->unique()->toArray();
|
||||
$newProducts = $this->inventoryService->getProductsByIds($newProductIds)->keyBy('id');
|
||||
|
||||
$newItemsFormatted = $newItemsCollection->map(function($item) use ($newProducts) {
|
||||
$product = $newProducts->get($item->product_id);
|
||||
return [
|
||||
'product_id' => $item->product_id,
|
||||
'product_name' => $product?->name ?? 'Unknown',
|
||||
'quantity_received' => (float) $item->quantity_received,
|
||||
'unit_price' => (float) $item->unit_price,
|
||||
'total_amount' => (float) $item->total_amount,
|
||||
];
|
||||
})->keyBy('product_id');
|
||||
|
||||
foreach ($oldItems as $productId => $oldItem) {
|
||||
if (!$newItemsFormatted->has($productId)) {
|
||||
$itemDiffs['removed'][] = $oldItem;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($newItemsFormatted as $productId => $newItem) {
|
||||
if (!$oldItems->has($productId)) {
|
||||
$itemDiffs['added'][] = $newItem;
|
||||
} else {
|
||||
$oldItem = $oldItems[$productId];
|
||||
if (
|
||||
$oldItem['quantity_received'] != $newItem['quantity_received'] ||
|
||||
$oldItem['unit_price'] != $newItem['unit_price'] ||
|
||||
$oldItem['total_amount'] != $newItem['total_amount']
|
||||
) {
|
||||
$itemDiffs['updated'][] = [
|
||||
'product_name' => $newItem['product_name'],
|
||||
'old' => [
|
||||
'quantity_received' => $oldItem['quantity_received'],
|
||||
'unit_price' => $oldItem['unit_price'],
|
||||
'total_amount' => $oldItem['total_amount'],
|
||||
],
|
||||
'new' => [
|
||||
'quantity_received' => $newItem['quantity_received'],
|
||||
'unit_price' => $newItem['unit_price'],
|
||||
'total_amount' => $newItem['total_amount'],
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有變更,手動觸發單一合併日誌
|
||||
if (!empty($newAttributes) || !empty($itemDiffs['added']) || !empty($itemDiffs['removed']) || !empty($itemDiffs['updated'])) {
|
||||
activity()
|
||||
->performedOn($goodsReceipt)
|
||||
->causedBy(auth()->user())
|
||||
->event('updated')
|
||||
->withProperties([
|
||||
'attributes' => $newAttributes,
|
||||
'old' => $oldAttributes,
|
||||
'items_diff' => $itemDiffs,
|
||||
])
|
||||
->log('updated');
|
||||
}
|
||||
|
||||
return $goodsReceipt->fresh('items');
|
||||
});
|
||||
}
|
||||
@@ -162,23 +251,35 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
|
||||
}
|
||||
|
||||
|
||||
private function generateCode(string $date)
|
||||
private function generateCode(string $date): string
|
||||
{
|
||||
// Format: GR-YYYYMMDD-NN
|
||||
$prefix = 'GR-' . date('Ymd', strtotime($date)) . '-';
|
||||
|
||||
$last = GoodsReceipt::where('code', 'like', $prefix . '%')
|
||||
->orderBy('id', 'desc')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
// 使用 Cache Lock 防止併發時產生重複單號
|
||||
$lock = \Illuminate\Support\Facades\Cache::lock('gr_code_generation', 10);
|
||||
|
||||
if ($last) {
|
||||
$seq = intval(substr($last->code, -2)) + 1;
|
||||
} else {
|
||||
$seq = 1;
|
||||
if (!$lock->get()) {
|
||||
throw new \Exception('系統忙碌中,進貨單號生成失敗,請稍後再試');
|
||||
}
|
||||
|
||||
return $prefix . str_pad($seq, 2, '0', STR_PAD_LEFT);
|
||||
try {
|
||||
// Format: GR-YYYYMMDD-NN
|
||||
$prefix = 'GR-' . date('Ymd', strtotime($date)) . '-';
|
||||
|
||||
$last = GoodsReceipt::where('code', 'like', $prefix . '%')
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if ($last) {
|
||||
$seq = intval(substr($last->code, -2)) + 1;
|
||||
} else {
|
||||
$seq = 1;
|
||||
}
|
||||
|
||||
$code = $prefix . str_pad($seq, 2, '0', STR_PAD_LEFT);
|
||||
|
||||
return $code;
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -181,11 +181,11 @@ class InventoryService implements InventoryServiceInterface
|
||||
|
||||
// 更新其他可能變更的欄位 (如最後入庫日)
|
||||
$inventory->arrival_date = $data['arrival_date'] ?? $inventory->arrival_date;
|
||||
$inventory->save();
|
||||
$inventory->saveQuietly();
|
||||
} else {
|
||||
// 若不存在,則建立新紀錄
|
||||
$unitCost = $data['unit_cost'] ?? 0;
|
||||
$inventory = Inventory::create([
|
||||
$inventory = new Inventory([
|
||||
'warehouse_id' => $data['warehouse_id'],
|
||||
'product_id' => $data['product_id'],
|
||||
'quantity' => $data['quantity'],
|
||||
@@ -199,9 +199,10 @@ class InventoryService implements InventoryServiceInterface
|
||||
'quality_status' => $data['quality_status'] ?? 'normal',
|
||||
'source_purchase_order_id' => $data['source_purchase_order_id'] ?? null,
|
||||
]);
|
||||
$inventory->saveQuietly();
|
||||
}
|
||||
|
||||
\App\Modules\Inventory\Models\InventoryTransaction::create([
|
||||
$transaction = new \App\Modules\Inventory\Models\InventoryTransaction([
|
||||
'inventory_id' => $inventory->id,
|
||||
'type' => '入庫',
|
||||
'quantity' => $data['quantity'],
|
||||
@@ -214,6 +215,7 @@ class InventoryService implements InventoryServiceInterface
|
||||
'user_id' => auth()->id(),
|
||||
'actual_time' => now(),
|
||||
]);
|
||||
$transaction->saveQuietly();
|
||||
|
||||
return $inventory;
|
||||
});
|
||||
@@ -225,13 +227,12 @@ class InventoryService implements InventoryServiceInterface
|
||||
$inventory = Inventory::lockForUpdate()->findOrFail($inventoryId);
|
||||
$balanceBefore = $inventory->quantity;
|
||||
|
||||
$inventory->decrement('quantity', $quantity); // decrement 不會自動觸發 total_value 更新
|
||||
// 需要手動更新總價值
|
||||
$inventory->refresh();
|
||||
// 手動更新以配合 saveQuietly 消除日誌
|
||||
$inventory->quantity -= $quantity;
|
||||
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
|
||||
$inventory->save();
|
||||
$inventory->saveQuietly();
|
||||
|
||||
\App\Modules\Inventory\Models\InventoryTransaction::create([
|
||||
$transaction = new \App\Modules\Inventory\Models\InventoryTransaction([
|
||||
'inventory_id' => $inventory->id,
|
||||
'type' => '出庫',
|
||||
'quantity' => -$quantity,
|
||||
@@ -244,6 +245,7 @@ class InventoryService implements InventoryServiceInterface
|
||||
'user_id' => auth()->id(),
|
||||
'actual_time' => now(),
|
||||
]);
|
||||
$transaction->saveQuietly();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -825,7 +827,8 @@ class InventoryService implements InventoryServiceInterface
|
||||
}
|
||||
|
||||
if (abs($changeQty) > 0.0001) {
|
||||
$inventory->transactions()->create([
|
||||
$transaction = new \App\Modules\Inventory\Models\InventoryTransaction([
|
||||
'inventory_id' => $inventory->id,
|
||||
'type' => $chineseType,
|
||||
'quantity' => $changeQty,
|
||||
'unit_cost' => $inventory->unit_cost,
|
||||
@@ -835,6 +838,7 @@ class InventoryService implements InventoryServiceInterface
|
||||
'actual_time' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
$transaction->saveQuietly();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,24 +23,77 @@ class StoreRequisitionService
|
||||
/**
|
||||
* 建立叫貨單(含明細)
|
||||
*/
|
||||
public function create(array $data, array $items, int $userId): StoreRequisition
|
||||
public function create(array $data, array $items, int $userId, bool $submitImmediately = false): StoreRequisition
|
||||
{
|
||||
return DB::transaction(function () use ($data, $items, $userId) {
|
||||
$requisition = StoreRequisition::create([
|
||||
return DB::transaction(function () use ($data, $items, $userId, $submitImmediately) {
|
||||
$requisition = new StoreRequisition([
|
||||
'store_warehouse_id' => $data['store_warehouse_id'],
|
||||
'status' => 'draft',
|
||||
'status' => $submitImmediately ? 'pending' : 'draft',
|
||||
'submitted_at' => $submitImmediately ? now() : null,
|
||||
'remark' => $data['remark'] ?? null,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
|
||||
// 手動產生單號,因為 saveQuietly 會繞過模型事件
|
||||
if (empty($requisition->doc_no)) {
|
||||
$today = date('Ymd');
|
||||
$prefix = 'SR-' . $today . '-';
|
||||
$lastDoc = StoreRequisition::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';
|
||||
}
|
||||
$requisition->doc_no = $prefix . $nextNumber;
|
||||
}
|
||||
|
||||
// 靜默建立以抑制自動日誌
|
||||
$requisition->saveQuietly();
|
||||
|
||||
$diff = ['added' => [], 'removed' => [], 'updated' => []];
|
||||
foreach ($items as $item) {
|
||||
$requisition->items()->create([
|
||||
'product_id' => $item['product_id'],
|
||||
'requested_qty' => $item['requested_qty'],
|
||||
'remark' => $item['remark'] ?? null,
|
||||
]);
|
||||
|
||||
$product = \App\Modules\Inventory\Models\Product::find($item['product_id']);
|
||||
$diff['added'][] = [
|
||||
'product_name' => $product?->name ?? '未知商品',
|
||||
'new' => [
|
||||
'quantity' => (float)$item['requested_qty'],
|
||||
'remark' => $item['remark'] ?? null,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 如果需直接提交,觸發通知
|
||||
if ($submitImmediately) {
|
||||
$this->notifyApprovers($requisition, 'submitted', $userId);
|
||||
}
|
||||
|
||||
// 手動發送高品質日誌
|
||||
activity()
|
||||
->performedOn($requisition)
|
||||
->causedBy($userId)
|
||||
->event('created')
|
||||
->withProperties([
|
||||
'items_diff' => $diff,
|
||||
'attributes' => [
|
||||
'doc_no' => $requisition->doc_no,
|
||||
'store_warehouse_id' => $requisition->store_warehouse_id,
|
||||
'status' => $requisition->status,
|
||||
'remark' => $requisition->remark,
|
||||
'created_by' => $requisition->created_by,
|
||||
'submitted_at' => $requisition->submitted_at,
|
||||
]
|
||||
])
|
||||
->log('created');
|
||||
|
||||
return $requisition->load('items');
|
||||
});
|
||||
}
|
||||
@@ -57,13 +110,74 @@ class StoreRequisitionService
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($requisition, $data, $items) {
|
||||
$requisition->update([
|
||||
'store_warehouse_id' => $data['store_warehouse_id'],
|
||||
'remark' => $data['remark'] ?? null,
|
||||
'reject_reason' => null, // 清除駁回原因
|
||||
]);
|
||||
// 擷取舊狀態供日誌對照
|
||||
$oldAttributes = [
|
||||
'store_warehouse_id' => $requisition->store_warehouse_id,
|
||||
'remark' => $requisition->remark,
|
||||
];
|
||||
|
||||
// 重建明細
|
||||
// 手動更新屬性
|
||||
$requisition->store_warehouse_id = $data['store_warehouse_id'];
|
||||
$requisition->remark = $data['remark'] ?? null;
|
||||
$requisition->reject_reason = null; // 清除駁回原因
|
||||
|
||||
// 品項對比邏輯
|
||||
$oldItems = $requisition->items()->with('product:id,name')->get();
|
||||
$oldItemsMap = $oldItems->keyBy('product_id');
|
||||
$newItemsMap = collect($items)->keyBy('product_id');
|
||||
|
||||
$diff = [
|
||||
'added' => [],
|
||||
'removed' => [],
|
||||
'updated' => [],
|
||||
];
|
||||
|
||||
// 1. 處理更新與新增
|
||||
foreach ($items as $itemData) {
|
||||
$productId = $itemData['product_id'];
|
||||
$newQty = (float)$itemData['requested_qty'];
|
||||
$newRemark = $itemData['remark'] ?? null;
|
||||
|
||||
if ($oldItemsMap->has($productId)) {
|
||||
$oldItem = $oldItemsMap->get($productId);
|
||||
if ((float)$oldItem->requested_qty !== $newQty || $oldItem->remark !== $newRemark) {
|
||||
$diff['updated'][] = [
|
||||
'product_name' => $oldItem->product?->name ?? '未知商品',
|
||||
'old' => [
|
||||
'quantity' => (float)$oldItem->requested_qty,
|
||||
'remark' => $oldItem->remark,
|
||||
],
|
||||
'new' => [
|
||||
'quantity' => $newQty,
|
||||
'remark' => $newRemark,
|
||||
]
|
||||
];
|
||||
}
|
||||
$oldItemsMap->forget($productId);
|
||||
} else {
|
||||
$product = \App\Modules\Inventory\Models\Product::find($productId);
|
||||
$diff['added'][] = [
|
||||
'product_name' => $product?->name ?? '未知商品',
|
||||
'new' => [
|
||||
'quantity' => $newQty,
|
||||
'remark' => $newRemark,
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 處理移除
|
||||
foreach ($oldItemsMap as $productId => $oldItem) {
|
||||
$diff['removed'][] = [
|
||||
'product_name' => $oldItem->product?->name ?? '未知商品',
|
||||
'old' => [
|
||||
'quantity' => (float)$oldItem->requested_qty,
|
||||
'remark' => $oldItem->remark,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 儲存實際變動
|
||||
$requisition->items()->delete();
|
||||
foreach ($items as $item) {
|
||||
$requisition->items()->create([
|
||||
@@ -73,6 +187,32 @@ class StoreRequisitionService
|
||||
]);
|
||||
}
|
||||
|
||||
// 檢查是否有任何變動 (主表或明細)
|
||||
$isDirty = $requisition->isDirty();
|
||||
$hasItemsDiff = !empty($diff['added']) || !empty($diff['removed']) || !empty($diff['updated']);
|
||||
|
||||
if ($isDirty || $hasItemsDiff) {
|
||||
// 擷取新狀態
|
||||
$newAttributes = [
|
||||
'store_warehouse_id' => $requisition->store_warehouse_id,
|
||||
'remark' => $requisition->remark,
|
||||
];
|
||||
|
||||
// 靜默更新
|
||||
$requisition->saveQuietly();
|
||||
|
||||
// 手動發送紀錄
|
||||
activity()
|
||||
->performedOn($requisition)
|
||||
->event('updated')
|
||||
->withProperties([
|
||||
'items_diff' => $diff,
|
||||
'attributes' => $newAttributes,
|
||||
'old' => $oldAttributes
|
||||
])
|
||||
->log('updated');
|
||||
}
|
||||
|
||||
return $requisition->load('items');
|
||||
});
|
||||
}
|
||||
@@ -241,6 +381,27 @@ class StoreRequisitionService
|
||||
|
||||
if (!empty($transferItems)) {
|
||||
$this->transferService->updateItems($transferOrder, $transferItems);
|
||||
|
||||
// 手動發送調撥單的「已建立」合併日誌,包含初始明細
|
||||
activity()
|
||||
->performedOn($transferOrder)
|
||||
->causedBy($userId)
|
||||
->event('created')
|
||||
->withProperties(array_merge(
|
||||
['items_diff' => $transferOrder->activityProperties['items_diff'] ?? []],
|
||||
[
|
||||
'attributes' => [
|
||||
'doc_no' => $transferOrder->doc_no,
|
||||
'from_warehouse_id' => $transferOrder->from_warehouse_id,
|
||||
'to_warehouse_id' => $transferOrder->to_warehouse_id,
|
||||
'transit_warehouse_id' => $transferOrder->transit_warehouse_id,
|
||||
'remarks' => $transferOrder->remarks,
|
||||
'status' => $transferOrder->status,
|
||||
'created_by' => $transferOrder->created_by,
|
||||
]
|
||||
]
|
||||
))
|
||||
->log('created');
|
||||
}
|
||||
|
||||
// 更新叫貨單狀態
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user