feat: 優化庫存調撥單操作紀錄與 UI 佈局
This commit is contained in:
@@ -82,50 +82,9 @@ class TransferOrderController extends Controller
|
||||
auth()->id()
|
||||
);
|
||||
|
||||
// 記錄活動
|
||||
activity()
|
||||
->performedOn($order)
|
||||
->causedBy(auth()->user())
|
||||
->event('created')
|
||||
->withProperties([
|
||||
'attributes' => $order->toArray(),
|
||||
'snapshot' => [
|
||||
'doc_no' => $order->doc_no,
|
||||
'from_warehouse_name' => $order->fromWarehouse?->name,
|
||||
'to_warehouse_name' => $order->toWarehouse?->name,
|
||||
]
|
||||
])
|
||||
->log('created');
|
||||
|
||||
// 如果請求包含單筆商品資訊
|
||||
if ($request->has('product_id')) {
|
||||
$this->transferService->updateItems($order, [[
|
||||
'product_id' => $validated['product_id'],
|
||||
'quantity' => $validated['quantity'],
|
||||
'batch_number' => $validated['batch_number'] ?? null,
|
||||
]]);
|
||||
}
|
||||
|
||||
// 如果是撥補單,執行直接過帳
|
||||
if ($request->input('instant_post') === true) {
|
||||
try {
|
||||
$this->transferService->post($order, auth()->id());
|
||||
|
||||
// 記錄過帳活動
|
||||
activity()
|
||||
->performedOn($order)
|
||||
->causedBy(auth()->user())
|
||||
->event('posted')
|
||||
->withProperties([
|
||||
'attributes' => ['status' => 'posted'],
|
||||
'old' => ['status' => 'draft'],
|
||||
'snapshot' => [
|
||||
'doc_no' => $order->doc_no,
|
||||
'from_warehouse_name' => $order->fromWarehouse?->name,
|
||||
'to_warehouse_name' => $order->toWarehouse?->name,
|
||||
]
|
||||
])
|
||||
->log('posted');
|
||||
|
||||
return redirect()->back()->with('success', '撥補成功,庫存已更新');
|
||||
} catch (\Exception $e) {
|
||||
@@ -185,60 +144,35 @@ class TransferOrderController extends Controller
|
||||
return redirect()->back()->with('error', '只能修改草稿狀態的單據');
|
||||
}
|
||||
|
||||
if ($request->input('action') === 'post') {
|
||||
try {
|
||||
$this->transferService->post($order, auth()->id());
|
||||
|
||||
// 記錄活動
|
||||
activity()
|
||||
->performedOn($order)
|
||||
->causedBy(auth()->user())
|
||||
->event('posted')
|
||||
->withProperties([
|
||||
'attributes' => ['status' => 'posted'],
|
||||
'old' => ['status' => 'draft'],
|
||||
'snapshot' => [
|
||||
'doc_no' => $order->doc_no,
|
||||
'from_warehouse_name' => $order->fromWarehouse?->name,
|
||||
'to_warehouse_name' => $order->toWarehouse?->name,
|
||||
]
|
||||
])
|
||||
->log('posted');
|
||||
|
||||
return redirect()->route('inventory.transfer.index')
|
||||
->with('success', '調撥單已過帳完成');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()->withErrors(['items' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'items' => 'array',
|
||||
'items.*.product_id' => 'required|exists:products,id',
|
||||
'items.*.quantity' => 'required|numeric|min:0.01',
|
||||
'items.*.batch_number' => 'nullable|string',
|
||||
'items.*.notes' => 'nullable|string',
|
||||
'remarks' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// 1. 先更新資料
|
||||
if ($request->has('items')) {
|
||||
$this->transferService->updateItems($order, $validated['items']);
|
||||
}
|
||||
|
||||
$order->update($request->only(['remarks']));
|
||||
$order->fill($request->only(['remarks']));
|
||||
|
||||
// [IMPORTANT] 使用 touch() 確保即便只有品項異動,也會因為 updated_at 變更而觸發自動日誌
|
||||
$order->touch();
|
||||
|
||||
// 記錄暫存活動
|
||||
activity()
|
||||
->performedOn($order)
|
||||
->causedBy(auth()->user())
|
||||
->event('updated')
|
||||
->withProperties([
|
||||
'snapshot' => [
|
||||
'doc_no' => $order->doc_no,
|
||||
'from_warehouse_name' => $order->fromWarehouse?->name,
|
||||
'to_warehouse_name' => $order->toWarehouse?->name,
|
||||
]
|
||||
])
|
||||
->log('updated_items');
|
||||
// 2. 判斷是否需要過帳
|
||||
if ($request->input('action') === 'post') {
|
||||
try {
|
||||
$this->transferService->post($order, auth()->id());
|
||||
return redirect()->route('inventory.transfer.index')
|
||||
->with('success', '調撥單已過帳完成');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()->withErrors(['items' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', '儲存成功');
|
||||
}
|
||||
@@ -249,20 +183,6 @@ class TransferOrderController extends Controller
|
||||
return redirect()->back()->with('error', '只能刪除草稿狀態的單據');
|
||||
}
|
||||
|
||||
// 記錄活動
|
||||
activity()
|
||||
->performedOn($order)
|
||||
->causedBy(auth()->user())
|
||||
->event('deleted')
|
||||
->withProperties([
|
||||
'snapshot' => [
|
||||
'doc_no' => $order->doc_no,
|
||||
'from_warehouse_name' => $order->fromWarehouse?->name,
|
||||
'to_warehouse_name' => $order->toWarehouse?->name,
|
||||
]
|
||||
])
|
||||
->log('deleted');
|
||||
|
||||
$order->items()->delete();
|
||||
$order->delete();
|
||||
|
||||
|
||||
@@ -1,16 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Inventory\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use App\Modules\Core\Models\User;
|
||||
|
||||
class InventoryTransferOrder extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasFactory, LogsActivity;
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logFillable()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array 暫存的活動紀錄屬性 (不會存入資料庫)
|
||||
*/
|
||||
public $activityProperties = [];
|
||||
|
||||
/**
|
||||
* 自定義日誌屬性名稱解析
|
||||
*/
|
||||
public function tapActivity(\Spatie\Activitylog\Models\Activity $activity, string $eventName)
|
||||
{
|
||||
$properties = $activity->properties->toArray();
|
||||
|
||||
// 處置日誌事件說明
|
||||
if ($eventName === 'created') {
|
||||
$activity->description = 'created';
|
||||
} elseif ($eventName === 'updated') {
|
||||
// 如果屬性中有 status 且變更為 completed,將描述改為 posted
|
||||
if (isset($properties['attributes']['status']) && $properties['attributes']['status'] === 'completed') {
|
||||
$activity->description = 'posted';
|
||||
$eventName = 'posted'; // 供後續快照邏輯判定
|
||||
} else {
|
||||
$activity->description = 'updated';
|
||||
}
|
||||
}
|
||||
|
||||
// 處理倉庫 ID 轉名稱
|
||||
$idToNameFields = [
|
||||
'from_warehouse_id' => 'fromWarehouse',
|
||||
'to_warehouse_id' => 'toWarehouse',
|
||||
'created_by' => 'createdBy',
|
||||
'posted_by' => 'postedBy',
|
||||
];
|
||||
|
||||
foreach (['attributes', 'old'] as $part) {
|
||||
if (isset($properties[$part])) {
|
||||
foreach ($idToNameFields as $idField => $relation) {
|
||||
if (isset($properties[$part][$idField])) {
|
||||
$id = $properties[$part][$idField];
|
||||
$nameField = str_replace('_id', '_name', $idField);
|
||||
|
||||
$name = null;
|
||||
if ($this->relationLoaded($relation) && $this->$relation && $this->$relation->id == $id) {
|
||||
$name = $this->$relation->name;
|
||||
} else {
|
||||
$model = $this->$relation()->getRelated()->find($id);
|
||||
$name = $model ? $model->name : "ID: $id";
|
||||
}
|
||||
$properties[$part][$nameField] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 基本單據資訊快照 (包含單號、來源、目的地)
|
||||
if (in_array($eventName, ['created', 'updated', 'posted', 'deleted'])) {
|
||||
$properties['snapshot'] = [
|
||||
'doc_no' => $this->doc_no,
|
||||
'from_warehouse_name' => $this->fromWarehouse?->name,
|
||||
'to_warehouse_name' => $this->toWarehouse?->name,
|
||||
'status' => $this->status,
|
||||
];
|
||||
}
|
||||
|
||||
// 移除輔助欄位與雜訊
|
||||
if (isset($properties['attributes'])) {
|
||||
unset($properties['attributes']['from_warehouse_name']);
|
||||
unset($properties['attributes']['to_warehouse_name']);
|
||||
unset($properties['attributes']['activityProperties']);
|
||||
unset($properties['attributes']['updated_at']);
|
||||
}
|
||||
if (isset($properties['old'])) {
|
||||
unset($properties['old']['updated_at']);
|
||||
}
|
||||
|
||||
// 合併暫存屬性 (例如 items_diff)
|
||||
if (!empty($this->activityProperties)) {
|
||||
$properties = array_merge($properties, $this->activityProperties);
|
||||
}
|
||||
|
||||
$activity->properties = collect($properties);
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'doc_no',
|
||||
|
||||
@@ -28,18 +28,94 @@ class TransferService
|
||||
/**
|
||||
* 更新調撥單明細
|
||||
*/
|
||||
/**
|
||||
* 更新調撥單明細 (支援精確 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) {
|
||||
$order->items()->create([
|
||||
$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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -49,6 +125,9 @@ class TransferService
|
||||
*/
|
||||
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;
|
||||
@@ -131,11 +210,25 @@ class TransferService
|
||||
]);
|
||||
}
|
||||
|
||||
$order->update([
|
||||
'status' => 'completed',
|
||||
'posted_at' => now(),
|
||||
'posted_by' => $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(); // 觸發自動日誌
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user