feat: API調整訂單與販賣機訂單同步強制使用warehouse_code,更新API對接文件,及優化生產與配方模組UI顯示
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s
This commit is contained in:
@@ -80,6 +80,12 @@ description: 確保 Star ERP 客戶端(租戶端)後台所有頁面的 UI
|
|||||||
- **麵包屑**:使用 `BreadcrumbItemType`(屬性為 `label`, `href`, `isPage`),不需要包含「首頁」
|
- **麵包屑**:使用 `BreadcrumbItemType`(屬性為 `label`, `href`, `isPage`),不需要包含「首頁」
|
||||||
- **Input 元件**:不額外設定 focus 樣式,直接使用 `@/Components/ui/input` 的內建樣式
|
- **Input 元件**:不額外設定 focus 樣式,直接使用 `@/Components/ui/input` 的內建樣式
|
||||||
- **日期顯示**:使用 `resources/js/lib/date.ts` 的 `formatDate` 工具
|
- **日期顯示**:使用 `resources/js/lib/date.ts` 的 `formatDate` 工具
|
||||||
|
- **數字顯示**:
|
||||||
|
- 所有的金額、數量等數值,應視情境使用千分位格式化(如 `1,234.56`)。
|
||||||
|
- **精確顯示**:數值應按實際數值顯示,避免在小數點後出現多餘的零(例如:應顯示 `10.5` 而非 `10.500`)。
|
||||||
|
- 數值應與單位(如 `元`、`kg`)保持適當間距或清晰呈現。
|
||||||
|
- 負數應視情境明確標示(如使用紅色 `text-other-error` 或負號)。
|
||||||
|
- 列表中的數值建議靠右對齊 (text-right),以便於視覺比較。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -29,3 +29,7 @@ Thumbs.db
|
|||||||
/docs/presentation
|
/docs/presentation
|
||||||
docs/Monthly_Report_2026_01.pptx
|
docs/Monthly_Report_2026_01.pptx
|
||||||
docs/f6_1770350984272.xlsx
|
docs/f6_1770350984272.xlsx
|
||||||
|
公共事業費-描述.md
|
||||||
|
.gitignore
|
||||||
|
BOM表自動計算成本.md
|
||||||
|
公共事業費-類別維護.md
|
||||||
|
|||||||
@@ -93,14 +93,16 @@ class SyncOrderAction
|
|||||||
'source_label' => $data['source_label'] ?? null,
|
'source_label' => $data['source_label'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2. 查找或建立倉庫
|
// 2. 查找倉庫
|
||||||
$warehouseId = $data['warehouse_id'] ?? null;
|
$warehouseCode = $data['warehouse_code'];
|
||||||
|
$warehouses = $this->inventoryService->getWarehousesByCodes([$warehouseCode]);
|
||||||
if (empty($warehouseId)) {
|
|
||||||
$warehouseName = $data['warehouse'] ?? '銷售倉庫';
|
if ($warehouses->isEmpty()) {
|
||||||
$warehouse = $this->inventoryService->findOrCreateWarehouseByName($warehouseName);
|
throw ValidationException::withMessages([
|
||||||
$warehouseId = $warehouse->id;
|
'warehouse_code' => ["Warehouse with code {$warehouseCode} not found."]
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
$warehouseId = $warehouses->first()->id;
|
||||||
|
|
||||||
$totalAmount = 0;
|
$totalAmount = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -90,14 +90,16 @@ class SyncVendingOrderAction
|
|||||||
'source_label' => $data['machine_id'] ?? null,
|
'source_label' => $data['machine_id'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2. 查找或建立倉庫
|
// 2. 查找倉庫
|
||||||
$warehouseId = $data['warehouse_id'] ?? null;
|
$warehouseCode = $data['warehouse_code'];
|
||||||
|
$warehouses = $this->inventoryService->getWarehousesByCodes([$warehouseCode]);
|
||||||
if (empty($warehouseId)) {
|
|
||||||
$warehouseName = $data['warehouse'] ?? '販賣機倉庫';
|
if ($warehouses->isEmpty()) {
|
||||||
$warehouse = $this->inventoryService->findOrCreateWarehouseByName($warehouseName);
|
throw ValidationException::withMessages([
|
||||||
$warehouseId = $warehouse->id;
|
'warehouse_code' => ["Warehouse with code {$warehouseCode} not found."]
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
$warehouseId = $warehouses->first()->id;
|
||||||
|
|
||||||
$totalAmount = 0;
|
$totalAmount = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ class SyncOrderRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'external_order_id' => 'required|string',
|
'external_order_id' => 'required|string',
|
||||||
'warehouse' => 'nullable|string',
|
'warehouse_code' => 'required|string',
|
||||||
'warehouse_id' => 'nullable|integer',
|
|
||||||
'payment_method' => 'nullable|string|in:cash,credit_card,line_pay,ecpay,transfer,other',
|
'payment_method' => 'nullable|string|in:cash,credit_card,line_pay,ecpay,transfer,other',
|
||||||
'sold_at' => 'nullable|date',
|
'sold_at' => 'nullable|date',
|
||||||
'items' => 'required|array|min:1',
|
'items' => 'required|array|min:1',
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ class SyncVendingOrderRequest extends FormRequest
|
|||||||
return [
|
return [
|
||||||
'external_order_id' => 'required|string',
|
'external_order_id' => 'required|string',
|
||||||
'machine_id' => 'nullable|string',
|
'machine_id' => 'nullable|string',
|
||||||
'warehouse' => 'nullable|string',
|
'warehouse_code' => 'required|string',
|
||||||
'warehouse_id' => 'nullable|integer',
|
|
||||||
'payment_method' => 'nullable|string|in:cash,electronic,line_pay,other',
|
'payment_method' => 'nullable|string|in:cash,electronic,line_pay,other',
|
||||||
'sold_at' => 'nullable|date',
|
'sold_at' => 'nullable|date',
|
||||||
'items' => 'required|array|min:1',
|
'items' => 'required|array|min:1',
|
||||||
|
|||||||
@@ -67,21 +67,46 @@ class ProductionOrderController extends Controller
|
|||||||
if (!in_array((int)$perPage, [10, 20, 50, 100])) {
|
if (!in_array((int)$perPage, [10, 20, 50, 100])) {
|
||||||
$perPage = $defaultPerPage;
|
$perPage = $defaultPerPage;
|
||||||
}
|
}
|
||||||
$productionOrders = $query->paginate($perPage)->withQueryString();
|
$productionOrders = $query->with('items')->paginate($perPage)->withQueryString();
|
||||||
|
|
||||||
// --- 手動資料水和 (Manual Hydration) ---
|
// --- 手動資料水和 (Manual Hydration) ---
|
||||||
$productIds = $productionOrders->pluck('product_id')->unique()->filter()->toArray();
|
$productIds = collect();
|
||||||
$warehouseIds = $productionOrders->pluck('warehouse_id')->unique()->filter()->toArray();
|
$warehouseIds = collect();
|
||||||
$userIds = $productionOrders->pluck('user_id')->unique()->filter()->toArray();
|
$userIds = collect();
|
||||||
|
$inventoryIds = collect();
|
||||||
|
|
||||||
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
foreach ($productionOrders as $order) {
|
||||||
$warehouses = $this->inventoryService->getAllWarehouses()->whereIn('id', $warehouseIds)->keyBy('id');
|
$productIds->push($order->product_id);
|
||||||
$users = $this->coreService->getUsersByIds($userIds)->keyBy('id');
|
$warehouseIds->push($order->warehouse_id);
|
||||||
|
$userIds->push($order->user_id);
|
||||||
|
if ($order->items) {
|
||||||
|
$inventoryIds = $inventoryIds->merge($order->items->pluck('inventory_id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$productionOrders->getCollection()->transform(function ($order) use ($products, $warehouses, $users) {
|
$products = $this->inventoryService->getProductsByIds($productIds->unique()->filter()->toArray())->keyBy('id');
|
||||||
|
$warehouses = $this->inventoryService->getAllWarehouses()->whereIn('id', $warehouseIds->unique()->filter()->toArray())->keyBy('id');
|
||||||
|
$users = $this->coreService->getUsersByIds($userIds->unique()->filter()->toArray())->keyBy('id');
|
||||||
|
$inventories = $this->inventoryService->getInventoriesByIds($inventoryIds->unique()->filter()->toArray())->keyBy('id');
|
||||||
|
|
||||||
|
$productionOrders->getCollection()->transform(function ($order) use ($products, $warehouses, $users, $inventories) {
|
||||||
$order->product = $products->get($order->product_id);
|
$order->product = $products->get($order->product_id);
|
||||||
$order->warehouse = $warehouses->get($order->warehouse_id);
|
$order->warehouse = $warehouses->get($order->warehouse_id);
|
||||||
$order->user = $users->get($order->user_id);
|
$order->user = $users->get($order->user_id);
|
||||||
|
|
||||||
|
$totalCost = 0;
|
||||||
|
if ($order->items) {
|
||||||
|
foreach ($order->items as $item) {
|
||||||
|
$inventory = $inventories->get($item->inventory_id);
|
||||||
|
if ($inventory) {
|
||||||
|
$totalCost += $item->quantity_used * ($inventory->unit_cost ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$order->estimated_total_cost = $totalCost;
|
||||||
|
$order->estimated_unit_cost = $order->output_quantity > 0 ? $totalCost / $order->output_quantity : 0;
|
||||||
|
unset($order->items);
|
||||||
|
|
||||||
return $order;
|
return $order;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,7 +256,9 @@ class ProductionOrderController extends Controller
|
|||||||
'unit_name' => $inv->product->baseUnit->name ?? '',
|
'unit_name' => $inv->product->baseUnit->name ?? '',
|
||||||
'base_unit_id' => $inv->product->base_unit_id ?? null,
|
'base_unit_id' => $inv->product->base_unit_id ?? null,
|
||||||
'large_unit_id' => $inv->product->large_unit_id ?? null,
|
'large_unit_id' => $inv->product->large_unit_id ?? null,
|
||||||
|
'purchase_unit_id' => $inv->product->purchase_unit_id ?? null,
|
||||||
'conversion_rate' => $inv->product->conversion_rate ?? 1,
|
'conversion_rate' => $inv->product->conversion_rate ?? 1,
|
||||||
|
'unit_cost' => (float) $inv->unit_cost,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -258,7 +285,10 @@ class ProductionOrderController extends Controller
|
|||||||
'expiry_date' => $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null,
|
'expiry_date' => $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null,
|
||||||
'unit_name' => $inv->product->baseUnit->name ?? '',
|
'unit_name' => $inv->product->baseUnit->name ?? '',
|
||||||
'base_unit_id' => $inv->product->base_unit_id ?? null,
|
'base_unit_id' => $inv->product->base_unit_id ?? null,
|
||||||
|
'large_unit_id' => $inv->product->large_unit_id ?? null,
|
||||||
|
'purchase_unit_id' => $inv->product->purchase_unit_id ?? null,
|
||||||
'conversion_rate' => $inv->product->conversion_rate ?? 1,
|
'conversion_rate' => $inv->product->conversion_rate ?? 1,
|
||||||
|
'unit_cost' => (float) $inv->unit_cost,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -46,14 +46,51 @@ class RecipeController extends Controller
|
|||||||
$perPage = $defaultPerPage;
|
$perPage = $defaultPerPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
$recipes = $query->paginate($perPage)->withQueryString();
|
$recipes = $query->with('items')->paginate($perPage)->withQueryString();
|
||||||
|
|
||||||
// Manual Hydration
|
// Manual Hydration
|
||||||
$productIds = $recipes->pluck('product_id')->unique()->filter()->toArray();
|
$productIds = collect();
|
||||||
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
$itemProductIds = collect();
|
||||||
|
|
||||||
|
foreach ($recipes as $recipe) {
|
||||||
|
$productIds->push($recipe->product_id);
|
||||||
|
if ($recipe->items) {
|
||||||
|
$itemProductIds = $itemProductIds->merge($recipe->items->pluck('product_id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$allProductIds = array_unique(array_merge(
|
||||||
|
$productIds->unique()->filter()->toArray(),
|
||||||
|
$itemProductIds->unique()->filter()->toArray()
|
||||||
|
));
|
||||||
|
|
||||||
|
$products = $this->inventoryService->getProductsByIds($allProductIds)->keyBy('id');
|
||||||
|
|
||||||
$recipes->getCollection()->transform(function ($recipe) use ($products) {
|
$recipes->getCollection()->transform(function ($recipe) use ($products) {
|
||||||
$recipe->product = $products->get($recipe->product_id);
|
$recipe->product = $products->get($recipe->product_id);
|
||||||
|
|
||||||
|
$totalCost = 0;
|
||||||
|
if ($recipe->items) {
|
||||||
|
foreach ($recipe->items as $item) {
|
||||||
|
$itemProduct = $products->get($item->product_id);
|
||||||
|
if ($itemProduct) {
|
||||||
|
$baseCost = $itemProduct->cost_price ?? 0;
|
||||||
|
$conversionRate = 1;
|
||||||
|
|
||||||
|
if ($item->unit_id == $itemProduct->large_unit_id && !is_null($itemProduct->conversion_rate)) {
|
||||||
|
$conversionRate = $itemProduct->conversion_rate;
|
||||||
|
} elseif ($item->unit_id == $itemProduct->purchase_unit_id && !is_null($itemProduct->conversion_rate_purchase)) {
|
||||||
|
$conversionRate = $itemProduct->conversion_rate_purchase;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalCost += ($item->quantity * $baseCost * $conversionRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$recipe->estimated_total_cost = $totalCost;
|
||||||
|
$recipe->estimated_unit_cost = $recipe->yield_quantity > 0 ? $totalCost / $recipe->yield_quantity : 0;
|
||||||
|
unset($recipe->items);
|
||||||
|
|
||||||
return $recipe;
|
return $recipe;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function SearchableSelect({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
disabled={option.disabled}
|
disabled={option.disabled}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer group"
|
||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -137,7 +137,7 @@ export function SearchableSelect({
|
|||||||
<div className="flex items-center justify-between flex-1">
|
<div className="flex items-center justify-between flex-1">
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
{option.sublabel && (
|
{option.sublabel && (
|
||||||
<span className="text-xs text-muted-foreground ml-2">
|
<span className="text-xs text-muted-foreground ml-2 group-data-[selected=true]:text-white">
|
||||||
{option.sublabel}
|
{option.sublabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ interface InventoryOption {
|
|||||||
base_unit_name?: string;
|
base_unit_name?: string;
|
||||||
large_unit_id?: number;
|
large_unit_id?: number;
|
||||||
large_unit_name?: string;
|
large_unit_name?: string;
|
||||||
|
purchase_unit_id?: number;
|
||||||
conversion_rate?: number;
|
conversion_rate?: number;
|
||||||
|
unit_cost?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BomItem {
|
interface BomItem {
|
||||||
@@ -73,6 +75,8 @@ interface BomItem {
|
|||||||
ui_large_unit_name?: string;
|
ui_large_unit_name?: string;
|
||||||
ui_base_unit_id?: number;
|
ui_base_unit_id?: number;
|
||||||
ui_large_unit_id?: number;
|
ui_large_unit_id?: number;
|
||||||
|
ui_purchase_unit_id?: number;
|
||||||
|
ui_unit_cost?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -203,7 +207,10 @@ export default function Create({ products, warehouses }: Props) {
|
|||||||
// 單位與轉換率
|
// 單位與轉換率
|
||||||
item.ui_base_unit_name = inv.unit_name || '';
|
item.ui_base_unit_name = inv.unit_name || '';
|
||||||
item.ui_base_unit_id = inv.base_unit_id;
|
item.ui_base_unit_id = inv.base_unit_id;
|
||||||
|
item.ui_large_unit_id = inv.large_unit_id;
|
||||||
|
item.ui_purchase_unit_id = inv.purchase_unit_id;
|
||||||
item.ui_conversion_rate = inv.conversion_rate || 1;
|
item.ui_conversion_rate = inv.conversion_rate || 1;
|
||||||
|
item.ui_unit_cost = inv.unit_cost || 0;
|
||||||
|
|
||||||
// 預設單位
|
// 預設單位
|
||||||
item.ui_selected_unit = 'base';
|
item.ui_selected_unit = 'base';
|
||||||
@@ -249,7 +256,7 @@ export default function Create({ products, warehouses }: Props) {
|
|||||||
|
|
||||||
const yieldQty = parseFloat(recipe.yield_quantity || "0") || 1;
|
const yieldQty = parseFloat(recipe.yield_quantity || "0") || 1;
|
||||||
// 自動帶入配方標準產量
|
// 自動帶入配方標準產量
|
||||||
setData('output_quantity', String(yieldQty));
|
setData('output_quantity', formatQuantity(yieldQty));
|
||||||
|
|
||||||
const newBomItems: BomItem[] = recipe.items.map((item: any) => {
|
const newBomItems: BomItem[] = recipe.items.map((item: any) => {
|
||||||
const baseQty = parseFloat(item.quantity || "0");
|
const baseQty = parseFloat(item.quantity || "0");
|
||||||
@@ -269,7 +276,7 @@ export default function Create({ products, warehouses }: Props) {
|
|||||||
ui_product_name: item.product_name,
|
ui_product_name: item.product_name,
|
||||||
ui_batch_number: "",
|
ui_batch_number: "",
|
||||||
ui_available_qty: 0,
|
ui_available_qty: 0,
|
||||||
ui_input_quantity: String(calculatedQty),
|
ui_input_quantity: formatQuantity(calculatedQty),
|
||||||
ui_selected_unit: 'base',
|
ui_selected_unit: 'base',
|
||||||
ui_base_unit_name: item.unit_name,
|
ui_base_unit_name: item.unit_name,
|
||||||
ui_base_unit_id: item.unit_id,
|
ui_base_unit_id: item.unit_id,
|
||||||
@@ -279,7 +286,7 @@ export default function Create({ products, warehouses }: Props) {
|
|||||||
setBomItems(newBomItems);
|
setBomItems(newBomItems);
|
||||||
|
|
||||||
toast.success(`已自動載入配方: ${recipe.name}`, {
|
toast.success(`已自動載入配方: ${recipe.name}`, {
|
||||||
description: `標準產量: ${yieldQty} 份`
|
description: `標準產量: ${formatQuantity(yieldQty)} 份`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -380,6 +387,24 @@ export default function Create({ products, warehouses }: Props) {
|
|||||||
submit('completed');
|
submit('completed');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBomItemUnitCost = (item: BomItem) => {
|
||||||
|
if (!item.ui_unit_cost) return 0;
|
||||||
|
let cost = Number(item.ui_unit_cost);
|
||||||
|
|
||||||
|
// Check if selected unit is large_unit or purchase_unit
|
||||||
|
if (item.unit_id && (String(item.unit_id) === String(item.ui_large_unit_id) || String(item.unit_id) === String(item.ui_purchase_unit_id))) {
|
||||||
|
cost = cost * Number(item.ui_conversion_rate || 1);
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalEstimatedCost = bomItems.reduce((sum, item) => {
|
||||||
|
if (!item.ui_input_quantity || !item.ui_unit_cost) return sum;
|
||||||
|
const inputQty = parseFloat(item.ui_input_quantity || '0');
|
||||||
|
const unitCost = getBomItemUnitCost(item);
|
||||||
|
return sum + (unitCost * inputQty);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("productionOrdersCreate")}>
|
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("productionOrdersCreate")}>
|
||||||
<Head title="建立生產單" />
|
<Head title="建立生產單" />
|
||||||
@@ -529,10 +554,12 @@ export default function Create({ products, warehouses }: Props) {
|
|||||||
<TableRow className="bg-gray-50/50">
|
<TableRow className="bg-gray-50/50">
|
||||||
<TableHead className="w-[18%]">商品 <span className="text-red-500">*</span></TableHead>
|
<TableHead className="w-[18%]">商品 <span className="text-red-500">*</span></TableHead>
|
||||||
<TableHead className="w-[15%]">來源倉庫 <span className="text-red-500">*</span></TableHead>
|
<TableHead className="w-[15%]">來源倉庫 <span className="text-red-500">*</span></TableHead>
|
||||||
<TableHead className="w-[30%]">批號 <span className="text-red-500">*</span></TableHead>
|
<TableHead className="w-[20%]">批號 <span className="text-red-500">*</span></TableHead>
|
||||||
<TableHead className="w-[12%]">數量 <span className="text-red-500">*</span></TableHead>
|
<TableHead className="w-[12%]">數量 <span className="text-red-500">*</span></TableHead>
|
||||||
<TableHead className="w-[10%]">單位</TableHead>
|
<TableHead className="w-[10%]">單位</TableHead>
|
||||||
<TableHead className="w-[10%]"></TableHead>
|
<TableHead className="w-[10%] text-right">預估單價</TableHead>
|
||||||
|
<TableHead className="w-[10%] text-right">成本小計</TableHead>
|
||||||
|
<TableHead className="w-[5%]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -560,7 +587,7 @@ export default function Create({ products, warehouses }: Props) {
|
|||||||
.map((inv: InventoryOption) => ({
|
.map((inv: InventoryOption) => ({
|
||||||
label: inv.batch_number,
|
label: inv.batch_number,
|
||||||
value: String(inv.id),
|
value: String(inv.id),
|
||||||
sublabel: `(存:${inv.quantity} | 效:${inv.expiry_date || '無'})`
|
sublabel: `(存:${formatQuantity(inv.quantity)} | 效:${inv.expiry_date || '無'})`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -638,6 +665,19 @@ export default function Create({ products, warehouses }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
{/* 5. 預估單價 */}
|
||||||
|
<TableCell className="align-top text-right">
|
||||||
|
<div className="h-9 flex items-center justify-end px-1 text-sm text-gray-600 font-medium">
|
||||||
|
{getBomItemUnitCost(item).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* 6. 成本小計 */}
|
||||||
|
<TableCell className="align-top text-right">
|
||||||
|
<div className="h-9 flex items-center justify-end px-1 text-sm font-bold text-gray-900">
|
||||||
|
{(getBomItemUnitCost(item) * parseFloat(item.ui_input_quantity || '0')).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="align-top">
|
<TableCell className="align-top">
|
||||||
<Button
|
<Button
|
||||||
@@ -660,9 +700,27 @@ export default function Create({ products, warehouses }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{errors.items && <p className="text-red-500 text-sm mt-2">{errors.items}</p>}
|
{errors.items && <p className="text-red-500 text-sm mt-2">{errors.items}</p>}
|
||||||
|
|
||||||
|
{/* 生產單預估總成本區塊 */}
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200 min-w-[300px]">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm text-gray-600">生產單總預估成本</span>
|
||||||
|
<span className="text-lg font-bold text-gray-900">{totalEstimatedCost.toLocaleString(undefined, { maximumFractionDigits: 2 })} 元</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t border-gray-200">
|
||||||
|
<span className="text-sm font-medium text-gray-700">預估單位生產成本
|
||||||
|
<span className="text-xs text-gray-500 ml-1">(共 {Number(data.output_quantity || 0).toLocaleString(undefined, { maximumFractionDigits: 4 })} 份)</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-md font-bold text-primary-main">
|
||||||
|
{(parseFloat(data.output_quantity) > 0 ? totalEstimatedCost / parseFloat(data.output_quantity) : 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</AuthenticatedLayout>
|
</AuthenticatedLayout >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ interface InventoryOption {
|
|||||||
base_unit_name?: string;
|
base_unit_name?: string;
|
||||||
large_unit_id?: number;
|
large_unit_id?: number;
|
||||||
large_unit_name?: string;
|
large_unit_name?: string;
|
||||||
|
purchase_unit_id?: number;
|
||||||
conversion_rate?: number;
|
conversion_rate?: number;
|
||||||
|
unit_cost?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BomItem {
|
interface BomItem {
|
||||||
@@ -76,7 +78,9 @@ interface BomItem {
|
|||||||
ui_large_unit_name?: string;
|
ui_large_unit_name?: string;
|
||||||
ui_base_unit_id?: number;
|
ui_base_unit_id?: number;
|
||||||
ui_large_unit_id?: number;
|
ui_large_unit_id?: number;
|
||||||
|
ui_purchase_unit_id?: number;
|
||||||
ui_product_code?: string;
|
ui_product_code?: string;
|
||||||
|
ui_unit_cost?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductionOrderItem {
|
interface ProductionOrderItem {
|
||||||
@@ -165,6 +169,7 @@ export default function ProductionEdit({ productionOrder, products, warehouses }
|
|||||||
ui_batch_number: item.inventory?.batch_number,
|
ui_batch_number: item.inventory?.batch_number,
|
||||||
ui_available_qty: item.inventory?.quantity,
|
ui_available_qty: item.inventory?.quantity,
|
||||||
ui_expiry_date: item.inventory?.expiry_date,
|
ui_expiry_date: item.inventory?.expiry_date,
|
||||||
|
ui_unit_cost: 0,
|
||||||
}));
|
}));
|
||||||
const [bomItems, setBomItems] = useState<BomItem[]>(initialBomItems);
|
const [bomItems, setBomItems] = useState<BomItem[]>(initialBomItems);
|
||||||
|
|
||||||
@@ -203,7 +208,9 @@ export default function ProductionEdit({ productionOrder, products, warehouses }
|
|||||||
ui_large_unit_name: inv.large_unit_name || '',
|
ui_large_unit_name: inv.large_unit_name || '',
|
||||||
ui_base_unit_id: inv.base_unit_id,
|
ui_base_unit_id: inv.base_unit_id,
|
||||||
ui_large_unit_id: inv.large_unit_id,
|
ui_large_unit_id: inv.large_unit_id,
|
||||||
|
ui_purchase_unit_id: inv.purchase_unit_id,
|
||||||
ui_conversion_rate: inv.conversion_rate || 1,
|
ui_conversion_rate: inv.conversion_rate || 1,
|
||||||
|
ui_unit_cost: inv.unit_cost || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,7 +284,9 @@ export default function ProductionEdit({ productionOrder, products, warehouses }
|
|||||||
item.ui_large_unit_name = inv.large_unit_name || '';
|
item.ui_large_unit_name = inv.large_unit_name || '';
|
||||||
item.ui_base_unit_id = inv.base_unit_id;
|
item.ui_base_unit_id = inv.base_unit_id;
|
||||||
item.ui_large_unit_id = inv.large_unit_id;
|
item.ui_large_unit_id = inv.large_unit_id;
|
||||||
|
item.ui_purchase_unit_id = inv.purchase_unit_id;
|
||||||
item.ui_conversion_rate = inv.conversion_rate || 1;
|
item.ui_conversion_rate = inv.conversion_rate || 1;
|
||||||
|
item.ui_unit_cost = inv.unit_cost || 0;
|
||||||
|
|
||||||
item.ui_selected_unit = 'base';
|
item.ui_selected_unit = 'base';
|
||||||
item.unit_id = String(inv.base_unit_id || '');
|
item.unit_id = String(inv.base_unit_id || '');
|
||||||
@@ -365,6 +374,24 @@ export default function ProductionEdit({ productionOrder, products, warehouses }
|
|||||||
submit('draft');
|
submit('draft');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBomItemUnitCost = (item: BomItem) => {
|
||||||
|
if (!item.ui_unit_cost) return 0;
|
||||||
|
let cost = Number(item.ui_unit_cost);
|
||||||
|
|
||||||
|
// Check if selected unit is large_unit or purchase_unit
|
||||||
|
if (item.unit_id && (String(item.unit_id) === String(item.ui_large_unit_id) || String(item.unit_id) === String(item.ui_purchase_unit_id))) {
|
||||||
|
cost = cost * Number(item.ui_conversion_rate || 1);
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalEstimatedCost = bomItems.reduce((sum, item) => {
|
||||||
|
if (!item.ui_input_quantity || !item.ui_unit_cost) return sum;
|
||||||
|
const inputQty = parseFloat(item.ui_input_quantity || '0');
|
||||||
|
const unitCost = getBomItemUnitCost(item);
|
||||||
|
return sum + (unitCost * inputQty);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("productionOrdersCreate")}>
|
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("productionOrdersCreate")}>
|
||||||
<Head title={`編輯生產單 - ${productionOrder.code}`} />
|
<Head title={`編輯生產單 - ${productionOrder.code}`} />
|
||||||
@@ -492,10 +519,12 @@ export default function ProductionEdit({ productionOrder, products, warehouses }
|
|||||||
<TableRow className="bg-gray-50/50">
|
<TableRow className="bg-gray-50/50">
|
||||||
<TableHead className="w-[18%]">商品 <span className="text-red-500">*</span></TableHead>
|
<TableHead className="w-[18%]">商品 <span className="text-red-500">*</span></TableHead>
|
||||||
<TableHead className="w-[15%]">來源倉庫 <span className="text-red-500">*</span></TableHead>
|
<TableHead className="w-[15%]">來源倉庫 <span className="text-red-500">*</span></TableHead>
|
||||||
<TableHead className="w-[30%]">批號 <span className="text-red-500">*</span></TableHead>
|
<TableHead className="w-[20%]">批號 <span className="text-red-500">*</span></TableHead>
|
||||||
<TableHead className="w-[15%]">數量 <span className="text-red-500">*</span></TableHead>
|
<TableHead className="w-[12%]">數量 <span className="text-red-500">*</span></TableHead>
|
||||||
<TableHead className="w-[12%]">單位</TableHead>
|
<TableHead className="w-[10%]">單位</TableHead>
|
||||||
<TableHead className="w-[10%]"></TableHead>
|
<TableHead className="w-[10%] text-right">預估單價</TableHead>
|
||||||
|
<TableHead className="w-[10%] text-right">成本小計</TableHead>
|
||||||
|
<TableHead className="w-[5%]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -523,7 +552,7 @@ export default function ProductionEdit({ productionOrder, products, warehouses }
|
|||||||
.map((inv: InventoryOption) => ({
|
.map((inv: InventoryOption) => ({
|
||||||
label: inv.batch_number,
|
label: inv.batch_number,
|
||||||
value: String(inv.id),
|
value: String(inv.id),
|
||||||
sublabel: `(存:${inv.quantity} | 效:${inv.expiry_date || '無'})`
|
sublabel: `(存:${formatQuantity(inv.quantity)} | 效:${inv.expiry_date || '無'})`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const displayBatchOptions = batchOptions.length > 0 ? batchOptions : (item.inventory_id && item.ui_batch_number ? [{ label: item.ui_batch_number, value: item.inventory_id }] : []);
|
const displayBatchOptions = batchOptions.length > 0 ? batchOptions : (item.inventory_id && item.ui_batch_number ? [{ label: item.ui_batch_number, value: item.inventory_id }] : []);
|
||||||
@@ -598,6 +627,18 @@ export default function ProductionEdit({ productionOrder, products, warehouses }
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="align-top text-right">
|
||||||
|
<div className="h-9 flex items-center justify-end px-1 text-sm text-gray-600 font-medium">
|
||||||
|
{getBomItemUnitCost(item).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="align-top text-right">
|
||||||
|
<div className="h-9 flex items-center justify-end px-1 text-sm font-bold text-gray-900">
|
||||||
|
{(getBomItemUnitCost(item) * parseFloat(item.ui_input_quantity || '0')).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="align-top">
|
<TableCell className="align-top">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -621,8 +662,24 @@ export default function ProductionEdit({ productionOrder, products, warehouses }
|
|||||||
{errors.items && <p className="text-red-500 text-sm mt-2">{errors.items}</p>}
|
{errors.items && <p className="text-red-500 text-sm mt-2">{errors.items}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200 min-w-[300px]">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm text-gray-600">生產單總預估成本</span>
|
||||||
|
<span className="text-lg font-bold text-gray-900">{totalEstimatedCost.toLocaleString(undefined, { maximumFractionDigits: 2 })} 元</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t border-gray-200">
|
||||||
|
<span className="text-sm font-medium text-gray-700">預估單位生產成本
|
||||||
|
<span className="text-xs text-gray-500 ml-1">(共 {Number(data.output_quantity || 0).toLocaleString(undefined, { maximumFractionDigits: 4 })} 份)</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-md font-bold text-primary-main">
|
||||||
|
{(parseFloat(data.output_quantity) > 0 ? totalEstimatedCost / parseFloat(data.output_quantity) : 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</AuthenticatedLayout>
|
</AuthenticatedLayout >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ interface ProductionOrder {
|
|||||||
production_date: string;
|
production_date: string;
|
||||||
status: 'draft' | 'pending' | 'approved' | 'in_progress' | 'completed' | 'cancelled';
|
status: 'draft' | 'pending' | 'approved' | 'in_progress' | 'completed' | 'cancelled';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
estimated_total_cost?: number;
|
||||||
|
estimated_unit_cost?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -201,6 +203,8 @@ export default function ProductionIndex({ productionOrders, filters }: Props) {
|
|||||||
<TableHead>成品</TableHead>
|
<TableHead>成品</TableHead>
|
||||||
<TableHead>成品批號</TableHead>
|
<TableHead>成品批號</TableHead>
|
||||||
<TableHead className="text-right">數量</TableHead>
|
<TableHead className="text-right">數量</TableHead>
|
||||||
|
<TableHead className="text-right">預估單位成本</TableHead>
|
||||||
|
<TableHead className="text-right">預估總成本</TableHead>
|
||||||
<TableHead>入庫倉庫</TableHead>
|
<TableHead>入庫倉庫</TableHead>
|
||||||
<TableHead>生產日期</TableHead>
|
<TableHead>生產日期</TableHead>
|
||||||
<TableHead className="text-center w-[100px]">狀態</TableHead>
|
<TableHead className="text-center w-[100px]">狀態</TableHead>
|
||||||
@@ -210,7 +214,7 @@ export default function ProductionIndex({ productionOrders, filters }: Props) {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{productionOrders.data.length === 0 ? (
|
{productionOrders.data.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={8} className="h-32 text-center text-gray-500">
|
<TableCell colSpan={10} className="h-32 text-center text-gray-500">
|
||||||
<div className="flex flex-col items-center justify-center gap-2">
|
<div className="flex flex-col items-center justify-center gap-2">
|
||||||
<Factory className="h-10 w-10 text-gray-300" />
|
<Factory className="h-10 w-10 text-gray-300" />
|
||||||
<p>尚無生產工單</p>
|
<p>尚無生產工單</p>
|
||||||
@@ -239,6 +243,12 @@ export default function ProductionIndex({ productionOrders, filters }: Props) {
|
|||||||
<TableCell className="text-right font-medium">
|
<TableCell className="text-right font-medium">
|
||||||
{formatQuantity(order.output_quantity)}
|
{formatQuantity(order.output_quantity)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-medium text-primary-main">
|
||||||
|
{Number(order.estimated_unit_cost || 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-medium text-gray-700">
|
||||||
|
{Number(order.estimated_total_cost || 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-gray-600">
|
<TableCell className="text-gray-600">
|
||||||
{order.warehouse?.name || '-'}
|
{order.warehouse?.name || '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ interface RecipeDetailModalProps {
|
|||||||
export function RecipeDetailModal({ isOpen, onClose, recipe, isLoading }: RecipeDetailModalProps) {
|
export function RecipeDetailModal({ isOpen, onClose, recipe, isLoading }: RecipeDetailModalProps) {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const getUnitCost = (product: any, unitId: string | number) => {
|
||||||
|
if (!product || !product.cost_price) return 0;
|
||||||
|
let cost = Number(product.cost_price);
|
||||||
|
|
||||||
|
if (String(unitId) === String(product.large_unit_id) || String(unitId) === String(product.purchase_unit_id)) {
|
||||||
|
cost = cost * Number(product.conversion_rate || 1);
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto p-0 gap-0">
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto p-0 gap-0">
|
||||||
@@ -92,7 +102,7 @@ export function RecipeDetailModal({ isOpen, onClose, recipe, isLoading }: Recipe
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-medium text-gray-700">標準產量</TableCell>
|
<TableCell className="font-medium text-gray-700">標準產量</TableCell>
|
||||||
<TableCell className="text-gray-900 font-medium">
|
<TableCell className="text-gray-900 font-medium">
|
||||||
{Number(recipe.yield_quantity).toLocaleString()} {recipe.product?.base_unit?.name || '份'}
|
{Number(recipe.yield_quantity).toLocaleString(undefined, { maximumFractionDigits: 4 })} {recipe.product?.base_unit?.name || '份'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{recipe.description && (
|
{recipe.description && (
|
||||||
@@ -120,30 +130,42 @@ export function RecipeDetailModal({ isOpen, onClose, recipe, isLoading }: Recipe
|
|||||||
<TableHead>原物料名稱 / 料號</TableHead>
|
<TableHead>原物料名稱 / 料號</TableHead>
|
||||||
<TableHead className="text-right">標準用量</TableHead>
|
<TableHead className="text-right">標準用量</TableHead>
|
||||||
<TableHead>單位</TableHead>
|
<TableHead>單位</TableHead>
|
||||||
|
<TableHead className="text-right">預估單價</TableHead>
|
||||||
|
<TableHead className="text-right">成本小計</TableHead>
|
||||||
<TableHead>備註</TableHead>
|
<TableHead>備註</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{recipe.items?.length > 0 ? (
|
{recipe.items?.length > 0 ? (
|
||||||
recipe.items.map((item: any, index: number) => (
|
recipe.items.map((item: any, index: number) => {
|
||||||
<TableRow key={index} className="hover:bg-gray-50/50">
|
const unitCost = item.product ? getUnitCost(item.product, item.unit_id) : 0;
|
||||||
<TableCell className="font-medium">
|
const subtotal = unitCost * Number(item.quantity);
|
||||||
<div className="flex flex-col">
|
return (
|
||||||
<span className="text-gray-900">{item.product?.name || 'Unknown'}</span>
|
<TableRow key={index} className="hover:bg-gray-50/50">
|
||||||
<span className="text-xs text-gray-400">{item.product?.code}</span>
|
<TableCell className="font-medium">
|
||||||
</div>
|
<div className="flex flex-col">
|
||||||
</TableCell>
|
<span className="text-gray-900">{item.product?.name || 'Unknown'}</span>
|
||||||
<TableCell className="text-right font-medium text-gray-900">
|
<span className="text-xs text-gray-400">{item.product?.code}</span>
|
||||||
{Number(item.quantity).toLocaleString()}
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-gray-600">
|
<TableCell className="text-right font-medium text-gray-900">
|
||||||
{item.unit?.name || '-'}
|
{Number(item.quantity).toLocaleString(undefined, { maximumFractionDigits: 4 })}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-gray-500 text-sm">
|
<TableCell className="text-gray-600">
|
||||||
{item.remark || '-'}
|
{item.unit?.name || '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
<TableCell className="text-right text-gray-600">
|
||||||
))
|
{unitCost.toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-medium text-gray-900">
|
||||||
|
{subtotal.toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-gray-500 text-sm">
|
||||||
|
{item.remark || '-'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={4} className="h-24 text-center text-gray-500">
|
<TableCell colSpan={4} className="h-24 text-center text-gray-500">
|
||||||
@@ -154,6 +176,20 @@ export function RecipeDetailModal({ isOpen, onClose, recipe, isLoading }: Recipe
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200 min-w-[300px]">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm text-gray-600">配方預估總成本</span>
|
||||||
|
<span className="text-lg font-bold text-gray-900">{(recipe.items || []).reduce((sum: number, item: any) => sum + (getUnitCost(item.product, item.unit_id) * Number(item.quantity)), 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t border-gray-200">
|
||||||
|
<span className="text-sm font-medium text-gray-700">單位生產成本
|
||||||
|
<span className="text-xs text-gray-500 ml-1">(共 {recipe.yield_quantity} 份)</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-md font-bold text-primary-main">{(Number(recipe.yield_quantity) > 0 ? (recipe.items || []).reduce((sum: number, item: any) => sum + (getUnitCost(item.product, item.unit_id) * Number(item.quantity)), 0) / Number(recipe.yield_quantity) : 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ interface Product {
|
|||||||
code: string;
|
code: string;
|
||||||
base_unit_id?: number;
|
base_unit_id?: number;
|
||||||
large_unit_id?: number;
|
large_unit_id?: number;
|
||||||
|
purchase_unit_id?: number;
|
||||||
|
cost_price?: number;
|
||||||
|
conversion_rate?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Unit {
|
interface Unit {
|
||||||
@@ -108,6 +111,25 @@ export default function RecipeCreate({ products, units }: Props) {
|
|||||||
post(route('recipes.store'));
|
post(route('recipes.store'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUnitCost = (productId: string, unitId: string) => {
|
||||||
|
const product = products.find(p => String(p.id) === productId);
|
||||||
|
if (!product || !product.cost_price) return 0;
|
||||||
|
let cost = Number(product.cost_price);
|
||||||
|
|
||||||
|
// Check if selected unit is large_unit or purchase_unit
|
||||||
|
if (unitId && (String(unitId) === String(product.large_unit_id) || String(unitId) === String(product.purchase_unit_id))) {
|
||||||
|
cost = cost * Number(product.conversion_rate || 1);
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalCost = data.items.reduce((sum, item) => {
|
||||||
|
const unitCost = getUnitCost(item.product_id, item.unit_id);
|
||||||
|
return sum + (unitCost * Number(item.quantity || 0));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const unitCost = Number(data.yield_quantity) > 0 ? totalCost / Number(data.yield_quantity) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("recipes", [{ label: "新增", isPage: true }])}>
|
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("recipes", [{ label: "新增", isPage: true }])}>
|
||||||
<Head title="新增配方" />
|
<Head title="新增配方" />
|
||||||
@@ -233,10 +255,12 @@ export default function RecipeCreate({ products, units }: Props) {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="bg-gray-50">
|
<TableHeader className="bg-gray-50">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[35%]">原物料商品</TableHead>
|
<TableHead className="w-[30%]">原物料商品</TableHead>
|
||||||
<TableHead className="w-[20%]">標準用量</TableHead>
|
<TableHead className="w-[15%]">標準用量</TableHead>
|
||||||
<TableHead className="w-[20%]">單位</TableHead>
|
<TableHead className="w-[15%]">單位</TableHead>
|
||||||
<TableHead className="w-[20%]">備註</TableHead>
|
<TableHead className="w-[10%] text-right">預估單價</TableHead>
|
||||||
|
<TableHead className="w-[10%] text-right">成本小計</TableHead>
|
||||||
|
<TableHead className="w-[15%]">備註</TableHead>
|
||||||
<TableHead className="w-[5%]"></TableHead>
|
<TableHead className="w-[5%]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -272,10 +296,23 @@ export default function RecipeCreate({ products, units }: Props) {
|
|||||||
className="text-right"
|
className="text-right"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="align-middle">
|
<TableCell className="align-top">
|
||||||
<div className="text-sm text-gray-700 bg-gray-50 px-3 py-2 rounded-md border border-gray-100 min-h-[38px] flex items-center">
|
<SearchableSelect
|
||||||
{item.ui_unit_name || (units.find(u => String(u.id) === item.unit_id)?.name) || '-'}
|
value={item.unit_id}
|
||||||
</div>
|
onValueChange={(v) => updateItem(index, 'unit_id', v)}
|
||||||
|
options={units.map(u => ({
|
||||||
|
label: u.name,
|
||||||
|
value: String(u.id)
|
||||||
|
}))}
|
||||||
|
placeholder="單位"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="align-middle text-right text-sm text-gray-600">
|
||||||
|
{getUnitCost(item.product_id, item.unit_id).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="align-middle text-right font-medium text-gray-900">
|
||||||
|
{(getUnitCost(item.product_id, item.unit_id) * Number(item.quantity || 0)).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="align-top">
|
<TableCell className="align-top">
|
||||||
<Input
|
<Input
|
||||||
@@ -300,6 +337,23 @@ export default function RecipeCreate({ products, units }: Props) {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 配方成本總計區塊 */}
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200 min-w-[300px]">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm text-gray-600">配方預估總成本</span>
|
||||||
|
<span className="text-lg font-bold text-gray-900">{totalCost.toLocaleString(undefined, { maximumFractionDigits: 2 })} 元</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t border-gray-200">
|
||||||
|
<span className="text-sm font-medium text-gray-700">單位生產成本
|
||||||
|
<span className="text-xs text-gray-500 ml-1">(共 {Number(data.yield_quantity || 0).toLocaleString(undefined, { maximumFractionDigits: 4 })} 份)</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-md font-bold text-primary-main">{unitCost.toLocaleString(undefined, { maximumFractionDigits: 2 })} 元</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{errors.items && <p className="text-red-500 text-sm mt-2">{errors.items}</p>}
|
{errors.items && <p className="text-red-500 text-sm mt-2">{errors.items}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ interface Product {
|
|||||||
code: string;
|
code: string;
|
||||||
base_unit_id?: number;
|
base_unit_id?: number;
|
||||||
large_unit_id?: number;
|
large_unit_id?: number;
|
||||||
|
purchase_unit_id?: number;
|
||||||
|
cost_price?: number;
|
||||||
|
conversion_rate?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Unit {
|
interface Unit {
|
||||||
@@ -73,10 +76,10 @@ export default function RecipeEdit({ recipe, products, units }: Props) {
|
|||||||
code: recipe.code,
|
code: recipe.code,
|
||||||
name: recipe.name,
|
name: recipe.name,
|
||||||
description: recipe.description || "",
|
description: recipe.description || "",
|
||||||
yield_quantity: String(recipe.yield_quantity),
|
yield_quantity: String(Number(recipe.yield_quantity || 0)),
|
||||||
items: recipe.items.map(item => ({
|
items: recipe.items.map(item => ({
|
||||||
product_id: String(item.product_id),
|
product_id: String(item.product_id),
|
||||||
quantity: String(item.quantity),
|
quantity: String(Number(item.quantity || 0)),
|
||||||
unit_id: String(item.unit_id),
|
unit_id: String(item.unit_id),
|
||||||
remark: item.remark || "",
|
remark: item.remark || "",
|
||||||
ui_product_name: item.product?.name,
|
ui_product_name: item.product?.name,
|
||||||
@@ -133,6 +136,25 @@ export default function RecipeEdit({ recipe, products, units }: Props) {
|
|||||||
put(route('recipes.update', recipe.id));
|
put(route('recipes.update', recipe.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUnitCost = (productId: string, unitId: string) => {
|
||||||
|
const product = products.find(p => String(p.id) === productId);
|
||||||
|
if (!product || !product.cost_price) return 0;
|
||||||
|
let cost = Number(product.cost_price);
|
||||||
|
|
||||||
|
// Check if selected unit is large_unit or purchase_unit
|
||||||
|
if (unitId && (String(unitId) === String(product.large_unit_id) || String(unitId) === String(product.purchase_unit_id))) {
|
||||||
|
cost = cost * Number(product.conversion_rate || 1);
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalCost = data.items.reduce((sum, item) => {
|
||||||
|
const unitCost = getUnitCost(item.product_id, item.unit_id);
|
||||||
|
return sum + (unitCost * Number(item.quantity || 0));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const unitCost = Number(data.yield_quantity) > 0 ? totalCost / Number(data.yield_quantity) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("recipes", [{ label: "編輯", isPage: true }])}>
|
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("recipes", [{ label: "編輯", isPage: true }])}>
|
||||||
<Head title="編輯配方" />
|
<Head title="編輯配方" />
|
||||||
@@ -258,10 +280,12 @@ export default function RecipeEdit({ recipe, products, units }: Props) {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="bg-gray-50">
|
<TableHeader className="bg-gray-50">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[35%]">原物料商品</TableHead>
|
<TableHead className="w-[30%]">原物料商品</TableHead>
|
||||||
<TableHead className="w-[20%]">標準用量</TableHead>
|
<TableHead className="w-[15%]">標準用量</TableHead>
|
||||||
<TableHead className="w-[20%]">單位</TableHead>
|
<TableHead className="w-[15%]">單位</TableHead>
|
||||||
<TableHead className="w-[20%]">備註</TableHead>
|
<TableHead className="w-[10%] text-right">預估單價</TableHead>
|
||||||
|
<TableHead className="w-[10%] text-right">成本小計</TableHead>
|
||||||
|
<TableHead className="w-[15%]">備註</TableHead>
|
||||||
<TableHead className="w-[5%]"></TableHead>
|
<TableHead className="w-[5%]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -297,10 +321,23 @@ export default function RecipeEdit({ recipe, products, units }: Props) {
|
|||||||
className="text-right"
|
className="text-right"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="align-middle">
|
<TableCell className="align-top">
|
||||||
<div className="text-sm text-gray-700 bg-gray-50 px-3 py-2 rounded-md border border-gray-100 min-h-[38px] flex items-center">
|
<SearchableSelect
|
||||||
{item.ui_unit_name || (units.find(u => String(u.id) === item.unit_id)?.name) || '-'}
|
value={item.unit_id}
|
||||||
</div>
|
onValueChange={(v) => updateItem(index, 'unit_id', v)}
|
||||||
|
options={units.map(u => ({
|
||||||
|
label: u.name,
|
||||||
|
value: String(u.id)
|
||||||
|
}))}
|
||||||
|
placeholder="單位"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="align-middle text-right text-sm text-gray-600">
|
||||||
|
{getUnitCost(item.product_id, item.unit_id).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="align-middle text-right font-medium text-gray-900">
|
||||||
|
{(getUnitCost(item.product_id, item.unit_id) * Number(item.quantity || 0)).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="align-top">
|
<TableCell className="align-top">
|
||||||
<Input
|
<Input
|
||||||
@@ -325,6 +362,23 @@ export default function RecipeEdit({ recipe, products, units }: Props) {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 配方成本總計區塊 */}
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200 min-w-[300px]">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm text-gray-600">配方預估總成本</span>
|
||||||
|
<span className="text-lg font-bold text-gray-900">{totalCost.toLocaleString(undefined, { maximumFractionDigits: 2 })} 元</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t border-gray-200">
|
||||||
|
<span className="text-sm font-medium text-gray-700">單位生產成本
|
||||||
|
<span className="text-xs text-gray-500 ml-1">(共 {Number(data.yield_quantity || 0).toLocaleString(undefined, { maximumFractionDigits: 4 })} 份)</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-md font-bold text-primary-main">{unitCost.toLocaleString(undefined, { maximumFractionDigits: 2 })} 元</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{errors.items && <p className="text-red-500 text-sm mt-2">{errors.items}</p>}
|
{errors.items && <p className="text-red-500 text-sm mt-2">{errors.items}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ interface Recipe {
|
|||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
estimated_total_cost?: number;
|
||||||
|
estimated_unit_cost?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -187,6 +189,8 @@ export default function RecipeIndex({ recipes, filters }: Props) {
|
|||||||
<TableHead>配方名稱</TableHead>
|
<TableHead>配方名稱</TableHead>
|
||||||
<TableHead>對應成品</TableHead>
|
<TableHead>對應成品</TableHead>
|
||||||
<TableHead className="text-right">標準產量</TableHead>
|
<TableHead className="text-right">標準產量</TableHead>
|
||||||
|
<TableHead className="text-right">預估單位成本</TableHead>
|
||||||
|
<TableHead className="text-right">預估總成本</TableHead>
|
||||||
<TableHead className="text-center w-[100px]">狀態</TableHead>
|
<TableHead className="text-center w-[100px]">狀態</TableHead>
|
||||||
<TableHead className="w-[150px]">更新時間</TableHead>
|
<TableHead className="w-[150px]">更新時間</TableHead>
|
||||||
<TableHead className="text-center w-[150px]">操作</TableHead>
|
<TableHead className="text-center w-[150px]">操作</TableHead>
|
||||||
@@ -195,7 +199,7 @@ export default function RecipeIndex({ recipes, filters }: Props) {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{recipes.data.length === 0 ? (
|
{recipes.data.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className="h-32 text-center text-gray-500">
|
<TableCell colSpan={9} className="h-32 text-center text-gray-500">
|
||||||
<div className="flex flex-col items-center justify-center gap-2">
|
<div className="flex flex-col items-center justify-center gap-2">
|
||||||
<BookOpen className="h-10 w-10 text-gray-300" />
|
<BookOpen className="h-10 w-10 text-gray-300" />
|
||||||
<p>尚無配方資料</p>
|
<p>尚無配方資料</p>
|
||||||
@@ -227,7 +231,13 @@ export default function RecipeIndex({ recipes, filters }: Props) {
|
|||||||
) : '-'}
|
) : '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-medium">
|
<TableCell className="text-right font-medium">
|
||||||
{recipe.yield_quantity}
|
{Number(recipe.yield_quantity).toLocaleString(undefined, { maximumFractionDigits: 4 })}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-medium text-primary-main">
|
||||||
|
{Number(recipe.estimated_unit_cost || 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-medium text-gray-700">
|
||||||
|
{Number(recipe.estimated_total_cost || 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{recipe.is_active ? (
|
{recipe.is_active ? (
|
||||||
@@ -338,6 +348,6 @@ export default function RecipeIndex({ recipes, filters }: Props) {
|
|||||||
isLoading={isViewLoading}
|
isLoading={isViewLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AuthenticatedLayout>
|
</AuthenticatedLayout >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ interface ProductionOrderItem {
|
|||||||
origin_country: string | null;
|
origin_country: string | null;
|
||||||
product: { id: number; name: string; code: string } | null;
|
product: { id: number; name: string; code: string } | null;
|
||||||
warehouse?: { id: number; name: string } | null;
|
warehouse?: { id: number; name: string } | null;
|
||||||
|
unit_cost?: number;
|
||||||
source_purchase_order?: {
|
source_purchase_order?: {
|
||||||
id: number;
|
id: number;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -109,6 +110,13 @@ export default function ProductionShow({ productionOrder, warehouses, auth }: Pr
|
|||||||
const canCancel = hasPermission('production_orders.cancel');
|
const canCancel = hasPermission('production_orders.cancel');
|
||||||
const canEdit = hasPermission('production_orders.edit');
|
const canEdit = hasPermission('production_orders.edit');
|
||||||
|
|
||||||
|
// 計算總預估成本
|
||||||
|
const totalEstimatedCost = productionOrder.items.reduce((sum, item) => {
|
||||||
|
const qty = Number(item.quantity_used) || 0;
|
||||||
|
const cost = Number(item.inventory?.unit_cost) || 0;
|
||||||
|
return sum + (qty * cost);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("productionOrdersShow")}>
|
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("productionOrdersShow")}>
|
||||||
<Head title={`生產單 ${productionOrder.code}`} />
|
<Head title={`生產單 ${productionOrder.code}`} />
|
||||||
@@ -368,6 +376,8 @@ export default function ProductionShow({ productionOrder, warehouses, auth }: Pr
|
|||||||
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none">使用批號</TableHead>
|
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none">使用批號</TableHead>
|
||||||
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none text-center">來源國家</TableHead>
|
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none text-center">來源國家</TableHead>
|
||||||
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none">使用數量</TableHead>
|
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none">使用數量</TableHead>
|
||||||
|
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none text-right">預估單價</TableHead>
|
||||||
|
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none text-right">成本小計</TableHead>
|
||||||
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none">來源單據</TableHead>
|
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none">來源單據</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -404,6 +414,16 @@ export default function ProductionShow({ productionOrder, warehouses, auth }: Pr
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="px-6 py-5 text-right">
|
||||||
|
<div className="text-grey-0 font-medium">
|
||||||
|
{Number(item.inventory?.unit_cost || 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-6 py-5 text-right">
|
||||||
|
<div className="font-bold text-grey-900">
|
||||||
|
{(Number(item.quantity_used) * Number(item.inventory?.unit_cost || 0)).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
<TableCell className="px-6 py-5">
|
<TableCell className="px-6 py-5">
|
||||||
{item.inventory?.source_purchase_order ? (
|
{item.inventory?.source_purchase_order ? (
|
||||||
<div className="group flex flex-col">
|
<div className="group flex flex-col">
|
||||||
@@ -428,6 +448,22 @@ export default function ProductionShow({ productionOrder, warehouses, auth }: Pr
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
<div className="bg-gray-50 p-6 border-t border-grey-4 flex justify-end">
|
||||||
|
<div className="min-w-[300px]">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm text-gray-600">生產單總預估成本</span>
|
||||||
|
<span className="text-lg font-bold text-gray-900">{totalEstimatedCost.toLocaleString(undefined, { maximumFractionDigits: 2 })} 元</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t border-gray-200">
|
||||||
|
<span className="text-sm font-medium text-gray-700">預估單位生產成本
|
||||||
|
<span className="text-xs text-gray-500 ml-1">(共 {Number(productionOrder.output_quantity || 0).toLocaleString(undefined, { maximumFractionDigits: 4 })} 份)</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-md font-bold text-primary-main">
|
||||||
|
{(productionOrder.output_quantity > 0 ? totalEstimatedCost / productionOrder.output_quantity : 0).toLocaleString(undefined, { maximumFractionDigits: 2 })} 元
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -136,8 +136,7 @@ Star ERP 系統提供外部整合 API (Integration API) 供電商前台、POS
|
|||||||
| 欄位名稱 | 型態 | 必填 | 說明 |
|
| 欄位名稱 | 型態 | 必填 | 說明 |
|
||||||
| :--- | :--- | :---: | :--- |
|
| :--- | :--- | :---: | :--- |
|
||||||
| `external_order_id` | String | **是** | 第三方系統中的唯一訂單編號,不可重複 (Unique) |
|
| `external_order_id` | String | **是** | 第三方系統中的唯一訂單編號,不可重複 (Unique) |
|
||||||
| `warehouse_id` | Integer | 否 | 指定出貨倉庫的系統 ID (若已知) |
|
| `warehouse_code` | String | **是** | 指定扣除庫存的倉庫代碼 (例如:`STORE-001`)。若找不到對應倉庫將直接拒絕請求 |
|
||||||
| `warehouse` | String | 否 | 指定出貨倉庫名稱。若 `warehouse_id` 與此欄皆未傳,系統將預設寫入並建立「銷售倉庫」 |
|
|
||||||
| `payment_method` | String | 否 | 付款方式,僅接受:`cash`, `credit_card`, `line_pay`, `ecpay`, `transfer`, `other`。預設為 `cash` |
|
| `payment_method` | String | 否 | 付款方式,僅接受:`cash`, `credit_card`, `line_pay`, `ecpay`, `transfer`, `other`。預設為 `cash` |
|
||||||
| `sold_at` | String(Date) | 否 | 交易發生時間,預設為當下時間 (格式: YYYY-MM-DD HH:mm:ss) |
|
| `sold_at` | String(Date) | 否 | 交易發生時間,預設為當下時間 (格式: YYYY-MM-DD HH:mm:ss) |
|
||||||
| `items` | Array | **是** | 訂單明細陣列,至少需包含一筆商品 |
|
| `items` | Array | **是** | 訂單明細陣列,至少需包含一筆商品 |
|
||||||
@@ -154,7 +153,7 @@ Star ERP 系統提供外部整合 API (Integration API) 供電商前台、POS
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"external_order_id": "ORD-20231026-0001",
|
"external_order_id": "ORD-20231026-0001",
|
||||||
"warehouse": "台北大安門市",
|
"warehouse_code": "STORE-001",
|
||||||
"payment_method": "credit_card",
|
"payment_method": "credit_card",
|
||||||
"sold_at": "2023-10-26 14:30:00",
|
"sold_at": "2023-10-26 14:30:00",
|
||||||
"items": [
|
"items": [
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ Route::get('/api/docs', function () {
|
|||||||
|
|
||||||
$markdown = file_get_contents($path);
|
$markdown = file_get_contents($path);
|
||||||
|
|
||||||
|
// 動態替換 API Base URL 的租戶網域
|
||||||
|
$currentHost = request()->getHost();
|
||||||
|
$markdown = str_replace('[租戶網域]', $currentHost, $markdown);
|
||||||
|
|
||||||
// 解析 Markdown 內容
|
// 解析 Markdown 內容
|
||||||
$content = Illuminate\Support\Str::markdown($markdown, [
|
$content = Illuminate\Support\Str::markdown($markdown, [
|
||||||
'html_input' => 'strip',
|
'html_input' => 'strip',
|
||||||
|
|||||||
Reference in New Issue
Block a user