[FIX] 修正採購單大單位換算問題並建立 Git 開發規範
All checks were successful
ERP-Deploy-Production / deploy-production (push) Successful in 1m19s
All checks were successful
ERP-Deploy-Production / deploy-production (push) Successful in 1m19s
This commit is contained in:
33
CLAUDE.md
Normal file
33
CLAUDE.md
Normal file
@@ -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)。
|
||||
45
GIT_WORKFLOW.md
Normal file
45
GIT_WORKFLOW.md
Normal file
@@ -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/*` 分支。
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,6 +15,7 @@ class PurchaseOrderItem extends Model
|
||||
'purchase_order_id',
|
||||
'product_id',
|
||||
'quantity',
|
||||
'unit_id',
|
||||
'unit_price',
|
||||
'subtotal',
|
||||
// 驗收欄位
|
||||
|
||||
@@ -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 */}
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
min="0"
|
||||
value={item.quantity_received}
|
||||
onChange={(e) => updateItem(index, 'quantity_received', e.target.value)}
|
||||
className={`w-full text-right ${errors && (errors as any)[errorKey] ? 'border-red-500' : ''}`}
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
min="0"
|
||||
value={item.quantity_received}
|
||||
onChange={(e) => 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 && (
|
||||
<div className="text-[10px] text-primary-main text-right font-medium">
|
||||
= {(parseFloat(item.quantity_received) * item.conversion_rate).toLocaleString()} {item.base_unit_name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(errors as any)[errorKey] && (
|
||||
<p className="text-xs text-red-500 mt-1">{(errors as any)[errorKey]}</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user