實作 InventoryService 的批量入庫 (processIncomingInventory) 與庫存調整 (adjustInventory) 邏輯
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s

This commit is contained in:
2026-03-02 10:47:43 +08:00
parent 5f8b2a1c2d
commit 649af40919
5 changed files with 330 additions and 226 deletions

View File

@@ -43,10 +43,15 @@ interface Props {
// 欄位翻譯對照表
const fieldLabels: Record<string, string> = {
id: 'ID',
created_at: '建立時間',
updated_at: '更新時間',
deleted_at: '刪除時間',
name: '名稱',
code: '商品代號',
username: '登入帳號',
description: '描述',
// ... (保持原有翻譯)
price: '價格',
cost: '成本',
stock: '庫存',
@@ -66,6 +71,11 @@ const fieldLabels: Record<string, string> = {
role_id: '角色',
email_verified_at: '電子郵件驗證時間',
remember_token: '登入權杖',
barcode: '條碼',
external_pos_id: '外部 POS ID',
cost_price: '成本價',
member_price: '會員價',
wholesale_price: '批發價',
// 快照欄位
category_name: '分類名稱',
base_unit_name: '基本單位名稱',
@@ -78,6 +88,9 @@ const fieldLabels: Record<string, string> = {
contact_name: '聯絡人',
tel: '電話',
remark: '備註',
license_plate: '車牌號碼',
driver_name: '司機姓名',
default_transit_warehouse_id: '預設在途倉庫',
// 倉庫與庫存欄位
warehouse_name: '倉庫名稱',
product_name: '商品名稱',
@@ -98,6 +111,12 @@ const fieldLabels: Record<string, string> = {
quality_status: '品質狀態',
quality_remark: '品質備註',
purchase_order_id: '來源採購單',
inventory_id: '庫存 ID',
balance_before: '異動前餘額',
balance_after: '異動後餘額',
reference_type: '參考單據類型',
reference_id: '參考單據 ID',
actual_time: '實際時點',
// 採購單欄位
po_number: '採購單號',
vendor_id: '廠商',
@@ -173,7 +192,50 @@ const statusMap: Record<string, string> = {
in_progress: '生產中',
// 調撥單狀態
voided: '已作廢',
// completed 已定義
};
// 主體類型解析 (Model 類名轉中文)
const subjectTypeMap: Record<string, string> = {
// 完整路徑映射
'App\\Modules\\Inventory\\Models\\Product': '商品資料',
'App\\Modules\\Inventory\\Models\\Warehouse': '倉庫資料',
'App\\Modules\\Inventory\\Models\\Inventory': '庫存異動',
'App\\Modules\\Inventory\\Models\\Category': '商品分類',
'App\\Modules\\Inventory\\Models\\Unit': '單位',
'App\\Modules\\Inventory\\Models\\InventoryTransaction': '庫存異動紀錄',
'App\\Modules\\Inventory\\Models\\GoodsReceipt': '進貨單',
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
'App\\Modules\\Production\\Models\\ProductionOrder': '生產工單',
'App\\Modules\\Production\\Models\\Recipe': '生產配方',
'App\\Modules\\Production\\Models\\RecipeItem': '配方品項',
'App\\Modules\\Production\\Models\\ProductionOrderItem': '工單品項',
'App\\Modules\\Finance\\Models\\UtilityFee': '公共事業費',
'App\\Modules\\Core\\Models\\User': '使用者帳號',
'App\\Modules\\Core\\Models\\Role': '角色權限',
// 簡寫映射 (應對後端回傳 class_basename 的情況)
'Product': '商品資料',
'Warehouse': '倉庫資料',
'Inventory': '庫存異動',
'InventoryTransaction': '庫存異動紀錄',
'Category': '商品分類',
'Unit': '單位',
'Vendor': '廠商資料',
'PurchaseOrder': '採購單',
'GoodsReceipt': '進貨單',
'ProductionOrder': '生產工單',
'Recipe': '生產配方',
'InventoryCountDoc': '庫存盤點單',
'InventoryAdjustDoc': '庫存盤調單',
'InventoryTransferOrder': '庫存調撥單',
'StockMovementDoc': '庫存單據',
'User': '使用者帳號',
'Role': '角色權限',
'UtilityFee': '公共事業費',
};
// 庫存品質狀態對照表
@@ -202,9 +264,10 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
// 自訂欄位排序順序
const sortOrder = [
'po_number', 'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark',
'doc_no', 'po_number', 'gr_number', 'production_number',
'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark',
'invoice_number', 'invoice_date', 'invoice_amount',
'total_amount', 'tax_amount', 'grand_total' // 確保金額的特定順序
'total_amount', 'tax_amount', 'grand_total'
];
// 過濾掉通常會記錄但對使用者無用的內部鍵
@@ -215,30 +278,22 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
.sort((a, b) => {
const indexA = sortOrder.indexOf(a);
const indexB = sortOrder.indexOf(b);
// 如果兩者都在排序順序中,比較索引
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
// 如果只有 A 在排序順序中,它排在前面(或根據邏輯,通常將已知欄位排在前面)
if (indexA !== -1) return -1;
if (indexB !== -1) return 1;
// 否則按字母順序或預設
return a.localeCompare(b);
});
// 檢查鍵是否為快照名稱欄位或輔助名稱欄位的輔助函式
const isSnapshotField = (key: string) => {
// 隱藏快照欄位
const snapshotFields = [
'category_name', 'base_unit_name', 'large_unit_name', 'purchase_unit_name',
'warehouse_name', 'user_name', 'from_warehouse_name', 'to_warehouse_name',
'created_by_name', 'updated_by_name', 'completed_by_name', 'posted_by_name'
'created_by_name', 'updated_by_name', 'completed_by_name', 'posted_by_name',
'vendor_name', 'product_name', 'recipe_name'
];
if (snapshotFields.includes(key)) return true;
// 隱藏所有以 _name 結尾的欄位(因為它們通常是 ID 欄位的文字補充)
if (key.endsWith('_name')) return true;
return false;
};
@@ -262,36 +317,26 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
};
const formatValue = (key: string, value: any) => {
// 遮蔽密碼
if (key === 'password') return '******';
if (value === null || value === undefined) return '-';
if (typeof value === 'boolean') return value ? '是' : '否';
if (key === 'is_active') return value ? '啟用' : '停用';
// 處理採購單狀態
if (key === 'status' && typeof value === 'string' && statusMap[value]) {
return statusMap[value];
}
// 處理庫存品質狀態
if (key === 'quality_status' && typeof value === 'string' && qualityStatusMap[value]) {
return qualityStatusMap[value];
}
// 處理入庫類型
if (key === 'type' && typeof value === 'string' && typeMap[value]) {
return typeMap[value];
}
// 處理日期欄位 (YYYY-MM-DD)
if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date') && typeof value === 'string') {
// 僅取日期部分 (YYYY-MM-DD)
// 處理日期與時間
if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date' || key === 'received_date' || key === 'production_date') && typeof value === 'string') {
return value.split('T')[0].split(' ')[0];
}
// 處理日期時間欄位 (YYYY-MM-DD HH:mm:ss)
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at') && typeof value === 'string') {
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at') && typeof value === 'string') {
try {
const date = new Date(value);
return date.toLocaleString('zh-TW', {
@@ -308,44 +353,31 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
return value;
}
}
return String(value);
};
const getFormattedValue = (key: string, value: any) => {
// 如果是 ID 欄位,嘗試在快照或屬性中尋找對應名稱
if (key.endsWith('_id')) {
const nameKey = key.replace('_id', '_name');
// 先檢查快照,然後檢查屬性
const nameValue = snapshot[nameKey] || attributes[nameKey];
if (nameValue) {
return `${nameValue}`;
}
if (nameValue) return `${nameValue}`;
}
return formatValue(key, value);
};
// 取得翻譯欄位標籤的輔助函式
const getFieldLabel = (key: string) => {
return fieldLabels[key] || key;
};
const getFieldLabel = (key: string) => fieldLabels[key] || key;
const getSubjectTypeLabel = (type: string) => subjectTypeMap[type] || type;
// 取得標題的主題名稱
const getSubjectName = () => {
// 庫存的特殊處理:顯示 "倉庫 - 商品"
if ((snapshot.warehouse_name || attributes.warehouse_name) && (snapshot.product_name || attributes.product_name)) {
const wName = snapshot.warehouse_name || attributes.warehouse_name;
const pName = snapshot.product_name || attributes.product_name;
return `${wName} - ${pName}`;
return `${snapshot.warehouse_name || attributes.warehouse_name} - ${snapshot.product_name || attributes.product_name}`;
}
const nameParams = ['doc_no', 'po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title'];
const nameParams = ['doc_no', 'po_number', 'gr_number', 'production_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'title'];
for (const param of nameParams) {
if (snapshot[param]) return snapshot[param];
if (attributes[param]) return attributes[param];
if (old[param]) return old[param];
}
if (attributes.id || old.id) return `#${attributes.id || old.id}`;
return '';
};
@@ -365,7 +397,6 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
</Badge>
</div>
{/* 現代化元數據條 */}
<div className="flex flex-wrap items-center gap-6 pt-2 text-sm text-gray-500">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
@@ -379,19 +410,19 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
<Package className="w-4 h-4 text-gray-400" />
<span className="font-medium text-gray-700">
{subjectName ? `${subjectName} ` : ''}
{activity.properties?.sub_subject || activity.subject_type}
({getSubjectTypeLabel(activity.properties?.sub_subject || activity.subject_type)})
</span>
</div>
{/* 僅在描述與事件名稱不同時顯示(不太可能發生但為了安全起見) */}
{activity.description !== getEventLabel(activity.event) &&
activity.description !== 'created' && activity.description !== 'updated' && (
<div className="flex items-center gap-2">
<ActivityIcon className="w-4 h-4 text-gray-400" />
<span>{activity.description}</span>
</div>
)}
</div>
</DialogHeader>
{/* 僅在描述與事件名稱不同時顯示(不太可能發生但為了安全起見) */}
{activity.description !== getEventLabel(activity.event) &&
activity.description !== 'created' && activity.description !== 'updated' && (
<div className="flex items-center gap-2 px-6 pb-2 -mt-2">
<ActivityIcon className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500">{activity.description}</span>
</div>
)}
<div className="bg-gray-50/50 p-6 min-h-[300px]">
{activity.event === 'created' ? (
@@ -571,7 +602,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
</div>
)}
</div>
</DialogContent>
</Dialog>
</DialogContent >
</Dialog >
);
}

View File

@@ -29,6 +29,50 @@ interface LogTableProps {
from?: number; // 起始索引編號 (paginator.from)
}
// 主體類型解析 (Model 類名轉中文)
const subjectTypeMap: Record<string, string> = {
// 完整路徑映射
'App\\Modules\\Inventory\\Models\\Product': '商品資料',
'App\\Modules\\Inventory\\Models\\Warehouse': '倉庫資料',
'App\\Modules\\Inventory\\Models\\Inventory': '庫存異動',
'App\\Modules\\Inventory\\Models\\Category': '商品分類',
'App\\Modules\\Inventory\\Models\\Unit': '單位',
'App\\Modules\\Inventory\\Models\\InventoryTransaction': '庫存異動紀錄',
'App\\Modules\\Inventory\\Models\\GoodsReceipt': '進貨單',
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
'App\\Modules\\Production\\Models\\ProductionOrder': '生產工單',
'App\\Modules\\Production\\Models\\Recipe': '生產配方',
'App\\Modules\\Production\\Models\\RecipeItem': '配方品項',
'App\\Modules\\Production\\Models\\ProductionOrderItem': '工單品項',
'App\\Modules\\Finance\\Models\\UtilityFee': '公共事業費',
'App\\Modules\\Core\\Models\\User': '使用者帳號',
'App\\Modules\\Core\\Models\\Role': '角色權限',
// 簡寫映射
'Product': '商品資料',
'Warehouse': '倉庫資料',
'Inventory': '庫存異動',
'InventoryTransaction': '庫存異動紀錄',
'Category': '商品分類',
'Unit': '單位',
'Vendor': '廠商資料',
'PurchaseOrder': '採購單',
'GoodsReceipt': '進貨單',
'ProductionOrder': '生產工單',
'Recipe': '生產配方',
'InventoryCountDoc': '庫存盤點單',
'InventoryAdjustDoc': '庫存盤調單',
'InventoryTransferOrder': '庫存調撥單',
'StockMovementDoc': '庫存單據',
'User': '使用者帳號',
'Role': '角色權限',
'UtilityFee': '公共事業費',
};
export default function LogTable({
activities,
sortField,
@@ -37,6 +81,8 @@ export default function LogTable({
onViewDetail,
from = 1
}: LogTableProps) {
const getSubjectTypeLabel = (type: string) => subjectTypeMap[type] || type;
const getEventBadgeClass = (event: string) => {
switch (event) {
case 'created': return 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100';
@@ -111,7 +157,7 @@ export default function LogTable({
{props.sub_subject ? (
<span className="text-gray-700">{props.sub_subject}</span>
) : (
<span className="text-gray-700">{activity.subject_type}</span>
<span className="text-gray-700">{getSubjectTypeLabel(activity.subject_type)}</span>
)}
{/* 如果有原因/來源則顯示(例如:來自補貨) */}
@@ -185,7 +231,7 @@ export default function LogTable({
</TableCell>
<TableCell className="max-w-[200px]">
<Badge variant="outline" className="bg-slate-50 text-slate-600 border-slate-200 break-all whitespace-normal text-left h-auto py-1">
{activity.subject_type}
{getSubjectTypeLabel(activity.subject_type)}
</Badge>
</TableCell>
<TableCell className="text-center">