Compare commits

..

4 Commits

Author SHA1 Message Date
dd2e63c08b [DOCS] 移除冗餘規範文件並同步開發規則至所有分支
All checks were successful
ERP-Deploy-Production / deploy-production (push) Successful in 1m1s
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m22s
2026-03-05 08:53:41 +08:00
b498fe93ff [DOCS] 更新 Git 規範:加入新功能合併至 main 的時段限制 2026-03-05 08:51:43 +08:00
6b324b4bd0 [DOCS] 將 Git 規範整合至開發 Rules 並移除重複文件 2026-03-05 08:49:05 +08:00
a898873211 [FIX] 修正採購單大單位換算問題並建立 Git 開發規範
All checks were successful
ERP-Deploy-Production / deploy-production (push) Successful in 1m19s
2026-03-05 08:46:26 +08:00
5 changed files with 150 additions and 22 deletions

View File

@@ -0,0 +1,50 @@
---
name: Git 分支管理與開發規範
description: 強制執行 main 分支保護與開發分支流程,確保主分支僅用於 Bug 修正與版本釋放。
---
# Git 分支管理與開發規範 (Git Workflow)
為了確保 `main` 分支的穩定性,所有開發者與 AI 助手必須嚴格遵守以下分支管理與合併規範。
## 1. 分支架構與用途
| 分支類型 | 命名規範 | 描述 | 合併目標 |
| :--- | :--- | :--- | :--- |
| **Main (穩定版)** | `main` | 生產環境分支,僅存放穩定、已測試的代碼。**禁止直接開發新功能**。 | N/A |
| **Develop (開發版)** | `dev` | 日常開發整合分支,所有變更在此測試。 | `main` |
| **Feature (新功能)** | `feature/*` | 用於開發新功能。 | `dev` |
| **Hotfix (緊急修正)** | `hotfix/*` | 用於修復 `main` 分支的緊急 Bug。 | `main` & `dev` |
| **Bugfix (修復)** | `bugfix/*` | 用於修復 `dev` 分支中的 Bug。 | `dev` |
## 2. Main 分支約束條款 (Mandatory)
1. **禁止隨意上功能**`main` 分支僅接受從 `dev` 合併過來的穩定版本,或用於修復生產環境 Bug 的 `hotfix/*` 分支。
2. **新功能合併時段限制**
- **允許時段**週一至週四12:00中午之前。
- **非允許時段**:若在上述時段以外(如週五、週末或下班時間)欲合併新功能至 `main`**AI 助手必須主動提醒風險**,並取得使用者明確同意後方可執行。
3. **新功能隔離**:新功能開發必須在單獨的 `feature/*` 分支進行,並先合併至 `dev` 驗證。
4. **禁止直接 Commit**:嚴禁直接在 `main` 進行提交,必須透過合併流程並確保已測試。
## 3. 開發流程 (Standard Operating Procedure)
### 開發新功能
1. 從 `dev` 建立 `feature/功能名稱`
2. 開發完成後合併至 `dev`
### 修復 Main Bug (Hotfix)
1. 從 `main` 建立 `hotfix/Bug描述`
2. 修復後合併回 `main`,並**務必**同步合併至 `dev`
## 4. 提交訊息規範 (Commit Messages)
提交訊息必須包含以下前綴:
- `[FIX]`:修復 Bug。
- `[FEAT]`:新增功能。
- `[DOCS]`:文件更新。
- `[STYLE]`UI/CSS/格式調整。
- `[REFACTOR]`:程式碼重構。
---
> [!IMPORTANT]
> 身為 AI 助手 (Antigravity),我在接收到任務時會優先判斷其性質。若為「新功能」且操作分支為 `main`,應主動提醒並引導切換至正確的分支開發。

View File

@@ -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(),

View File

@@ -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

View File

@@ -15,6 +15,7 @@ class PurchaseOrderItem extends Model
'purchase_order_id',
'product_id',
'quantity',
'unit_id',
'unit_price',
'subtotal',
// 驗收欄位

View File

@@ -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>
)}