From 036f4a4fb6f6f576fa3dca2b213f73f2c1d57bb9 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Mon, 2 Mar 2026 17:30:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=84=AA=E5=8C=96=E6=8E=A1=E8=B3=BC=E5=96=AE?= =?UTF-8?q?=E8=88=87=E9=80=B2=E8=B2=A8=E5=96=AE=E6=93=8D=E4=BD=9C=E7=B4=80?= =?UTF-8?q?=E9=8C=84=EF=BC=9A=E6=96=B0=E5=A2=9E=E5=93=81=E9=A0=85=E6=98=8E?= =?UTF-8?q?=E7=B4=B0=E3=80=81ID=20=E8=BD=89=E5=90=8D=E7=A8=B1=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E3=80=81=E5=89=8D=E7=AB=AF=E5=A4=9A=E6=95=B8=E9=87=8F?= =?UTF-8?q?=20key=20=E9=80=9A=E7=94=A8=E9=A1=AF=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重構 PurchaseOrder@tapActivity:支援 vendor_id/warehouse_id/user_id 自動解析為名稱 - 修改 PurchaseOrderController@store:改用 saveQuietly + 手動日誌,建立時紀錄品項明細 - 修正 PurchaseOrderController update/destroy snapshot 跨模組取值為 null 的問題 - 修改 GoodsReceiptService@store:改用 saveQuietly + 手動日誌,建立時紀錄品項明細 - 修改 ActivityDetailDialog.tsx:支援 quantity/quantity_received/requested_qty 多 key 通用渲染 - 新增項目顯示金額與備註,更新項目增加金額與備註變更對比 --- .../Services/GoodsReceiptService.php | 42 ++++++- .../Controllers/PurchaseOrderController.php | 48 +++++++- .../Procurement/Models/PurchaseOrder.php | 57 ++++++++-- .../ActivityLog/ActivityDetailDialog.tsx | 106 +++++++++++------- resources/js/Pages/PurchaseOrder/Create.tsx | 7 +- 5 files changed, 195 insertions(+), 65 deletions(-) diff --git a/app/Modules/Inventory/Services/GoodsReceiptService.php b/app/Modules/Inventory/Services/GoodsReceiptService.php index ce34971..bd52690 100644 --- a/app/Modules/Inventory/Services/GoodsReceiptService.php +++ b/app/Modules/Inventory/Services/GoodsReceiptService.php @@ -38,10 +38,15 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei $data['user_id'] = auth()->id(); $data['status'] = GoodsReceipt::STATUS_DRAFT; // 預設草稿 - // 2. Create Header - $goodsReceipt = GoodsReceipt::create($data); + // 2. 靜默建立以抑制自動日誌(後續手動發送含品項明細的日誌) + $goodsReceipt = new GoodsReceipt($data); + $goodsReceipt->saveQuietly(); + + // 3. 建立品項並收集 items_diff + $diff = ['added' => [], 'removed' => [], 'updated' => []]; + $productIds = collect($data['items'])->pluck('product_id')->unique()->toArray(); + $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); - // 3. Process Items foreach ($data['items'] as $itemData) { // Create GR Item $grItem = new GoodsReceiptItem([ @@ -54,8 +59,39 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei 'expiry_date' => $itemData['expiry_date'] ?? null, ]); $goodsReceipt->items()->save($grItem); + + $product = $products->get($itemData['product_id']); + $diff['added'][] = [ + 'product_name' => $product?->name ?? '未知商品', + 'new' => [ + 'quantity_received' => (float)$itemData['quantity_received'], + 'unit_price' => (float)$itemData['unit_price'], + 'total_amount' => (float)($itemData['quantity_received'] * $itemData['unit_price']), + ] + ]; } + // 4. 手動發送高品質日誌(包含品項明細) + activity() + ->performedOn($goodsReceipt) + ->causedBy(auth()->user()) + ->event('created') + ->withProperties([ + 'items_diff' => $diff, + 'attributes' => [ + 'gr_number' => $goodsReceipt->code, + 'type' => $goodsReceipt->type, + 'warehouse_id' => $goodsReceipt->warehouse_id, + 'vendor_id' => $goodsReceipt->vendor_id, + 'purchase_order_id' => $goodsReceipt->purchase_order_id, + 'received_date' => $goodsReceipt->received_date, + 'status' => $goodsReceipt->status, + 'remarks' => $goodsReceipt->remarks, + 'user_id' => $goodsReceipt->user_id, + ] + ]) + ->log('created'); + return $goodsReceipt; }); } diff --git a/app/Modules/Procurement/Controllers/PurchaseOrderController.php b/app/Modules/Procurement/Controllers/PurchaseOrderController.php index c69dc44..dd6b72f 100644 --- a/app/Modules/Procurement/Controllers/PurchaseOrderController.php +++ b/app/Modules/Procurement/Controllers/PurchaseOrderController.php @@ -230,7 +230,8 @@ class PurchaseOrderController extends Controller $userId = $user->id; } - $order = PurchaseOrder::create([ + // 靜默建立以抑制自動日誌(後續手動發送含品項明細的日誌) + $order = new PurchaseOrder([ 'code' => $code, 'vendor_id' => $validated['vendor_id'], 'warehouse_id' => $validated['warehouse_id'], @@ -246,6 +247,12 @@ class PurchaseOrderController extends Controller 'invoice_date' => $validated['invoice_date'] ?? null, 'invoice_amount' => $validated['invoice_amount'] ?? null, ]); + $order->saveQuietly(); + + // 建立品項並收集 items_diff + $diff = ['added' => [], 'removed' => [], 'updated' => []]; + $productIds = collect($validated['items'])->pluck('productId')->unique()->toArray(); + $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); foreach ($validated['items'] as $item) { // 反算單價 @@ -258,8 +265,43 @@ class PurchaseOrderController extends Controller 'unit_price' => $unitPrice, 'subtotal' => $item['subtotal'], ]); + + $product = $products->get($item['productId']); + $diff['added'][] = [ + 'product_name' => $product?->name ?? '未知商品', + 'new' => [ + 'quantity' => (float)$item['quantity'], + 'subtotal' => (float)$item['subtotal'], + ] + ]; } + // 手動發送高品質日誌(包含品項明細) + activity() + ->performedOn($order) + ->causedBy($userId) + ->event('created') + ->withProperties([ + 'items_diff' => $diff, + 'attributes' => [ + 'po_number' => $order->code, + 'vendor_id' => $order->vendor_id, + 'warehouse_id' => $order->warehouse_id, + 'user_id' => $order->user_id, + 'status' => $order->status, + 'order_date' => $order->order_date, + 'expected_delivery_date' => $order->expected_delivery_date, + 'total_amount' => $order->total_amount, + 'tax_amount' => $order->tax_amount, + 'grand_total' => $order->grand_total, + 'remark' => $order->remark, + 'invoice_number' => $order->invoice_number, + 'invoice_date' => $order->invoice_date, + 'invoice_amount' => $order->invoice_amount, + ] + ]) + ->log('created'); + DB::commit(); } finally { $lock->release(); @@ -619,8 +661,6 @@ class PurchaseOrderController extends Controller 'snapshot' => [ 'po_number' => $order->code, 'vendor_name' => $order->vendor?->name, - 'warehouse_name' => $order->warehouse?->name, - 'user_name' => $order->user?->name, ] ]) ->log('updated'); @@ -673,8 +713,6 @@ class PurchaseOrderController extends Controller 'snapshot' => [ 'po_number' => $order->code, 'vendor_name' => $order->vendor?->name, - 'warehouse_name' => $order->warehouse?->name, - 'user_name' => $order->user?->name, ] ]) ->log('deleted'); diff --git a/app/Modules/Procurement/Models/PurchaseOrder.php b/app/Modules/Procurement/Models/PurchaseOrder.php index 0f0ded7..4029580 100644 --- a/app/Modules/Procurement/Models/PurchaseOrder.php +++ b/app/Modules/Procurement/Models/PurchaseOrder.php @@ -45,19 +45,52 @@ class PurchaseOrder extends Model public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) { - $snapshot = $activity->properties['snapshot'] ?? []; - - $snapshot['po_number'] = $this->code; - - if ($this->vendor) { - $snapshot['vendor_name'] = $this->vendor->name; - } - // Warehouse relation removed in Strict Mode. Snapshot should be set via manual hydration if needed, - // or during the procurement process where warehouse_id is known. + // 🚩 核心:轉換為陣列以避免 Indirect modification error + $properties = $activity->properties instanceof \Illuminate\Support\Collection + ? $activity->properties->toArray() + : $activity->properties; - $activity->properties = $activity->properties->merge([ - 'snapshot' => $snapshot - ]); + // 1. Snapshot 快照 + $snapshot = $properties['snapshot'] ?? []; + $snapshot['po_number'] = $this->code; + $snapshot['vendor_name'] = $this->vendor?->name; + // 倉庫名稱需透過服務取得(跨模組),若已在 snapshot 中則保留 + if (!isset($snapshot['warehouse_name']) && $this->warehouse_id) { + $warehouse = app(\App\Modules\Inventory\Contracts\InventoryServiceInterface::class)->getWarehouse($this->warehouse_id); + $snapshot['warehouse_name'] = $warehouse?->name ?? null; + } + $properties['snapshot'] = $snapshot; + + // 2. 名稱解析:自動將 attributes 與 old 中的 ID 換成人名/物名 + $resolver = function (&$data) { + if (empty($data) || !is_array($data)) return; + + // 使用者 ID 轉換 + foreach (['user_id', 'created_by', 'updated_by'] as $f) { + if (isset($data[$f]) && is_numeric($data[$f])) { + $data[$f] = \App\Modules\Core\Models\User::find($data[$f])?->name ?? $data[$f]; + } + } + // 廠商 ID 轉換 + if (isset($data['vendor_id']) && is_numeric($data['vendor_id'])) { + $data['vendor_id'] = Vendor::find($data['vendor_id'])?->name ?? $data['vendor_id']; + } + // 倉庫 ID 轉換(跨模組,透過服務) + if (isset($data['warehouse_id']) && is_numeric($data['warehouse_id'])) { + $warehouse = app(\App\Modules\Inventory\Contracts\InventoryServiceInterface::class)->getWarehouse($data['warehouse_id']); + $data['warehouse_id'] = $warehouse?->name ?? $data['warehouse_id']; + } + }; + + if (isset($properties['attributes'])) $resolver($properties['attributes']); + if (isset($properties['old'])) $resolver($properties['old']); + + // 3. 合併 activityProperties (手動傳入的 items_diff 等) + if (!empty($this->activityProperties)) { + $properties = array_merge($properties, $this->activityProperties); + } + + $activity->properties = $properties; } public function vendor(): \Illuminate\Database\Eloquent\Relations\BelongsTo diff --git a/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx b/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx index f32cdfd..0e838c7 100644 --- a/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx +++ b/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx @@ -573,50 +573,76 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P {!Array.isArray(activity.properties?.items_diff) && ( <> {/* 更新項目 */} - {activity.properties?.items_diff?.updated?.map((item: any, idx: number) => ( - - {item.product_name} - - 更新 - - -
- {item.old?.quantity !== item.new?.quantity && item.old?.quantity !== undefined && ( -
數量: {item.old.quantity}{item.new.quantity}
- )} - {item.old?.counted_qty !== item.new?.counted_qty && item.old?.counted_qty !== undefined && ( -
盤點量: {item.old.counted_qty ?? '未盤'}{item.new.counted_qty ?? '未盤'}
- )} -
-
-
- )) || null} + {activity.properties?.items_diff?.updated?.map((item: any, idx: number) => { + const getQty = (obj: any) => obj?.quantity ?? obj?.quantity_received ?? obj?.requested_qty; + const getAmt = (obj: any) => obj?.subtotal ?? obj?.total_amount; + const oldQty = getQty(item.old); + const newQty = getQty(item.new); + const oldAmt = getAmt(item.old); + const newAmt = getAmt(item.new); + return ( + + {item.product_name} + + 更新 + + +
+ {oldQty !== newQty && oldQty !== undefined && ( +
數量: {oldQty}{newQty}
+ )} + {oldAmt !== newAmt && oldAmt !== undefined && ( +
金額: {oldAmt}{newAmt}
+ )} + {item.old?.counted_qty !== item.new?.counted_qty && item.old?.counted_qty !== undefined && ( +
盤點量: {item.old.counted_qty ?? '未盤'}{item.new.counted_qty ?? '未盤'}
+ )} + {item.old?.remark !== item.new?.remark && item.old?.remark !== undefined && ( +
備註: {item.old.remark || '無'}{item.new.remark || '無'}
+ )} +
+
+
+ ); + }) || null} {/* 新增項目 */} - {activity.properties?.items_diff?.added?.map((item: any, idx: number) => ( - - {item.product_name} - - 新增 - - - 數量: {item.new?.quantity ?? item.quantity} {item.unit_name || item.new?.unit_name || ''} - - - )) || null} + {activity.properties?.items_diff?.added?.map((item: any, idx: number) => { + const qty = item.new?.quantity ?? item.new?.quantity_received ?? item.new?.requested_qty ?? item.quantity; + const amt = item.new?.subtotal ?? item.new?.total_amount; + const remark = item.new?.remark; + return ( + + {item.product_name} + + 新增 + + +
+
數量: {qty} {item.unit_name || item.new?.unit_name || ''}
+ {amt !== undefined &&
金額: {amt}
} + {remark &&
備註: {remark}
} +
+
+
+ ); + }) || null} {/* 移除項目 */} - {activity.properties?.items_diff?.removed?.map((item: any, idx: number) => ( - - {item.product_name} - - 移除 - - - 原數量: {item.old?.quantity ?? item.quantity} {item.unit_name || item.old?.unit_name || ''} - - - )) || null} + {activity.properties?.items_diff?.removed?.map((item: any, idx: number) => { + const qty = item.old?.quantity ?? item.old?.quantity_received ?? item.old?.requested_qty ?? item.quantity ?? item.quantity_received; + return ( + + {item.product_name} + + 移除 + + + 原數量: {qty} {item.unit_name || item.old?.unit_name || ''} + + + ); + }) || null} )} diff --git a/resources/js/Pages/PurchaseOrder/Create.tsx b/resources/js/Pages/PurchaseOrder/Create.tsx index 8b159cc..e4f525a 100644 --- a/resources/js/Pages/PurchaseOrder/Create.tsx +++ b/resources/js/Pages/PurchaseOrder/Create.tsx @@ -106,10 +106,7 @@ export default function CreatePurchaseOrder({ return; } - if (!expectedDate) { - toast.error("請選擇預計到貨日期"); - return; - } + if (items.length === 0) { toast.error("請至少新增一項採購商品"); @@ -140,7 +137,7 @@ export default function CreatePurchaseOrder({ vendor_id: supplierId, warehouse_id: warehouseId, order_date: orderDate, - expected_delivery_date: expectedDate, + expected_delivery_date: expectedDate || null, remark: notes, status: status, invoice_number: invoiceNumber || null,