diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ab7648f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,33 @@ +# Star ERP 開發手冊 + +## 專案概述 +- 技術棧:Laravel 12, React, Inertia.js, Tailwind CSS. +- 架構:模組化單體架構 (Modular Monolith). + +## Git 分支管理規範 (嚴格遵守) +為了確保專案穩定性,請遵循以下分支流程: + +1. **Main 分支限制**: + - `main` 分支僅限 Bug 修正 (`hotfix/*`) 與版本發布。 + - **禁止直接在 `main` 開發新功能**。 +2. **開發流程**: + - **新功能**:一律在 `feature/*` 分支開發,合併至 `dev` 測試。 + - **修復 Bug (開發中)**:在 `bugfix/*` 分支開發。 + - **緊急修復 (生產環境)**:從 `main` 建立 `hotfix/*` 分支,修復後合併回 `main` 與 `dev`。 +3. **提交前標籤**: + - `[FIX]`:Bug 修正 + - `[FEAT]`:新功能 + - `[DOCS]`:文件更新 + +## 開發指令 +- 啟動環境:`./vendor/bin/sail up -d` +- 執行測試:`./vendor/bin/sail artisan test` +- 執行 Artisan 命令:`./vendor/bin/sail artisan ...` +- 前端編譯:`./vendor/bin/sail npm run dev` + +## 程式碼風格 +- PHP:遵循 PSR-12 規範。 +- React:使用 Functional Components 與 Hooks,統一使用 Lucide-react。 +- 翻譯:所有的說明、註解與 docstring 請使用**繁體中文**。 + +詳情請參閱 [GIT_WORKFLOW.md](./GIT_WORKFLOW.md)。 diff --git a/GIT_WORKFLOW.md b/GIT_WORKFLOW.md new file mode 100644 index 0000000..f3d9b5b --- /dev/null +++ b/GIT_WORKFLOW.md @@ -0,0 +1,45 @@ +# Git 分支管理與開發規範 (Star ERP) + +為了確保 `main` 分支的穩定性,本專案即日起實施以下分支管理約束。 + +## 1. 分支定義 + +| 分支類型 | 命名規範 | 描述 | 合併目標 | +| :--- | :--- | :--- | :--- | +| **Main (穩定版)** | `main` | 生產環境分支,僅存放**穩定、已測試**的代碼。 | N/A | +| **Develop (開發版)** | `dev` | 日常開發整合分支,所有功能在此測試。 | `main` (週期性) | +| **Feature (新功能)** | `feature/*` | 用於開發新功能。 | `dev` | +| **Hotfix (緊急修正)** | `hotfix/*` | 用於修復 `main` 分支的緊急 Bug。 | `main` & `dev` | +| **Bugfix (一般修復)** | `bugfix/*` | 用於修復 `dev` 分支中的 Bug。 | `dev` | + +## 2. Main 分支約束條款 + +1. **禁止直接提交**:嚴禁直接在 `main` 分支進行 `git commit`。所有變更必須透過 Pull Request (PR) 或 Merge Request。 +2. **功能凍結**:`main` 分支僅接受 `hotfix/*` 或從 `dev` 合併過來的穩定版本。**嚴禁將未經驗證的新功能 `feature/*` 直接合併至 `main`**。 +3. **強制審閱**:所有進入 `main` 的合併請求必須經過至少一人審閱 (Code Review) 且通過自動化測試 (CI)。 + +## 3. 開發流程 (Workflow) + +### 開發新功能 +1. 從 `dev` 建立 `feature/your-feature-name`。 +2. 完成開發後,發起 PR 合併至 `dev`。 +3. 在 `dev` 環境進行測試。 + +### 修復 Main 的 Bug (Hotfix) +1. 從 `main` 建立 `hotfix/bug-description`。 +2. 修復完畢後,發起 PR 合併至 `main`。 +3. **重要**:合併至 `main` 後,必須同時將該修復合併回 `dev`,以防 Bug 在下次發布時再次出現。 + +## 4. 提交訊息規範 (Commit Messages) + +請遵循以下前綴標籤: +- `[FIX]`:修復 Bug。 +- `[FEAT]`:新增功能。 +- `[DOCS]`:文件更新。 +- `[STYLE]`:調整格式(不影響邏輯)。 +- `[REFACTOR]`:重構。 + +--- +> [!IMPORTANT] +> 身為 AI 助手 (Antigravity),我在接到任務時會優先判斷任務性質。 +> 若為「新功能」且使用者要求直接在 `main` 執行,我會主動提醒並建議切換至 `feature/*` 分支。 diff --git a/app/Modules/Inventory/Controllers/GoodsReceiptController.php b/app/Modules/Inventory/Controllers/GoodsReceiptController.php index a7df0ed..11709e2 100644 --- a/app/Modules/Inventory/Controllers/GoodsReceiptController.php +++ b/app/Modules/Inventory/Controllers/GoodsReceiptController.php @@ -133,6 +133,7 @@ class GoodsReceiptController extends Controller 'id' => $po->id, 'code' => $po->code, 'status' => $po->status, + 'supplierId' => $po->vendor_id, // Alias for frontend 'vendor_id' => $po->vendor_id, 'vendor_name' => $po->vendor?->name ?? '', 'warehouse_id' => $po->warehouse_id, @@ -140,15 +141,35 @@ class GoodsReceiptController extends Controller 'items' => $po->items->map(function ($item) use ($products) { $product = $products->get($item->product_id); $remaining = max(0, $item->quantity - ($item->received_quantity ?? 0)); + + // 獲取單位名稱 + $baseUnitName = $product?->baseUnit?->name ?? '個'; + $largeUnitName = $product?->largeUnit?->name ?? ''; + + // 判斷當前採購使用的單位 (這需要從 PurchaseOrderItem 獲取 unit_id 並與產品的 large_unit_id 比較) + $selectedUnit = 'base'; + if ($item->unit_id && $product && $item->unit_id == $product->large_unit_id) { + $selectedUnit = 'large'; + } + return [ 'id' => $item->id, + 'productId' => $item->product_id, // Alias for frontend 'product_id' => $item->product_id, + 'productName' => $product?->name ?? '', // Alias for frontend 'product_name' => $product?->name ?? '', 'product_code' => $product?->code ?? '', - 'unit' => $product?->baseUnit?->name ?? '個', + 'unit' => $product?->baseUnit?->name ?? '個', // 預設顯示文字 + 'selectedUnit' => $selectedUnit, + 'base_unit_id' => $product?->base_unit_id, + 'base_unit_name' => $baseUnitName, + 'large_unit_id' => $product?->large_unit_id, + 'large_unit_name' => $largeUnitName, + 'conversion_rate' => $product?->conversion_rate ?? 1, 'quantity' => $item->quantity, 'received_quantity' => $item->received_quantity ?? 0, 'remaining' => $remaining, + 'unitPrice' => $item->unit_price, // Alias for frontend 'unit_price' => $item->unit_price, ]; })->filter(fn($item) => $item['remaining'] > 0)->values(), @@ -209,12 +230,24 @@ class GoodsReceiptController extends Controller $product = $products->get($item->product_id); $poItem = $poItems->get($item->purchase_order_item_id); + // 判斷單位 + $selectedUnit = 'base'; + if ($poItem && $product && $poItem->unit_id && $poItem->unit_id == $product->large_unit_id) { + $selectedUnit = 'large'; + } + return [ 'product_id' => $item->product_id, 'purchase_order_item_id' => $item->purchase_order_item_id, 'product_name' => $product?->name ?? '', 'product_code' => $product?->code ?? '', - 'unit' => $product?->baseUnit?->name ?? '個', + 'unit' => $poItem && $selectedUnit === 'large' ? ($product?->largeUnit?->name ?? '') : ($product?->baseUnit?->name ?? '個'), + 'selectedUnit' => $selectedUnit, + 'base_unit_id' => $product?->base_unit_id, + 'base_unit_name' => $product?->baseUnit?->name ?? '個', + 'large_unit_id' => $product?->large_unit_id, + 'large_unit_name' => $product?->largeUnit?->name ?? '', + 'conversion_rate' => $product?->conversion_rate ?? 1, 'quantity_ordered' => $poItem ? $poItem->quantity : null, 'quantity_received_so_far' => $poItem ? ($poItem->received_quantity ?? 0) : null, 'quantity_received' => (float) $item->quantity_received, @@ -237,6 +270,7 @@ class GoodsReceiptController extends Controller 'id' => $po->id, 'code' => $po->code, 'status' => $po->status, + 'supplierId' => $po->vendor_id, 'vendor_id' => $po->vendor_id, 'vendor_name' => $po->vendor?->name ?? '', 'warehouse_id' => $po->warehouse_id, @@ -244,15 +278,30 @@ class GoodsReceiptController extends Controller 'items' => $po->items->map(function ($item) use ($productsForPOs) { $product = $productsForPOs->get($item->product_id); $remaining = max(0, $item->quantity - ($item->received_quantity ?? 0)); + + $selectedUnit = 'base'; + if ($item->unit_id && $product && $item->unit_id == $product->large_unit_id) { + $selectedUnit = 'large'; + } + return [ 'id' => $item->id, + 'productId' => $item->product_id, 'product_id' => $item->product_id, + 'productName' => $product?->name ?? '', 'product_name' => $product?->name ?? '', 'product_code' => $product?->code ?? '', - 'unit' => $product?->baseUnit?->name ?? '個', + 'unit' => $item->unit_id && $product && $item->unit_id == $product->large_unit_id ? ($product?->largeUnit?->name ?? '') : ($product?->baseUnit?->name ?? '個'), + 'selectedUnit' => $selectedUnit, + 'base_unit_id' => $product?->base_unit_id, + 'base_unit_name' => $product?->baseUnit?->name ?? '個', + 'large_unit_id' => $product?->large_unit_id, + 'large_unit_name' => $product?->largeUnit?->name ?? '', + 'conversion_rate' => $product?->conversion_rate ?? 1, 'quantity' => $item->quantity, 'received_quantity' => $item->received_quantity ?? 0, 'remaining' => $remaining, + 'unitPrice' => $item->unit_price, 'unit_price' => $item->unit_price, ]; })->filter(fn($item) => $item['remaining'] > 0)->values(), diff --git a/app/Modules/Inventory/Services/GoodsReceiptService.php b/app/Modules/Inventory/Services/GoodsReceiptService.php index adf57bd..79e16a4 100644 --- a/app/Modules/Inventory/Services/GoodsReceiptService.php +++ b/app/Modules/Inventory/Services/GoodsReceiptService.php @@ -266,10 +266,24 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei default => '進貨入庫', }; + $quantityToRecord = $grItem->quantity_received; + + // 單位換算邏輯:僅針對標準採購且有連結 PO Item 時 + if ($goodsReceipt->type === 'standard' && $grItem->purchase_order_item_id) { + $poItem = \App\Modules\Procurement\Models\PurchaseOrderItem::find($grItem->purchase_order_item_id); + $product = $this->inventoryService->getProduct($grItem->product_id); + + if ($poItem && $product && $poItem->unit_id && $product->large_unit_id && $poItem->unit_id == $product->large_unit_id) { + // 如果使用的是大單位,則換算為基本單位數量 + $quantityToRecord = $grItem->quantity_received * ($product->conversion_rate ?: 1); + Log::info("Goods Receipt [{$goodsReceipt->code}] converted quantity for product [{$product->id}]: {$grItem->quantity_received} large unit -> {$quantityToRecord} base unit."); + } + } + $this->inventoryService->createInventoryRecord([ 'warehouse_id' => $goodsReceipt->warehouse_id, 'product_id' => $grItem->product_id, - 'quantity' => $grItem->quantity_received, + 'quantity' => $quantityToRecord, 'unit_cost' => $grItem->unit_price, 'batch_number' => $grItem->batch_number, 'expiry_date' => $grItem->expiry_date, @@ -282,6 +296,7 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei // 2. Update PO if linked and type is standard if ($goodsReceipt->type === 'standard' && $goodsReceipt->purchase_order_id && $grItem->purchase_order_item_id) { + // 更新採購單的實收數量 (維持原始單位數量,以便與採購數量比較) $this->procurementService->updateReceivedQuantity( $grItem->purchase_order_item_id, $grItem->quantity_received diff --git a/app/Modules/Procurement/Models/PurchaseOrderItem.php b/app/Modules/Procurement/Models/PurchaseOrderItem.php index d8ab795..dfccdba 100644 --- a/app/Modules/Procurement/Models/PurchaseOrderItem.php +++ b/app/Modules/Procurement/Models/PurchaseOrderItem.php @@ -15,6 +15,7 @@ class PurchaseOrderItem extends Model 'purchase_order_id', 'product_id', 'quantity', + 'unit_id', 'unit_price', 'subtotal', // 驗收欄位 diff --git a/resources/js/Pages/Inventory/GoodsReceipt/Create.tsx b/resources/js/Pages/Inventory/GoodsReceipt/Create.tsx index 4df37bf..309c0a6 100644 --- a/resources/js/Pages/Inventory/GoodsReceipt/Create.tsx +++ b/resources/js/Pages/Inventory/GoodsReceipt/Create.tsx @@ -162,19 +162,25 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders, }, [isNonStandard, selectedVendor]); // 選擇採購單 - const handleSelectPO = (po: PendingPO) => { + const handleSelectPO = (po: any) => { setSelectedPO(po); // 將採購單項目轉換為進貨單項目,預填剩餘可收貨量 - const pendingItems = po.items.map((item) => ({ - product_id: item.product_id, + const pendingItems = po.items.map((item: any) => ({ + product_id: item.productId, purchase_order_item_id: item.id, - product_name: item.product_name, - product_code: item.product_code, - unit: item.unit, + product_name: item.productName, + product_code: item.product_code || '', + unit: item.selectedUnit === 'large' ? item.large_unit_name : item.base_unit_name, + selectedUnit: item.selectedUnit, + base_unit_id: item.base_unit_id, + base_unit_name: item.base_unit_name, + large_unit_id: item.large_unit_id, + large_unit_name: item.large_unit_name, + conversion_rate: item.conversion_rate, quantity_ordered: item.quantity, - quantity_received_so_far: item.received_quantity, - quantity_received: item.remaining, // 預填剩餘量 - unit_price: item.unit_price, + quantity_received_so_far: item.received_quantity || 0, + quantity_received: (item.quantity - (item.received_quantity || 0)).toString(), // 預填剩餘量 + unit_price: item.unitPrice, batch_number: '', batchMode: 'new', originCountry: 'TW', @@ -184,7 +190,7 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders, setData((prev) => ({ ...prev, purchase_order_id: po.id.toString(), - vendor_id: po.vendor_id.toString(), + vendor_id: po.supplierId.toString(), warehouse_id: po.warehouse_id ? po.warehouse_id.toString() : prev.warehouse_id, items: pendingItems, })); @@ -777,14 +783,21 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders, {/* Received Quantity */} - updateItem(index, 'quantity_received', e.target.value)} - className={`w-full text-right ${errors && (errors as any)[errorKey] ? 'border-red-500' : ''}`} - /> +
+ updateItem(index, 'quantity_received', e.target.value)} + className={`w-full text-right ${errors && (errors as any)[errorKey] ? 'border-red-500' : ''}`} + /> + {item.selectedUnit === 'large' && item.conversion_rate > 1 && ( +
+ = {(parseFloat(item.quantity_received) * item.conversion_rate).toLocaleString()} {item.base_unit_name} +
+ )} +
{(errors as any)[errorKey] && (

{(errors as any)[errorKey]}

)}