實作 InventoryService 的批量入庫 (processIncomingInventory) 與庫存調整 (adjustInventory) 邏輯
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:
@@ -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 >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user