fix: 从包装库存读取 BOM 配件仓位 #8
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user