From 1833ca192de5a6951a2c7abc9c61ef76d132f3e0 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Thu, 29 Jan 2026 09:36:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(inventory):=20=E5=84=AA=E5=8C=96=E7=9B=A4?= =?UTF-8?q?=E9=BB=9E=E9=A1=AF=E7=A4=BA=E8=88=87=E6=AC=8A=E9=99=90=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/CountDocController.php | 2 +- .../Inventory/Services/CountService.php | 51 +-- database/seeders/PermissionSeeder.php | 5 +- resources/js/Pages/Inventory/Adjust/Show.tsx | 165 ++++---- resources/js/Pages/Inventory/Count/Show.tsx | 367 +++++++++--------- 5 files changed, 273 insertions(+), 317 deletions(-) diff --git a/app/Modules/Inventory/Controllers/CountDocController.php b/app/Modules/Inventory/Controllers/CountDocController.php index f33edad..8b37e9a 100644 --- a/app/Modules/Inventory/Controllers/CountDocController.php +++ b/app/Modules/Inventory/Controllers/CountDocController.php @@ -137,7 +137,7 @@ class CountDocController extends Controller if ($request->input('action') === 'complete') { $this->countService->complete($doc, auth()->id()); return redirect()->route('inventory.count.index') - ->with('success', '盤點已完成並過帳'); + ->with('success', '盤點單已完成'); } return redirect()->back()->with('success', '暫存成功'); diff --git a/app/Modules/Inventory/Services/CountService.php b/app/Modules/Inventory/Services/CountService.php index 12b0d1e..3404e5e 100644 --- a/app/Modules/Inventory/Services/CountService.php +++ b/app/Modules/Inventory/Services/CountService.php @@ -75,55 +75,8 @@ class CountService public function complete(InventoryCountDoc $doc, int $userId): void { DB::transaction(function () use ($doc, $userId) { - foreach ($doc->items as $item) { - // 如果沒有輸入實盤數量,預設跳過或是視為 0? - // 安全起見:如果 counted_qty 是 null,表示沒盤到,跳過不處理 (或者依業務邏輯視為0) - // 這裡假設前端會確保有送出資料,若 null 則不做異動 - if (is_null($item->counted_qty)) { - continue; - } - - $diff = $item->counted_qty - $item->system_qty; - - // 如果無差異,更新 item 狀態即可 (diff_qty 已經是 computed field 或在儲存時計算) - // 這裡 update 一下 diff_qty 以防萬一 - $item->update(['diff_qty' => $diff]); - - if (abs($diff) > 0.0001) { - // 找回原本的 Inventory - $inventory = Inventory::where('warehouse_id', $doc->warehouse_id) - ->where('product_id', $item->product_id) - ->where('batch_number', $item->batch_number) - ->first(); - - if (!$inventory) { - // 如果原本沒庫存紀錄 (例如是新增的盤點項目),需要新建 Inventory - // 但目前 snapshot 邏輯只抓現有。若允許 "盤盈" (發現不在帳上的),需要額外邏輯 - // 暫時略過 "新增 Inventory" 的複雜邏輯,假設只能針對 existing batch 調整 - continue; - } - - $oldQty = $inventory->quantity; - $newQty = $oldQty + $diff; - - $inventory->quantity = $newQty; - $inventory->total_value = $inventory->unit_cost * $newQty; - $inventory->save(); - - // 寫入 Transaction - $inventory->transactions()->create([ - 'type' => '盤點調整', - 'quantity' => $diff, - 'unit_cost' => $inventory->unit_cost, - 'balance_before' => $oldQty, - 'balance_after' => $newQty, - 'reason' => "盤點單 {$doc->doc_no} 過帳", - 'actual_time' => now(), - 'user_id' => $userId, - ]); - } - } - + // 僅更新單據狀態為「已完成」,不執行庫存入庫/調整 + // 盤點單僅作為記錄,後續調整由盤調單 (AdjustDoc) 執行 $doc->update([ 'status' => 'completed', 'completed_at' => now(), diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index d07794a..e8beb24 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -36,7 +36,8 @@ class PermissionSeeder extends Seeder 'inventory.view', 'inventory.view_cost', // 查看成本與價值 'inventory.adjust', - 'inventory.transfer', + 'inventory.count', // 庫存盤點 + 'inventory.transfer', // 庫存調撥 'inventory.delete', // 進貨單管理 @@ -132,7 +133,7 @@ class PermissionSeeder extends Seeder // warehouse-manager 管理庫存與倉庫 $warehouseManager->givePermissionTo([ 'products.view', - 'inventory.view', 'inventory.adjust', 'inventory.transfer', 'inventory.delete', + 'inventory.view', 'inventory.adjust', 'inventory.count', 'inventory.transfer', 'inventory.delete', 'goods_receipts.view', 'goods_receipts.create', 'goods_receipts.edit', 'goods_receipts.delete', 'production_orders.view', 'production_orders.create', 'production_orders.edit', 'warehouses.view', 'warehouses.create', 'warehouses.edit', diff --git a/resources/js/Pages/Inventory/Adjust/Show.tsx b/resources/js/Pages/Inventory/Adjust/Show.tsx index 3a86fe2..eaf8bf1 100644 --- a/resources/js/Pages/Inventory/Adjust/Show.tsx +++ b/resources/js/Pages/Inventory/Adjust/Show.tsx @@ -79,7 +79,6 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) { action: 'save', }); - const [newItemOpen, setNewItemOpen] = useState(false); // Helper to add new item const addItem = (product: any, batchNumber: string | null) => { @@ -107,7 +106,6 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) { notes: '', } ]); - setNewItemOpen(false); }; const removeItem = (index: number) => { @@ -294,96 +292,99 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) { - - - 調整項目清單 +
+
+

調整項目清單

{isDraft && ( addItem(product, batch)} /> )} - - - - - # - 商品資訊 - 批號 - 單位 - 調整前庫存 - 調整數量 (+/-) - 備註說明 - {isDraft && } - - - - {data.items.length === 0 ? ( + +
+
+ - - 尚未載入任何調整項目 - + # + 商品資訊 + 批號 + 單位 + 調整前庫存 + 調整數量 (+/-) + 備註說明 + {isDraft && } - ) : ( - data.items.map((item, index) => ( - - {index + 1} - -
{item.product_name}
-
{item.product_code}
+
+ + {data.items.length === 0 ? ( + + + 尚未載入任何調整項目 - {item.batch_number || '-'} - {item.unit} - - {item.qty_before} - - - {isDraft ? ( - updateItem(index, 'adjust_qty', e.target.value)} - /> - ) : ( - 0 ? 'text-green-600' : 'text-red-600'}`}> - {Number(item.adjust_qty) > 0 ? '+' : ''}{item.adjust_qty} - - )} - - - {isDraft ? ( - updateItem(index, 'notes', e.target.value)} - placeholder="輸入備註..." - /> - ) : ( - {item.notes || '-'} - )} - - {isDraft && ( - - - - )} - )) - )} - -
- + ) : ( + data.items.map((item, index) => ( + + {index + 1} + +
{item.product_name}
+
{item.product_code}
+
+ {item.batch_number || '-'} + {item.unit} + + {item.qty_before} + + + {isDraft ? ( + updateItem(index, 'adjust_qty', e.target.value)} + /> + ) : ( + 0 ? 'text-green-600' : 'text-red-600'}`}> + {Number(item.adjust_qty) > 0 ? '+' : ''}{item.adjust_qty} + + )} + + + {isDraft ? ( + updateItem(index, 'notes', e.target.value)} + placeholder="輸入備註..." + /> + ) : ( + {item.notes || '-'} + )} + + {isDraft && ( + + + + )} +
+ )) + )} + + +
+
+
diff --git a/resources/js/Pages/Inventory/Count/Show.tsx b/resources/js/Pages/Inventory/Count/Show.tsx index f062642..05d2e21 100644 --- a/resources/js/Pages/Inventory/Count/Show.tsx +++ b/resources/js/Pages/Inventory/Count/Show.tsx @@ -10,9 +10,8 @@ import { } from '@/Components/ui/table'; import { Button } from '@/Components/ui/button'; import { Input } from '@/Components/ui/input'; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/Components/ui/card'; import { Badge } from '@/Components/ui/badge'; -import { Save, CheckCircle, Printer, Trash2, ClipboardCheck, Eye, Pencil } from 'lucide-react'; +import { Save, CheckCircle, Printer, Trash2, ClipboardCheck } from 'lucide-react'; import { AlertDialog, AlertDialogAction, @@ -25,11 +24,10 @@ import { AlertDialogTrigger, } from "@/Components/ui/alert-dialog" import { Can } from '@/Components/Permission/Can'; -import { Link } from '@inertiajs/react'; export default function Show({ doc }: any) { // Transform items to form data structure - const { data, setData, put, delete: destroy, processing } = useForm({ + const { data, setData, put, delete: destroy, processing, transform } = useForm({ items: doc.items.map((item: any) => ({ id: item.id, counted_qty: item.counted_qty, @@ -46,12 +44,11 @@ export default function Show({ doc }: any) { }; const handleSubmit = (action: string) => { - setData('action', action); - put(route('inventory.count.update', [doc.id]), { - onSuccess: () => { - // Handle success if needed - } - }); + transform((data) => ({ + ...data, + action: action, + })); + put(route('inventory.count.update', [doc.id])); }; const handleDelete = () => { @@ -75,186 +72,190 @@ export default function Show({ doc }: any) { > -
-
-
-
-
-

