From 65d009dcdcb6d00c2522b6a5442b2a8db9e31e4b Mon Sep 17 00:00:00 2001 From: lunanting Date: Wed, 16 Sep 2026 17:52:49 +0800 Subject: [PATCH] fix: read BOM locations from pack inventory --- backend/src/services/mkhBomService.js | 76 +++++++++++++++++++++++---- backend/test/mkhBomService.test.js | 19 +++++++ 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/backend/src/services/mkhBomService.js b/backend/src/services/mkhBomService.js index 3f3bd17..9da13ae 100644 --- a/backend/src/services/mkhBomService.js +++ b/backend/src/services/mkhBomService.js @@ -30,8 +30,9 @@ // payload 里的 bomDetails / factoryRows / pricingRows 属成本与工厂行,结构不同 // (无 SKU 条码),也不能直接当配件 BOM。 // -// ④ 配件仓位 —— 业务库 SKU 档案中的库位字段; -// 业务库 jst_sku.bin / other_bin 全库为空,故多数配件显示「未维护」。 +// ④ 配件仓位 —— 业务库 rpt.jst_packInventoryInfo; +// sku_id 可能带“-包装序号”后缀,需按基础配件编码匹配。优先显示 bin_group / bin, +// 仓位为空时显示净数量大于 0 的 pack_id(箱号)与 wh_id(仓库)。 // // ⑤ 配件库存 —— 业务库 rpt.jst_inventoryInfo 按 sku_id 汇总。 @@ -335,6 +336,46 @@ function countDistinctBomProducts(...boms) { const sumQty = (components) => components.reduce((sum, item) => sum + (toNumber(item.qty) || 0), 0); +function matchesPackSku(componentCode, packSkuCode) { + const component = toText(componentCode)?.toUpperCase(); + const packSku = toText(packSkuCode)?.toUpperCase(); + if (!component || !packSku) return false; + return packSku === component || packSku.startsWith(`${component}-`); +} + +function formatPackLocation(row) { + const warehouseId = toText(row.warehouseId); + const binGroup = toText(row.binGroup); + const bin = toText(row.bin); + const packId = toText(row.packId); + const warehouse = warehouseId ? `仓库 ${warehouseId}` : ''; + const position = [binGroup, bin].filter(Boolean).join(' / ') + || (packId ? `箱号 ${packId}` : ''); + return [warehouse, position].filter(Boolean).join(' · '); +} + +function buildPackLocationMap(rows, componentCodes) { + const locations = new Map(componentCodes.map((code) => [code, new Set()])); + + rows.forEach((row) => { + const componentCode = componentCodes.find((code) => matchesPackSku(code, row.skuId)); + if (!componentCode) return; + const location = formatPackLocation(row); + if (location) locations.get(componentCode).add(location); + }); + + const result = new Map(); + locations.forEach((values, code) => { + const all = [...values]; + if (!all.length) return; + const visible = all.slice(0, 10); + result.set(code, all.length > visible.length + ? `${visible.join('、')} 等 ${all.length} 个位置` + : visible.join('、')); + }); + return result; +} + /** * 找出「资料库里已维护 BOM、但业务库列表里没有」的模块化商品, * 作为追加行放在列表最前面,保证已维护的数据一定能在页面上看到。 @@ -594,17 +635,31 @@ async function getMkhBomDetail(mkhSkuId) { } try { + const locationParams = {}; + const locationConditions = codes.map((code, index) => { + locationParams[`lc${index}`] = code; + locationParams[`lp${index}`] = `${code}-%`; + return `(p.sku_id = @lc${index} OR p.sku_id LIKE @lp${index})`; + }); const locRows = await cloudDatabase.query( - `SELECT sku_id AS skuId, - COALESCE(NULLIF(LTRIM(RTRIM(bin)), N''), NULLIF(LTRIM(RTRIM(other_bin)), N'')) AS location - FROM dbo.jst_sku - WHERE sku_id IN (${placeholders})`, - codeParams, + `SELECT p.sku_id AS skuId, + p.pack_id AS packId, + p.wh_id AS warehouseId, + p.bin AS bin, + p.bin_group AS binGroup, + SUM(p.qty) AS qty + FROM rpt.jst_packInventoryInfo p + WHERE ${locationConditions.join(' OR ')} + GROUP BY p.sku_id, p.pack_id, p.wh_id, p.bin, p.bin_group + HAVING SUM(p.qty) > 0`, + locationParams, { requestTimeout: 120000 } ); - locRows.forEach((row) => locationMap.set(toText(row.skuId), toText(row.location))); + buildPackLocationMap(locRows, codes).forEach((location, code) => { + locationMap.set(code, location); + }); } catch (error) { - console.warn('[MkhBom] 配件仓位查询降级:', error.message); + console.warn('[MkhBom] 配件箱号/仓位查询降级:', error.message); } } @@ -642,5 +697,8 @@ module.exports = { buildBusinessBomExtraRows, mergeExtraRows, filterExtraRows, + matchesPackSku, + formatPackLocation, + buildPackLocationMap, }, }; diff --git a/backend/test/mkhBomService.test.js b/backend/test/mkhBomService.test.js index 603c7b6..c553d1d 100644 --- a/backend/test/mkhBomService.test.js +++ b/backend/test/mkhBomService.test.js @@ -93,3 +93,22 @@ test('BOM-only rows honor the same keyword filter as jst_sku rows', () => { assert.deepEqual(rows.map((row) => row.skuId), ['MKH-B-1']); }); + +test('pack inventory locations match a BOM component and its package suffixes', () => { + assert.equal(_private.matchesPackSku('PART-1', 'PART-1'), true); + assert.equal(_private.matchesPackSku('PART-1', 'PART-1-10'), true); + assert.equal(_private.matchesPackSku('PART-1', 'PART-10'), false); +}); + +test('pack inventory location prefers bins and falls back to positive-stock box ids', () => { + const locations = _private.buildPackLocationMap([ + { + skuId: 'PART-1-1', warehouseId: 1, binGroup: '成品区', bin: 'A-01', packId: 'BOX-1', qty: 2, + }, + { + skuId: 'PART-1-2', warehouseId: 1, binGroup: null, bin: '', packId: 'BOX-2', qty: 1, + }, + ], ['PART-1']); + + assert.equal(locations.get('PART-1'), '仓库 1 · 成品区 / A-01、仓库 1 · 箱号 BOX-2'); +});