import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { buildGenericItemOptions, buildProductOperationOptions } from "../src/utils/inventoryOperationOptions.js"; const stockedBalances = [ { item_id: 101, item_code: "原材料00001", item_name: "304不锈钢", warehouse_id: 1, warehouse_type: "RAW", weight_available_kg: 100, qty_available: 0, avg_unit_cost: 4 }, { item_id: 101, item_code: "原材料00001", item_name: "304不锈钢", warehouse_id: 1, warehouse_type: "RAW", weight_available_kg: 40, qty_available: 0, avg_unit_cost: 5 }, { item_id: 102, item_code: "原材料00002", item_name: "冷轧板", warehouse_id: 1, warehouse_type: "RAW", weight_available_kg: 0, qty_available: 0 }, { item_id: 103, item_code: "原材料00003", item_name: "铝合金", warehouse_id: 2, warehouse_type: "RAW", weight_available_kg: 80, qty_available: 0 } ]; const rawOutboundOptions = buildGenericItemOptions({ direction: "out", activeInventoryTab: "raw", activeWarehouseType: "RAW", warehouseId: 1, balances: stockedBalances, materials: [], products: [] }); assert.deepEqual( rawOutboundOptions.map((item) => ({ item_id: item.item_id, item_code: item.item_code, item_name: item.item_name, weight_available_kg: item.weight_available_kg })), [ { item_id: 101, item_code: "原材料00001", item_name: "304不锈钢", weight_available_kg: 140 } ] ); const rawInboundOptions = buildGenericItemOptions({ direction: "in", activeInventoryTab: "raw", activeWarehouseType: "RAW", warehouseId: 1, balances: stockedBalances, materials: [{ item_id: 201, item_code: "原材料00004", item_name: "铜板" }], products: [{ item_id: 301, item_code: "产品00001", item_name: "支架" }] }); assert.deepEqual(rawInboundOptions.map((item) => item.item_id), [201]); const finishedOutboundOptions = buildGenericItemOptions({ direction: "out", activeInventoryTab: "finished", activeWarehouseType: "FINISHED", warehouseId: 8, balances: [ { item_id: 401, item_code: "产品需规00001", item_name: "电脑支架", warehouse_id: 8, warehouse_type: "FINISHED", qty_available: 12, weight_available_kg: 30 } ], materials: [], products: [] }); assert.equal(finishedOutboundOptions.length, 1); assert.equal(finishedOutboundOptions[0].qty_available, 12); const semiOutboundOptions = buildGenericItemOptions({ direction: "out", activeInventoryTab: "semi", activeWarehouseType: "SEMI", warehouseId: 9, balances: [ { item_id: 501, item_code: "产品需规00002", item_name: "半成品支架", source_line_id: 9101, option_key: "501::9101", operation_label: "产品:半成品支架;已完成工序:第1道 冲压", warehouse_id: 9, warehouse_type: "SEMI", qty_available: 6, weight_available_kg: 12 }, { item_id: 501, item_code: "产品需规00002", item_name: "半成品支架", source_line_id: 9102, option_key: "501::9102", operation_label: "产品:半成品支架;已完成工序:第2道 折弯", warehouse_id: 9, warehouse_type: "SEMI", qty_available: 8, weight_available_kg: 20 } ], materials: [], products: [] }); assert.equal(semiOutboundOptions.length, 2); assert.deepEqual(semiOutboundOptions.map((item) => item.option_key), ["501::9101", "501::9102"]); const productMasterRows = [{ item_id: 601, item_code: "产品需规00003", item_name: "委外成品" }]; const productOperationRows = [ { option_key: "601::7001", item_id: 601, item_code: "产品需规00003", item_name: "委外成品", route_id: 8001, route_operation_id: 7001, seq_no: 1, operation_name: "冲压" }, { option_key: "601::7002", item_id: 601, item_code: "产品需规00003", item_name: "委外成品", route_id: 8001, route_operation_id: 7002, seq_no: 2, operation_name: "折弯" } ]; const routeOperationOptions = buildProductOperationOptions([ { route_id: 8001, product_item_id: 601, product_code: "产品需规00003", product_name: "委外成品", version_no: "V1.0", status: "ACTIVE", operations: [ { route_operation_id: 7001, seq_no: 1, operation_name: "1", process_name: "1", stamping_method: "切边冲孔", status: "ACTIVE" } ] } ]); assert.equal(routeOperationOptions[0].operation_label, "第1道 切边冲孔"); assert.equal(routeOperationOptions[0].source_material_summary, "产品:委外成品;已完成工序:第1道 切边冲孔"); const operationWithoutStampingMethod = buildProductOperationOptions([ { route_id: 8002, product_item_id: 602, product_code: "产品需规00004", product_name: "无冲压方式产品", version_no: "V1.0", status: "ACTIVE", operations: [ { route_operation_id: 7003, seq_no: 1, operation_name: "1", process_name: "开平", stamping_method: "", status: "ACTIVE" } ] } ]); assert.equal(operationWithoutStampingMethod[0].operation_label, "第1道"); const semiInboundOptions = buildGenericItemOptions({ direction: "in", activeInventoryTab: "semi", activeWarehouseType: "SEMI", warehouseId: 9, balances: [], materials: [], products: productMasterRows, productOperations: productOperationRows }); const finishedInboundOptions = buildGenericItemOptions({ direction: "in", activeInventoryTab: "finished", activeWarehouseType: "FINISHED", warehouseId: 8, balances: [], materials: [], products: productMasterRows }); assert.deepEqual(semiInboundOptions.map((item) => item.option_key), ["601::7001", "601::7002"]); assert.deepEqual(semiInboundOptions.map((item) => item.route_operation_id), [7001, 7002]); assert.deepEqual(finishedInboundOptions.map((item) => item.item_id), [601]); const auxiliaryOutboundOptions = buildGenericItemOptions({ direction: "out", activeInventoryTab: "auxiliary", activeWarehouseType: "AUX", warehouseId: 10, balances: [ { item_id: 701, item_code: "辅料00001", item_name: "包装膜", warehouse_id: 10, warehouse_type: "AUX", qty_available: 0, weight_available_kg: 25 } ], materials: [], products: [] }); const auxiliaryInboundOptions = buildGenericItemOptions({ direction: "in", activeInventoryTab: "auxiliary", activeWarehouseType: "AUX", warehouseId: 10, balances: [], materials: [{ item_id: 702, item_code: "辅料00002", item_name: "手套" }], products: [] }); assert.equal(auxiliaryOutboundOptions.length, 1); assert.equal(auxiliaryOutboundOptions[0].item_id, 701); assert.deepEqual(auxiliaryInboundOptions.map((item) => item.item_id), [702]); const scrapInboundOptions = buildGenericItemOptions({ direction: "in", activeInventoryTab: "scrap", activeWarehouseType: "SCRAP", warehouseId: 11, balances: [], materials: [{ item_id: 801, item_code: "原材料00005", item_name: "冲压边角料" }], products: [{ item_id: 901, item_code: "产品需规00004", item_name: "报废支架" }] }); const scrapOutboundOptions = buildGenericItemOptions({ direction: "out", activeInventoryTab: "scrap", activeWarehouseType: "SCRAP", warehouseId: 11, balances: [ { item_id: 801, item_code: "原材料00005", item_name: "冲压边角料", warehouse_id: 11, warehouse_type: "SCRAP", qty_available: 10, weight_available_kg: 0 }, { item_id: 901, item_code: "产品需规00004", item_name: "报废支架", warehouse_id: 11, warehouse_type: "SCRAP", qty_available: 0, weight_available_kg: 42 }, { item_id: 902, item_code: "产品需规00005", item_name: "异仓废料", warehouse_id: 12, warehouse_type: "SCRAP", qty_available: 0, weight_available_kg: 18 } ], materials: [], products: [] }); const scrapWarehouseTypeInboundOptions = buildGenericItemOptions({ direction: "in", activeInventoryTab: "raw", activeWarehouseType: "SCRAP", warehouseId: 11, balances: [], materials: [{ item_id: 803, item_code: "原材料00006", item_name: "废铜" }], products: [{ item_id: 903, item_code: "产品需规00006", item_name: "报废夹具" }] }); assert.deepEqual(scrapInboundOptions.map((item) => item.item_id), [801, 901]); assert.deepEqual(scrapWarehouseTypeInboundOptions.map((item) => item.item_id), [803, 903]); assert.deepEqual( scrapOutboundOptions.map((item) => ({ item_id: item.item_id, qty_available: item.qty_available, weight_available_kg: item.weight_available_kg })), [ { item_id: 901, qty_available: 0, weight_available_kg: 42 } ] ); const inventoryLedgerSource = readFileSync( fileURLToPath(new URL("../src/views/InventoryLedgerView.vue", import.meta.url)), "utf8" ); const deliveryManagementSource = readFileSync( fileURLToPath(new URL("../src/views/DeliveryManagementView.vue", import.meta.url)), "utf8" ); const tableControlsSource = readFileSync( fileURLToPath(new URL("../src/components/TableControls.vue", import.meta.url)), "utf8" ); const stockLotTagSelectSource = readFileSync( fileURLToPath(new URL("../src/components/StockLotTagSelect.vue", import.meta.url)), "utf8" ); assert.match(inventoryLedgerSource, /key: "scrap"/); assert.match(inventoryLedgerSource, /label: "废料库"/); assert.match(inventoryLedgerSource, /PRODUCTION_SCRAP_IN/); assert.match( inventoryLedgerSource, /const genericUnitCostLabel = computed\(\(\) =>[\s\S]*isProductionScrapInbound\.value[\s\S]*原材料成本单价\(元\/kg\)/, "production scrap inbound should label unit cost as raw material cost unit price" ); assert.match( inventoryLedgerSource, /const productionScrapToleranceRemainingWeight = computed\(\(\) => \{[\s\S]*productionScrapExpectedWeight\.value[\s\S]*1\.15[\s\S]*productionScrapInboundBeforeWeight/, "production scrap inbound should calculate the non-settle upper limit from cumulative scrap inbound weight" ); assert.match( inventoryLedgerSource, /genericForm\.settle_work_order && productionScrapSettleNeedsRemark\.value/, "production scrap inbound should require a settle explanation when final scrap weight is outside tolerance" ); assert.match( inventoryLedgerSource, /const scrapToleranceMax = Number\(productionScrapToleranceRemainingWeight\.value \|\| 0\);[\s\S]*inboundWeight > scrapToleranceMax/, "production scrap inbound validation should compare inbound weight against cumulative upper-limit remaining amount" ); assert.match( inventoryLedgerSource, /activeOperation\?\.direction !== 'out' && !isCustomerSuppliedInbound && !isSourceLotReturnInbound && !isSemiSourceLotTraceInbound && !isFinishedOutsourcingInbound && !isProductionScrapInbound && !isReworkScrapInbound && !isOutsourcingScrapInbound/, "production scrap inbound should hide the manual batch number field" ); assert.match( inventoryLedgerSource, /lot_no: isCustomerSuppliedInbound\.value \? null : isProductionScrapInbound\.value \? null[\s\S]*isOutsourcingScrapInbound\.value \? null/, "production scrap inbound should never submit a manually-entered batch number" ); assert.match(inventoryLedgerSource, /OUTSOURCING_SCRAP_IN/); assert.match(inventoryLedgerSource, /SCRAP_SALE_OUT/); assert.match(inventoryLedgerSource, /LOGISTICS_REQUIRED_BIZ_TYPES/); assert.match(inventoryLedgerSource, /"CUSTOMER_SUPPLIED"/); assert.match(inventoryLedgerSource, /const isCustomerSuppliedInbound = computed/); assert.match(inventoryLedgerSource, /customerSuppliedLotNoPreview/); assert.match(inventoryLedgerSource, /提交后自动生成/); assert.match(inventoryLedgerSource, /activeOperation\?\.direction !== 'out' && !isCustomerSuppliedInbound/); assert.match(inventoryLedgerSource, /lot_no: isCustomerSuppliedInbound\.value \? null/); assert.match(inventoryLedgerSource, /source_material_sub_batch_no: isCustomerSuppliedInbound\.value\s+\?\s+null/); assert.match(inventoryLedgerSource, /const workOrderLedgerRows = ref\(\[\]\);/); assert.match(inventoryLedgerSource, /fetchResource\("\/production\/work-order-ledger\?limit=500", \[\]\)/); assert.match(inventoryLedgerSource, /workOrderLedgerRows\.value = workOrderLedgerRowData;/); assert.match( stockLotTagSelectSource, /issued_weight_kg:\s*normalizeSelectedIssuedWeight\(lot\)/, "StockLotTagSelect should preserve an option's issued_weight_kg instead of overwriting it with remaining weight" ); assert.match( stockLotTagSelectSource, /function displayWeight\(lot\)/, "StockLotTagSelect should support scenario-specific selected-tag weight display" ); assert.match( inventoryLedgerSource, /:display-weight-field="isProductionSurplusInbound \? 'issued_weight_kg' : 'remaining_weight_kg'"/, "production surplus inbound should display selected source lots by work-order issued weight" ); assert.doesNotMatch( inventoryLedgerSource, /
/, "source-lot return/outbound live total should not use feedback-banner because it triggers toast notifications while typing" ); assert.doesNotMatch( inventoryLedgerSource, /
/, "source-lot trace-only live summary should not use feedback-banner because it is not an operation result" ); assert.match( inventoryLedgerSource, /function computeWorkOrderSourceLotReturnLimit\(workOrder,\s*material\)/, "production surplus inbound should calculate source-lot return limit from the work-order surplus limit" ); assert.match( inventoryLedgerSource, /max_return_weight_kg:\s*returnLimit\.normal/, "source-lot normal return weight should be capped by the work-order normal surplus amount" ); assert.match( inventoryLedgerSource, /tolerance_return_weight_kg:\s*returnLimit\.tolerance/, "source-lot tolerance return weight should be capped by the work-order 15 percent tolerance amount" ); assert.match( inventoryLedgerSource, /const productionSurplusToleranceRemainingWeight = computed\(\(\) => \{[\s\S]*productionSurplusExpectedReturnWeight\.value[\s\S]*1\.15[\s\S]*productionSurplusReturnedBeforeWeight/, "production surplus inbound should calculate the non-settle upper limit from cumulative returned weight" ); assert.match( inventoryLedgerSource, /:max="productionSurplusReturnInputMax\(lot\)"/, "production surplus inbound input should use a dynamic return max so settle mode can allow explained deviations" ); assert.match( inventoryLedgerSource, /function productionSurplusReturnInputMax\(lot\)[\s\S]*genericForm\.settle_work_order[\s\S]*source_remaining_weight_kg/, "production surplus inbound should relax the input max to source-lot remaining return weight when settling" ); assert.match( inventoryLedgerSource, /const toleranceMax = Number\(productionSurplusToleranceRemainingWeight\.value \|\| 0\);[\s\S]*genericSourceLotReturnWeight\.value > toleranceMax/, "production surplus inbound validation should compare the selected return total against the cumulative upper-limit remaining amount" ); assert.match( inventoryLedgerSource, /const receivableWorkOrders = computed\(\(\) =>\s*workOrderLedgerRows\.value\.filter\([\s\S]*!isWorkOrderSettled\(item\)[\s\S]*tolerance_finished_inbound_qty[\s\S]*\)\s*\);/ ); assert.match( inventoryLedgerSource, /completionForm\.tolerance_receivable_qty = Number\(workOrder\.limits\?\.tolerance_finished_inbound_qty \|\| completionForm\.max_receivable_qty \|\| 0\);/ ); assert.match( inventoryLedgerSource, /const genericWorkOrderOptions = computed\(\(\) => \{[\s\S]*!isWorkOrderSettled\(item,\s*"PRODUCTION_SCRAP_IN"\)[\s\S]*tolerance_scrap_inbound_weight_kg[\s\S]*!isWorkOrderSettled\(item,\s*"PRODUCTION_SURPLUS"\)[\s\S]*tolerance_surplus_inbound_weight_kg[\s\S]*return workOrderLedgerRows\.value\.filter\(\(item\) => !isWorkOrderSettled\(item\)\);[\s\S]*\}\);/ ); assert.match( inventoryLedgerSource, /source_doc_type: isProductionSurplusInbound\.value \? "WORK_ORDER" : null[\s\S]*source_doc_id: isProductionSurplusInbound\.value \? Number\(selectedGenericWorkOrder\.value\.work_order_id\) : null/ ); assert.match( inventoryLedgerSource, /source_doc_type: isProductionScrapInbound\.value \|\| isReworkScrapInbound\.value \? "WORK_ORDER" : activeWarehouseType\.value === "SEMI" \? "PRODUCT_ROUTE_OPERATION" : null[\s\S]*source_doc_id: isProductionScrapInbound\.value \|\| isReworkScrapInbound\.value \? Number\(selectedGenericWorkOrder\.value\.work_order_id\) : activeWarehouseType\.value === "SEMI" \? Number\(selectedSourceDocId \|\| 0\) : null/ ); assert.match(inventoryLedgerSource, /:key="`inventory-main-table-\$\{activeInventoryTab\}`"/); assert.match(inventoryLedgerSource, /:key="`inventory-controls-\$\{activeInventoryTab\}`"/); assert.match(inventoryLedgerSource, /balanceControls\.sortKey\.value === "operation_label"/); assert.match(tableControlsSource, /hasEnhancedButton/); assert.match(tableControlsSource, /currentHeaderLabel/); assert.match(inventoryLedgerSource, /const isSelectedFinishedWarehouse = computed/); assert.match(inventoryLedgerSource, /const selectedPrimaryAvailableLabel = computed/); assert.match(inventoryLedgerSource, /const selectedPrimaryOnHandLabel = computed/); assert.match(inventoryLedgerSource, /const selectedAuxiliaryMetricLabel = computed/); assert.match(inventoryLedgerSource, /function formatSelectedUnitCost/); assert.match(inventoryLedgerSource, /selectedWarehouseType\.value === "FINISHED"[\s\S]*return "\/件"/); assert.match(inventoryLedgerSource, /import DrawerDataTable from "\.\.\/components\/DrawerDataTable\.vue"/); assert.match(inventoryLedgerSource, /const overviewLotDrawerColumns = computed/); assert.match(inventoryLedgerSource, /const lotDrawerColumns = computed/); assert.match(inventoryLedgerSource, /const txnDrawerColumns = computed/); assert.match(inventoryLedgerSource, /key: "inbound_qty"[\s\S]*label: "入库数量"/); assert.match(inventoryLedgerSource, /key: "auxiliary_weight"[\s\S]*label: "辅助重量\(kg\)"/); assert.match(inventoryLedgerSource, /v-model:expanded-rows="expandedLotRows"/); assert.match(inventoryLedgerSource, /v-model:expanded-rows="expandedTxnRows"/); assert.match(inventoryLedgerSource, /"OUTSOURCING_SURPLUS"/); assert.match(inventoryLedgerSource, /"OUTSOURCING_IN"/); assert.match(inventoryLedgerSource, /"OUTSOURCING_SCRAP_IN"/); assert.match(inventoryLedgerSource, /"OUTSOURCING_OUT"/); assert.match(inventoryLedgerSource, /"SCRAP_SALE_OUT"/); assert.match(inventoryLedgerSource, /v-model\.trim="genericForm\.waybill_no"/); assert.match(inventoryLedgerSource, /v-model\.number="genericForm\.freight_amount"/); assert.match(inventoryLedgerSource, /handleLogisticsPhotoUpload\(genericForm/); assert.match(inventoryLedgerSource, /order_photo_url: genericRequiresLogistics\.value \? genericForm\.order_photo_url/); assert.match(inventoryLedgerSource, /v-model\.trim="deliveryForm\.waybill_no"/); assert.match(inventoryLedgerSource, /v-model\.number="deliveryForm\.freight_amount"/); assert.match(inventoryLedgerSource, /handleLogisticsPhotoUpload\(deliveryForm/); assert.match(inventoryLedgerSource, /order_photo_url: deliveryForm\.order_photo_url/); assert.match(deliveryManagementSource, /发货台账/); assert.match(deliveryManagementSource, /交付查询/); assert.doesNotMatch(deliveryManagementSource, /v-model\.trim="form\.waybill_no"/); assert.doesNotMatch(deliveryManagementSource, /v-model\.number="form\.freight_amount"/); assert.doesNotMatch(deliveryManagementSource, /handleLogisticsPhotoUpload/); assert.doesNotMatch(deliveryManagementSource, /waybill_no: form\.waybill_no/); assert.doesNotMatch(deliveryManagementSource, /order_photo_url: form\.order_photo_url/); assert.doesNotMatch(deliveryManagementSource, /freight_amount: Number\(form\.freight_amount\)/); assert.match(inventoryLedgerSource, /fetchResource\("\/master-data\/materials\?limit=200", \[\]\)/); assert.match(inventoryLedgerSource, /fetchResource\("\/master-data\/products\?limit=200", \[\]\)/); assert.match(inventoryLedgerSource, /fetchResource\("\/sales\/customers\?limit=200", \[\]\)/); assert.doesNotMatch(inventoryLedgerSource, /fetchResource\("\/master-data\/materials\?limit=500", \[\]\)/); assert.doesNotMatch(inventoryLedgerSource, /fetchResource\("\/master-data\/products\?limit=500", \[\]\)/); assert.doesNotMatch(inventoryLedgerSource, /fetchResource\("\/sales\/customers\?limit=500", \[\]\)/); assert.match(inventoryLedgerSource, /const isSemiSourceLotTraceInbound = computed/); assert.match(inventoryLedgerSource, /\["WIP_IN", "OUTSOURCING_IN"\]\.includes/); assert.match(inventoryLedgerSource, /const isSemiAutoSourceOutbound = computed/); assert.match(inventoryLedgerSource, /\["WIP_OUT", "OUTSOURCING_OUT"\]\.includes/); assert.match(inventoryLedgerSource, /const semiAutoSourceOutboundSourceLots = computed/); assert.match(inventoryLedgerSource, /入库数量/); assert.match(inventoryLedgerSource, /出库数量/); assert.match(inventoryLedgerSource, /入库类型/); assert.match(inventoryLedgerSource, /按件入库/); assert.match(inventoryLedgerSource, /按重入库/); assert.match(inventoryLedgerSource, /const selectedSemiMeasureBasis = computed/); assert.match(inventoryLedgerSource, /function formatSemiAvailability/); assert.match(inventoryLedgerSource, /function formatSemiMeasureValue/); assert.match(inventoryLedgerSource, /委外方/); assert.match(inventoryLedgerSource, /outsourcing_party_name/); assert.match(inventoryLedgerSource, /getProductBomMaterialItemIds\(genericForm\.item_id\)/); assert.match(inventoryLedgerSource, /:allow-unavailable="isSourceLotReturnInbound \|\| isSemiSourceLotTraceInbound \|\| isOutsourcingScrapInbound"/); assert.match(inventoryLedgerSource, /const inboundQty = activeWarehouseType\.value === "AUX" \|\| isFinishedOutsourcingInbound\.value[\s\S]*\? Number\(genericForm\.qty \|\| 0\)[\s\S]*: isSemiSourceLotTraceInbound\.value && selectedSemiMeasureBasis\.value === "PIECE"/); assert.match(inventoryLedgerSource, /outbound_qty: activeWarehouseType\.value === "AUX"[\s\S]*\? Number\(genericForm\.qty \|\| 0\)[\s\S]*: isSemiAutoSourceOutbound\.value && selectedSemiMeasureBasis\.value === "PIECE"/); assert.match(inventoryLedgerSource, /const selectedTraceSourceLotIds = isProductTraceOnlySourceLotInbound\.value[\s\S]*selectedGenericSourceLots\.value\.map/); assert.match(inventoryLedgerSource, /source_lot_ids: selectedTraceSourceLotIds/); assert.match(inventoryLedgerSource, /semiAutoSourceOutboundSourceLotSummary/); assert.match(inventoryLedgerSource, /仅用于来源追溯,不扣减原材料库存/); console.log("inventory ledger option tests passed");