- - 盤點單: {doc.doc_no} -

- {doc.status === 'completed' ? ( - 已完成 - ) : ( - 盤點中 - )} +
+
+
+
+
+
+

+ + 盤點單: {doc.doc_no} +

+ {doc.status === 'completed' ? ( + 已完成 + ) : ( + 盤點中 + )} +
+

+ 倉庫: {doc.warehouse_name} | 建立人: {doc.created_by} | 快照時間: {doc.snapshot_date} +

-

- 倉庫: {doc.warehouse_name} | 建立人: {doc.created_by} | 快照時間: {doc.snapshot_date} -

+
+
+ {!isCompleted && ( +
+ + + + + + + + 確定要作廢此盤點單嗎? + + 此動作無法復原。作廢後請重新建立盤點單。 + + + + 取消 + 確認作廢 + + + + + + + +
+ )} + {isCompleted && ( + + )}
-
- {!isCompleted && ( -
- - - - - - - - 確定要作廢此盤點單嗎? - - 此動作無法復原。作廢後請重新建立盤點單。 - - - - 取消 - 確認作廢 - - - - - - -
- )} - {isCompleted && ( - - )} -
-
- -
{!isCompleted && ( - - -
- 盤點進度: {countedItems} / {totalItems} 項目 - {progress}% -
-
-
-
-
-
+
+
+ 盤點進度: {countedItems} / {totalItems} 項目 + {progress}% +
+
+
+
+
)} - - - 盤點明細 - - 請輸入每個項目的「實盤數量」。若有差異系統將自動計算。 - - - - - - # - 商品代號 / 名稱 - 批號 - 系統庫存 - 實盤數量 - 差異 - 單位 - 備註 - - - - {doc.items.map((item: any, index: number) => { - const formItem = data.items[index]; - const diff = formItem.counted_qty !== '' && formItem.counted_qty !== null - ? (parseFloat(formItem.counted_qty) - item.system_qty) - : 0; - const hasDiff = Math.abs(diff) > 0.0001; +
+
+
+

