Merge remote-tracking branch 'origin/main' into codex/mkh-alert-export-0917
远端 6c00977(PR #10 "show MKH component warehouse locations")与本地的 通知推送 / 导出 / 在途加工单改动改到了同一批文件,手工合并如下: 配件的仓位与库存统一到 loadComponentLocationData(),列表展开、单品明细、 导出配件明细三处共用(新增 loadComponentLocations 作为兼容入口): - 仓位四级兜底链:真货位 bin_group/bin → 箱号 pack_id → 库存正数量仓库 (未分箱)→ jst_sku.other_3 商品档案仓(暂无箱号) - 库存与仓位一次查询同时产出,两条查询都成功才写缓存(TTL 10 分钟) - 修正远端版本的仓库关联列:仓名必须用 jst_warehouseInfo.Id = p.wh_id; jst_packInventoryInfo.wms_co_id 全表 NULL,用它关联 85,152 行全部取不到仓名。 (库存表相反,必须用 w.wms_co_id = i.wms_co_id) - 前端口径说明同时保留补货预警/在途加工单与新仓位口径
This commit is contained in:
commit
897bbd2afd
@ -38,21 +38,26 @@
|
|||||||
// (无 SKU 条码),也不能直接当配件 BOM。
|
// (无 SKU 条码),也不能直接当配件 BOM。
|
||||||
//
|
//
|
||||||
// ⑤ 配件仓位 —— 业务库 rpt.jst_packInventoryInfo(聚水潭「箱及仓位库存」)
|
// ⑤ 配件仓位 —— 业务库 rpt.jst_packInventoryInfo(聚水潭「箱及仓位库存」)
|
||||||
// sku_id 可能带“-装箱序号”后缀(DNZ0019110N08V01-27),按基础配件编码匹配;
|
// sku_id 可能带“-装箱序号”后缀(DNZ0019110N08V01-27),按基础配件编码匹配。
|
||||||
// 仓库取 jst_warehouseInfo.name(wh_id → Id);有真货位(bin_group / bin)就显示货位,
|
// 取值是一条**四级兜底链**,宁可标注「未分箱 / 暂无箱号」也不误报具体货位:
|
||||||
// 没有则退回「仓库 · 箱号 pack_id」,两者都取不到时前端显示「未维护」。
|
// 1) 真货位 bin_group / bin → 「仓名 · bin_group / bin」
|
||||||
// · 列表展开(/detail)与导出配件明细共用 loadComponentLocations(),按编码缓存 10 分钟。
|
// 2) 箱号(净数量 > 0 的 pack_id) → 「仓名 · 箱号 pack_id」
|
||||||
|
// 3) 尚未分箱 → rpt.jst_inventoryInfo 正库存仓库 → 「仓名(未分箱)」
|
||||||
|
// 4) 连库存记录也没有 → jst_sku.other_3 商品档案仓库 → 「仓名(暂无箱号)」
|
||||||
|
// 取不到任何一层时前端显示「未维护」。
|
||||||
|
// · 列表展开(/detail)、导出配件明细共用 loadComponentLocations(),按编码缓存 10 分钟。
|
||||||
|
// ⚠️ 关联列别踩(2026-09-17 实测):仓名必须用 `jst_warehouseInfo.Id = p.wh_id`;
|
||||||
|
// 该表 `wms_co_id` **全表 NULL**,用 `w.wms_co_id = p.wms_co_id` 关联 85,152 行全部取不到仓名。
|
||||||
|
// (库存表相反:`jst_inventoryInfo` 必须用 `w.wms_co_id = i.wms_co_id`,194,053 行仅 316 行无仓名。)
|
||||||
// ⚠️ 2026-09-17 全量复核(别再重复排查):该表全表 85,152 行,但只有 **3 个箱**
|
// ⚠️ 2026-09-17 全量复核(别再重复排查):该表全表 85,152 行,但只有 **3 个箱**
|
||||||
// (914138566000001000 / …1003 / …4000)、2 个仓库(wh_id=1 常州简尚家居 85,145 行、
|
// (914138566000001000 / …1003 / …4000)、2 个仓库(wh_id=1 常州简尚家居 85,145 行、
|
||||||
// wh_id=4 简尚-新苏北仓 7 行);`bin` / `bin_group` **全表为空**,`wms_co_id` 全 NULL。
|
// wh_id=4 简尚-新苏北仓 7 行);`bin` / `bin_group` **全表为空**。
|
||||||
// 且表内容与 MKH 配件**零交集**:正式 BOM 的 784 个配件编码(DNZ0398xx / DNZ0409xx,
|
// 且表内容与 MKH 配件**零交集**:正式 BOM 的 784 个配件编码在该表一个都查不到;
|
||||||
// jst_sku.item_type=成品)在该表一个都查不到;表里存的是老型号半成品
|
// 表里存的是老型号半成品(DNZ0134xx / DNZ0199xx / DNZ0203xx,item_type=半成品)。
|
||||||
// (DNZ0134xx / DNZ0199xx / DNZ0203xx,item_type=半成品,名称如
|
// 结论:MKH 配件在第 1、2 层**必然落空**,实际显示值全部来自第 3 层
|
||||||
// 「G4号;云霜灰色+珍珠白色抽面;100/60/71.2」)。
|
// (jst_inventoryInfo 的实时库存仓库,如 DNZ039864N1410V01 →
|
||||||
// 结论:MKH 配件在**任何已接入数据源**里都没有货位/装箱数据
|
// 「简尚-崔桥工厂半成品包装项目仓(未分箱)」),需业务在聚水潭维护装箱货位后
|
||||||
// (jst_sku.bin / other_bin 全库 558,674 行全空;ERP 主库 JianSun.jst_packInventoryInfo
|
// 第 1、2 层才会生效。
|
||||||
// 同源同量;产品资料库 external.warehouse_locations_v1 仅 1 行)。
|
|
||||||
// → 仓位列只能显示「未维护」,需业务在聚水潭为配件维护装箱/货位后才会自动出现。
|
|
||||||
//
|
//
|
||||||
// ⑥ 配件库存 —— 业务库 rpt.jst_inventoryInfo 按 sku_id 汇总。
|
// ⑥ 配件库存 —— 业务库 rpt.jst_inventoryInfo 按 sku_id 汇总。
|
||||||
//
|
//
|
||||||
@ -104,16 +109,17 @@ const DEFAULT_PAGE_SIZE = 50;
|
|||||||
const MAX_PAGE_SIZE = 200;
|
const MAX_PAGE_SIZE = 200;
|
||||||
const BOM_CACHE_TTL_MS = 2 * 60 * 1000;
|
const BOM_CACHE_TTL_MS = 2 * 60 * 1000;
|
||||||
const OFFICIAL_BOM_DETAIL_CACHE_TTL_MS = 15 * 60 * 1000;
|
const OFFICIAL_BOM_DETAIL_CACHE_TTL_MS = 15 * 60 * 1000;
|
||||||
// 配件仓位(箱及仓位库)按编码缓存;单批编码数受 MSSQL 单语句 2100 参数上限约束
|
// 配件仓位 + 配件库存(箱及仓位库 + 库存表)按编码统一缓存;单批编码数受 MSSQL
|
||||||
// (每个编码占 2 个参数:精确匹配 + 带后缀前缀匹配),300 × 2 = 600 个参数,安全。
|
// 单语句 2100 参数上限约束(每个编码最多占 2 个参数:精确匹配 + 带后缀前缀匹配),
|
||||||
|
// 300 × 2 = 600 个参数,安全。
|
||||||
const COMPONENT_LOCATION_CACHE_TTL_MS = 10 * 60 * 1000;
|
const COMPONENT_LOCATION_CACHE_TTL_MS = 10 * 60 * 1000;
|
||||||
const COMPONENT_LOCATION_CHUNK_SIZE = 300;
|
const COMPONENT_LOCATION_CHUNK_SIZE = 300;
|
||||||
|
|
||||||
let businessBomCache = null;
|
let businessBomCache = null;
|
||||||
let officialBomIndexCache = null;
|
let officialBomIndexCache = null;
|
||||||
const officialBomDetailCache = new Map();
|
const officialBomDetailCache = new Map();
|
||||||
let productBomCache = null;
|
|
||||||
const componentLocationCache = new Map();
|
const componentLocationCache = new Map();
|
||||||
|
let productBomCache = null;
|
||||||
|
|
||||||
// MKH 商品编码 = 模块化成品
|
// MKH 商品编码 = 模块化成品
|
||||||
const DESIGNATED_WAREHOUSE = '简尚-崔桥工厂品牌仓';
|
const DESIGNATED_WAREHOUSE = '简尚-崔桥工厂品牌仓';
|
||||||
@ -1012,11 +1018,11 @@ function matchesPackSku(componentCode, packSkuCode) {
|
|||||||
function formatPackLocation(row) {
|
function formatPackLocation(row) {
|
||||||
const warehouseName = toText(row.warehouseName);
|
const warehouseName = toText(row.warehouseName);
|
||||||
const warehouseId = toText(row.warehouseId);
|
const warehouseId = toText(row.warehouseId);
|
||||||
|
// 仓库优先显示名称(wh_id → jst_warehouseInfo.Id → name),取不到时退回 id
|
||||||
|
const warehouse = warehouseName || (warehouseId ? `仓库 ${warehouseId}` : '');
|
||||||
const binGroup = toText(row.binGroup);
|
const binGroup = toText(row.binGroup);
|
||||||
const bin = toText(row.bin);
|
const bin = toText(row.bin);
|
||||||
const packId = toText(row.packId);
|
const packId = toText(row.packId);
|
||||||
// 仓库优先显示名称(wh_id → jst_warehouseInfo.name),取不到时退回 id
|
|
||||||
const warehouse = warehouseName || (warehouseId ? `仓库 ${warehouseId}` : '');
|
|
||||||
// 有真货位(bin_group / bin)才叫「仓位」;否则退回箱号
|
// 有真货位(bin_group / bin)才叫「仓位」;否则退回箱号
|
||||||
const position = [binGroup, bin].filter(Boolean).join(' / ')
|
const position = [binGroup, bin].filter(Boolean).join(' / ')
|
||||||
|| (packId ? `箱号 ${packId}` : '');
|
|| (packId ? `箱号 ${packId}` : '');
|
||||||
@ -1045,24 +1051,65 @@ function buildPackLocationMap(rows, componentCodes) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildInventoryWarehouseMap(rows, componentCodes) {
|
||||||
|
const positiveWarehouses = new Map(componentCodes.map((code) => [code, new Set()]));
|
||||||
|
const defaultWarehouses = new Map();
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const componentCode = componentCodes.find((code) => normalizeCode(code) === normalizeCode(row.skuId));
|
||||||
|
if (!componentCode) return;
|
||||||
|
|
||||||
|
const defaultWarehouse = toText(row.defaultWarehouse);
|
||||||
|
if (defaultWarehouse) defaultWarehouses.set(componentCode, defaultWarehouse);
|
||||||
|
|
||||||
|
const warehouseName = toText(row.warehouseName);
|
||||||
|
if (warehouseName && (toNumber(row.availableQty) ?? 0) > 0) {
|
||||||
|
positiveWarehouses.get(componentCode).add(warehouseName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = new Map();
|
||||||
|
componentCodes.forEach((code) => {
|
||||||
|
const warehouses = [...positiveWarehouses.get(code)];
|
||||||
|
if (warehouses.length > 0) {
|
||||||
|
result.set(code, `${warehouses.join('、')}(未分箱)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultWarehouse = defaultWarehouses.get(code);
|
||||||
|
if (defaultWarehouse) result.set(code, `${defaultWarehouse}(暂无箱号)`);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配件仓位批量查询(列表展开 / 单品明细 / 导出配件明细共用)。
|
* 配件仓位 + 配件库存批量查询(列表展开 / 单品明细 / 导出配件明细共用)。
|
||||||
* 数据源:rpt.jst_packInventoryInfo(箱及仓位库)+ rpt.jst_warehouseInfo(仓名)
|
* 数据源:rpt.jst_inventoryInfo(库存,及「未分箱 / 暂无箱号」兜底仓)、
|
||||||
|
* rpt.jst_packInventoryInfo(箱及仓位库)、jst_sku.other_3(商品档案仓)
|
||||||
* · 按基础配件编码匹配,兼容库内 sku_id 带「-装箱序号」后缀的形态
|
* · 按基础配件编码匹配,兼容库内 sku_id 带「-装箱序号」后缀的形态
|
||||||
|
* · 仓位走文件头 ⑤ 的四级兜底链:真货位 → 箱号 → 库存仓(未分箱)→ 档案仓(暂无箱号)
|
||||||
* · 单个配件跨多个箱时最多列出 10 个位置,超出补「等 N 个位置」
|
* · 单个配件跨多个箱时最多列出 10 个位置,超出补「等 N 个位置」
|
||||||
* · 结果按编码缓存(TTL 10 分钟);查询失败按 null 降级且不写缓存,下次重试
|
* · 结果按编码缓存(TTL 10 分钟);**两条查询都成功才写缓存**,
|
||||||
|
* 任一降级都不写,避免把不完整结果固化 10 分钟
|
||||||
|
* · 单元格超限由 COMPONENT_LOCATION_CHUNK_SIZE 分片,见常量处说明
|
||||||
|
* @returns {Promise<{stockMap: Map<string, number>, locationMap: Map<string, string>}>}
|
||||||
*/
|
*/
|
||||||
async function loadComponentLocations(codes = []) {
|
async function loadComponentLocationData(codes = []) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const unique = Array.from(new Set(codes.map((code) => toText(code)).filter(Boolean)));
|
const unique = Array.from(new Set(codes.map((code) => toText(code)).filter(Boolean)));
|
||||||
const result = new Map();
|
const stockMap = new Map();
|
||||||
|
const locationMap = new Map();
|
||||||
const missing = [];
|
const missing = [];
|
||||||
unique.forEach((code) => {
|
unique.forEach((code) => {
|
||||||
const hit = componentLocationCache.get(code);
|
const hit = componentLocationCache.get(normalizeCode(code));
|
||||||
if (hit && hit.expiresAt > now) result.set(code, hit.location);
|
if (hit && hit.expiresAt > now) {
|
||||||
else missing.push(code);
|
stockMap.set(code, hit.stock ?? 0);
|
||||||
|
if (hit.location) locationMap.set(code, hit.location);
|
||||||
|
} else {
|
||||||
|
missing.push(code);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (missing.length === 0) return result;
|
if (missing.length === 0) return { stockMap, locationMap };
|
||||||
|
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
for (let i = 0; i < missing.length; i += COMPONENT_LOCATION_CHUNK_SIZE) {
|
for (let i = 0; i < missing.length; i += COMPONENT_LOCATION_CHUNK_SIZE) {
|
||||||
@ -1071,6 +1118,46 @@ async function loadComponentLocations(codes = []) {
|
|||||||
|
|
||||||
// 组内串行(单个接口最多同时打两条查询,避免拖垮连接池)
|
// 组内串行(单个接口最多同时打两条查询,避免拖垮连接池)
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
|
const packLocations = new Map(); // 第 1、2 层:真货位 / 箱号
|
||||||
|
let fallbackLocations = new Map(); // 第 3、4 层:未分箱 / 暂无箱号
|
||||||
|
let stockQuerySucceeded = false;
|
||||||
|
let packQuerySucceeded = false;
|
||||||
|
|
||||||
|
// ① 配件库存 + 仓名 —— 一次查询同时供 stockMap 与兜底仓位使用
|
||||||
|
try {
|
||||||
|
const parameters = {};
|
||||||
|
const placeholders = chunk.map((code, index) => {
|
||||||
|
parameters[`c${index}`] = code;
|
||||||
|
return `@c${index}`;
|
||||||
|
}).join(', ');
|
||||||
|
const stockRows = await cloudDatabase.query(
|
||||||
|
`SELECT s.sku_id AS skuId,
|
||||||
|
s.other_3 AS defaultWarehouse,
|
||||||
|
w.name AS warehouseName,
|
||||||
|
SUM(COALESCE(i.qty, 0)) AS qty,
|
||||||
|
SUM(COALESCE(i.qty, 0)
|
||||||
|
- COALESCE(i.order_lock, 0)
|
||||||
|
- COALESCE(i.pick_lock, 0)) AS availableQty
|
||||||
|
FROM dbo.jst_sku s WITH (NOLOCK)
|
||||||
|
LEFT JOIN rpt.jst_inventoryInfo i WITH (NOLOCK) ON i.sku_id = s.sku_id
|
||||||
|
LEFT JOIN rpt.jst_warehouseInfo w WITH (NOLOCK) ON w.wms_co_id = i.wms_co_id
|
||||||
|
WHERE s.sku_id IN (${placeholders})
|
||||||
|
GROUP BY s.sku_id, s.other_3, i.wms_co_id, w.name`,
|
||||||
|
parameters,
|
||||||
|
{ requestTimeout: 180000 }
|
||||||
|
);
|
||||||
|
stockRows.forEach((row) => {
|
||||||
|
const code = chunk.find((candidate) => normalizeCode(candidate) === normalizeCode(row.skuId));
|
||||||
|
if (!code) return;
|
||||||
|
stockMap.set(code, (stockMap.get(code) || 0) + (toNumber(row.qty) ?? 0));
|
||||||
|
});
|
||||||
|
fallbackLocations = buildInventoryWarehouseMap(stockRows, chunk);
|
||||||
|
stockQuerySucceeded = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[MkhBom] 配件库存查询降级:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ② 箱及仓位(第 1、2 层)—— 仓名必须用 jst_warehouseInfo.Id = p.wh_id,见文件头 ⑤
|
||||||
try {
|
try {
|
||||||
const params = {};
|
const params = {};
|
||||||
const conditions = chunk.map((code, index) => {
|
const conditions = chunk.map((code, index) => {
|
||||||
@ -1078,7 +1165,7 @@ async function loadComponentLocations(codes = []) {
|
|||||||
params[`lp${index}`] = `${code}-%`;
|
params[`lp${index}`] = `${code}-%`;
|
||||||
return `(p.sku_id = @lc${index} OR p.sku_id LIKE @lp${index})`;
|
return `(p.sku_id = @lc${index} OR p.sku_id LIKE @lp${index})`;
|
||||||
});
|
});
|
||||||
const rows = await cloudDatabase.query(
|
const locRows = await cloudDatabase.query(
|
||||||
`SELECT p.sku_id AS skuId,
|
`SELECT p.sku_id AS skuId,
|
||||||
p.pack_id AS packId,
|
p.pack_id AS packId,
|
||||||
p.wh_id AS warehouseId,
|
p.wh_id AS warehouseId,
|
||||||
@ -1094,21 +1181,37 @@ async function loadComponentLocations(codes = []) {
|
|||||||
params,
|
params,
|
||||||
{ requestTimeout: 120000 }
|
{ requestTimeout: 120000 }
|
||||||
);
|
);
|
||||||
const byCode = buildPackLocationMap(rows, chunk);
|
buildPackLocationMap(locRows, chunk).forEach((location, code) => {
|
||||||
const expiresAt = Date.now() + COMPONENT_LOCATION_CACHE_TTL_MS;
|
packLocations.set(code, location);
|
||||||
chunk.forEach((code) => {
|
|
||||||
const location = byCode.get(code) || null;
|
|
||||||
componentLocationCache.set(code, { location, expiresAt });
|
|
||||||
result.set(code, location);
|
|
||||||
});
|
});
|
||||||
|
packQuerySucceeded = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[MkhBom] 配件仓位查询降级:', error.message);
|
console.warn('[MkhBom] 配件箱号/仓位查询降级:', error.message);
|
||||||
chunk.forEach((code) => {
|
|
||||||
if (!result.has(code)) result.set(code, null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expiresAt = Date.now() + COMPONENT_LOCATION_CACHE_TTL_MS;
|
||||||
|
chunk.forEach((code) => {
|
||||||
|
const location = packLocations.get(code) || fallbackLocations.get(code) || null;
|
||||||
|
if (stockQuerySucceeded && packQuerySucceeded) {
|
||||||
|
componentLocationCache.set(normalizeCode(code), {
|
||||||
|
stock: stockMap.get(code) ?? 0,
|
||||||
|
location,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (location) locationMap.set(code, location);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
|
return { stockMap, locationMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容入口:只要仓位映射(列表展开 / 导出配件明细)。
|
||||||
|
*/
|
||||||
|
async function loadComponentLocations(codes = []) {
|
||||||
|
const { locationMap } = await loadComponentLocationData(codes);
|
||||||
|
return locationMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1238,37 +1341,9 @@ async function getMkhBomDetail(mkhSkuId) {
|
|||||||
const components = matched?.components || [];
|
const components = matched?.components || [];
|
||||||
|
|
||||||
const codes = components.map((item) => item.barcode).filter(Boolean);
|
const codes = components.map((item) => item.barcode).filter(Boolean);
|
||||||
const stockMap = new Map();
|
// 与列表展开 / 导出配件明细走同一个批量加载器 ——
|
||||||
const locationMap = new Map();
|
// 含仓名解析、文件头 ⑤ 的四级兜底链、库存与仓位的统一 10 分钟缓存
|
||||||
|
const { stockMap, locationMap } = await loadComponentLocationData(codes);
|
||||||
if (codes.length > 0) {
|
|
||||||
const placeholders = codes.map((_, index) => `@c${index}`).join(', ');
|
|
||||||
const codeParams = Object.fromEntries(codes.map((code, index) => [`c${index}`, code]));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stockRows = await cloudDatabase.query(
|
|
||||||
`SELECT i.sku_id AS skuId, SUM(i.qty) AS qty
|
|
||||||
FROM dbo.jst_inventoryInfo i
|
|
||||||
WHERE i.sku_id IN (${placeholders})
|
|
||||||
GROUP BY i.sku_id`,
|
|
||||||
codeParams,
|
|
||||||
{ requestTimeout: 180000 }
|
|
||||||
);
|
|
||||||
stockRows.forEach((row) => stockMap.set(toText(row.skuId), toNumber(row.qty) ?? 0));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[MkhBom] 配件库存查询降级:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 与列表展开 / 导出配件明细走同一个批量加载器(含仓名解析与 10 分钟缓存)
|
|
||||||
const located = await loadComponentLocations(codes);
|
|
||||||
located.forEach((location, code) => {
|
|
||||||
locationMap.set(code, location);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[MkhBom] 配件箱号/仓位查询降级:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = components.map((component, index) => ({
|
const items = components.map((component, index) => ({
|
||||||
index: index + 1,
|
index: index + 1,
|
||||||
@ -1460,8 +1535,11 @@ module.exports = {
|
|||||||
matchesPackSku,
|
matchesPackSku,
|
||||||
formatPackLocation,
|
formatPackLocation,
|
||||||
buildPackLocationMap,
|
buildPackLocationMap,
|
||||||
|
buildInventoryWarehouseMap,
|
||||||
loadComponentLocations,
|
loadComponentLocations,
|
||||||
|
loadComponentLocationData,
|
||||||
COMPONENT_LOCATION_CHUNK_SIZE,
|
COMPONENT_LOCATION_CHUNK_SIZE,
|
||||||
|
COMPONENT_LOCATION_CACHE_TTL_MS,
|
||||||
buildReplenishMetrics,
|
buildReplenishMetrics,
|
||||||
loadPendingWorkOrderIndex,
|
loadPendingWorkOrderIndex,
|
||||||
parseFlag,
|
parseFlag,
|
||||||
|
|||||||
@ -155,14 +155,29 @@ test('pack inventory locations match a BOM component and its package suffixes',
|
|||||||
test('pack inventory location prefers bins and falls back to positive-stock box ids', () => {
|
test('pack inventory location prefers bins and falls back to positive-stock box ids', () => {
|
||||||
const locations = _private.buildPackLocationMap([
|
const locations = _private.buildPackLocationMap([
|
||||||
{
|
{
|
||||||
skuId: 'PART-1-1', warehouseId: 1, binGroup: '成品区', bin: 'A-01', packId: 'BOX-1', qty: 2,
|
skuId: 'PART-1-1', warehouseId: 1, warehouseName: '半成品仓', binGroup: '成品区', bin: 'A-01', packId: 'BOX-1', qty: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
skuId: 'PART-1-2', warehouseId: 1, binGroup: null, bin: '', packId: 'BOX-2', qty: 1,
|
skuId: 'PART-1-2', warehouseId: 1, binGroup: null, bin: '', packId: 'BOX-2', qty: 1,
|
||||||
},
|
},
|
||||||
], ['PART-1']);
|
], ['PART-1']);
|
||||||
|
|
||||||
assert.equal(locations.get('PART-1'), '仓库 1 · 成品区 / A-01、仓库 1 · 箱号 BOX-2');
|
assert.equal(locations.get('PART-1'), '半成品仓 · 成品区 / A-01、仓库 1 · 箱号 BOX-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('inventory warehouse fallback distinguishes unboxed stock from a warehouse with no box record', () => {
|
||||||
|
const locations = _private.buildInventoryWarehouseMap([
|
||||||
|
{
|
||||||
|
skuId: 'PART-1', defaultWarehouse: '默认半成品仓', warehouseName: '实际库存仓', availableQty: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skuId: 'PART-2', defaultWarehouse: '默认半成品仓', warehouseName: '实际库存仓', availableQty: 0,
|
||||||
|
},
|
||||||
|
], ['PART-1', 'PART-2', 'PART-3']);
|
||||||
|
|
||||||
|
assert.equal(locations.get('PART-1'), '实际库存仓(未分箱)');
|
||||||
|
assert.equal(locations.get('PART-2'), '默认半成品仓(暂无箱号)');
|
||||||
|
assert.equal(locations.has('PART-3'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 补货预警:近 7 日日销 > 0 且 现有库存 < 3 天日销 ────────────────
|
// ── 补货预警:近 7 日日销 > 0 且 现有库存 < 3 天日销 ────────────────
|
||||||
|
|||||||
@ -182,7 +182,7 @@ const PrintPreview = ({ data, onClose }) => {
|
|||||||
<div className="mkh-print-foot">
|
<div className="mkh-print-foot">
|
||||||
<span>
|
<span>
|
||||||
MKH 商品编码取自指定仓库商品主档;配件优先取聚水潭商品 BOM,业务组合 BOM 与产品资料系统作为补充;
|
MKH 商品编码取自指定仓库商品主档;配件优先取聚水潭商品 BOM,业务组合 BOM 与产品资料系统作为补充;
|
||||||
配件仓位取库位主数据,未维护时显示「未维护」。
|
配件仓位优先取包装箱与具体仓位,尚未分箱时显示库存仓库或商品档案仓库。
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -656,6 +656,8 @@ const MkhBomData = () => {
|
|||||||
<Text type="secondary" className="mkh-note">
|
<Text type="secondary" className="mkh-note">
|
||||||
数据口径:MKH 商品编码仅取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”的成品(当前 4408 条);
|
数据口径:MKH 商品编码仅取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”的成品(当前 4408 条);
|
||||||
配件明细与数量优先取自聚水潭商品 BOM,业务中台组合商品 BOM 与产品资料系统数据库中维护的配件作为补充。
|
配件明细与数量优先取自聚水潭商品 BOM,业务中台组合商品 BOM 与产品资料系统数据库中维护的配件作为补充。
|
||||||
|
无指定仓库商品档案的 BOM 组合编码不会进入列表;配件仓位优先显示箱号或具体仓位,
|
||||||
|
尚未分箱时显示实际库存仓库,没有库存记录时显示商品档案仓库。
|
||||||
<br />
|
<br />
|
||||||
补货预警:近7日日销取聚水潭库存监控快照(监控日 {data.monitorDate || '—'}),现有库存优先取品牌仓实时库存、
|
补货预警:近7日日销取聚水潭库存监控快照(监控日 {data.monitorDate || '—'}),现有库存优先取品牌仓实时库存、
|
||||||
该仓无记录时回退监控快照库存;可用天数 = 现有库存 ÷ 近7日日销,低于 3 天即预警,缺货单独标识;
|
该仓无记录时回退监控快照库存;可用天数 = 现有库存 ÷ 近7日日销,低于 3 天即预警,缺货单独标识;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user