盤點明細

+

+ 請輸入每個項目的「實盤數量」。若有差異系統將自動計算。 +

+
+
+ +
+
+ + + # + 商品名稱 / 代號 + 批號 + 系統庫存 + 實盤數量 + 差異 + 單位 + 備註 + + + + {doc.items.map((item: any, index: number) => { + const formItem = data.items[index]; + const diff = formItem.counted_qty !== '' && formItem.counted_qty !== null + ? (parseFloat(formItem.counted_qty) - item.system_qty) + : 0; + const hasDiff = Math.abs(diff) > 0.0001; + + return ( + + + {index + 1} + + +
+ {item.product_name} + {item.product_code} +
+
+ {item.batch_number || '-'} + {item.system_qty.toFixed(2)} + + {isCompleted ? ( + {item.counted_qty} + ) : ( + updateItem(index, 'counted_qty', e.target.value)} + onWheel={(e: any) => e.target.blur()} + disabled={processing} + className="h-9 text-right font-medium focus:ring-primary-main" + placeholder="盤點..." + /> + )} + + + 0 + ? 'text-green-600' + : 'text-red-600' + }`}> + {formItem.counted_qty !== '' && formItem.counted_qty !== null + ? diff.toFixed(2) + : '-'} + + + {item.unit || item.unit_name} + + {isCompleted ? ( + {item.notes} + ) : ( + updateItem(index, 'notes', e.target.value)} + disabled={processing} + className="h-9 text-sm" + placeholder="備註..." + /> + )} + +
+ ); + })} +
+
+
+
- return ( - - - {index + 1} - - -
- {item.product_code} - {item.product_name} -
-
- {item.batch_number || '-'} - {item.system_qty.toFixed(2)} - - {isCompleted ? ( - {item.counted_qty} - ) : ( - updateItem(index, 'counted_qty', e.target.value)} - onWheel={(e: any) => e.target.blur()} - disabled={processing} - className="h-9 text-right font-medium focus:ring-primary-main" - placeholder="盤點..." - /> - )} - - - 0 - ? 'text-green-600' - : 'text-red-600' - }`}> - {formItem.counted_qty !== '' && formItem.counted_qty !== null - ? diff.toFixed(2) - : '-'} - - - {item.unit || item.unit_name} - - {isCompleted ? ( - {item.notes} - ) : ( - updateItem(index, 'notes', e.target.value)} - disabled={processing} - className="h-9 text-sm" - placeholder="備註..." - /> - )} - -
- ); - })} - - -
@@ -264,7 +265,7 @@ export default function Show({ doc }: any) {

操作導引

資料會自動保存盤點人與時間。若尚未盤點完,您可以點擊「暫存進度」稍後繼續。 - 確認所有項目資料正確後,請點擊「完成盤點並過帳」正式生效庫存調整。 + 確認所有項目資料正確後,請點擊「完成盤點」結束盤點作業。