feat: 日销口径由近 7 天改为近 15 天(MKH 补货预警 + 模块化库存监控)
业务要求「近7日日销」改「近30天日销」。实测业务库与产品资料库均无 30 天日销,
已实现的最长周期是 pm_stockMonitor.fifteenAvgSale(15 天),经与业务确认改用 15 天。
为什么没有 30 天(2026-09-17 实测,避免重复排查):
- rpt.pm_stockMonitor 只有 threeAvgSale(3天)/sevenAvgSale(7天)/fifteenAvgSale(15天),
且整表只保留 1 天快照(2026-09-16),攒不出 30 天历史
- rpt.dws_sn_sales_full_info_di(stat_date+skuid+goods_count,新鲜到 09-15)
里 MKH 商品 0 个;目标仓 794 个 SKU 命中也是 0
- rpt.dws_order_sales_cost_detail_di 停在 2025-04(历史归档)
- rpt.jst_salesOutboundInfo 只有出库单头,无 SKU 明细;业务库无出库明细表
- rpt.op_BarcodeSalePrice30dDaily 名字带 30d,但只有 salePrice 没有销量
改动:
- backend/src/services/mkhBomService.js
· SALE_WINDOW_DAYS 7 → 15,新增 DAILY_SALE_COLUMN='fifteenAvgSale',
监控快照 SQL 按该常量插值取数(以后改口径只改常量)
· buildReplenishMetrics 入参 sevenAvgSale → avgSale(语义与周期解耦)
· 文件头 ⑦ 补「为什么是 15 天」的完整排查结论;⑦ / ⑩ / ⑪ 实测数字按新口径更新
- backend/src/services/modularStockMonitorService.js
· 取数列与 API 字段名 sevenAvgSale/avgShip7d → fifteenAvgSale/avgShip15d
(字段名同步改,避免名不副实)
- frontend/src/pages/MkhBomData/index.jsx
· 列头与口径说明「近7日」→「近15天」;动态文案兜底 7 → 15
- frontend/src/pages/ModularStockMonitor/index.jsx
· 列头「近7日日均发货量」→「近15天日均发货量」,dataIndex 同步改
· 口径说明补充:可用天数仍沿用 jsUseDays(源系统口径),与日销并非同一时间窗
影响(同一份 09-16 快照,7 天 vs 15 天):
有销商品 114 → 170 周期拉长,更多商品进得了「有销」集合
补货预警 25 → 19 反而变少:15 天日销普遍低于 7 天(近期销量走低),
可用天数 = 库存 ÷ 日销 随之变大,跌破 3 天的变少
缺货 9 → 10
已排产 11 → 13
订单驱动 14 → 8 更多商品自己有日销,不再需要靠订单兜底
配件不足 10 → 12
「预警变少」是口径变化的正常结果,不是 bug。
验证:
- 后端语法检查 151 个文件通过
- mkhBomService + mkhReplenishAlertJob 单测 52/52 通过
- 后端全量 334 tests / 333 pass(唯一失败是 resilientSecretStore 的
Windows 文件权限断言,与本改动无关的环境固有问题)
- 接口实测:total 4223 / warningCount 19 / outOfStockCount 10 /
saleWindowDays 15 / componentShortageCount 12
This commit is contained in:
parent
06952d8a04
commit
3e02c3e393
@ -64,19 +64,40 @@
|
||||
// ⑥ 配件库存 —— 业务库 rpt.jst_inventoryInfo 按 sku_id 汇总(**商品**库存,不是配件仓位)。
|
||||
//
|
||||
// ── ⑦ 补货预警(2026-09-17 业务确认口径)──────────────────────────
|
||||
// 规则:近 7 日日销 > 0 且 现有库存 < 3 天日销 → 预警补货
|
||||
// · 近7日日销 rpt.pm_stockMonitor.sevenAvgSale
|
||||
// 规则:近 15 天日销 > 0 且 现有库存 < 3 天日销 → 预警补货
|
||||
// · 近15天日销 rpt.pm_stockMonitor.fifteenAvgSale
|
||||
// (warehouse = '简尚-崔桥工厂品牌仓',按 date 取最新一条快照)
|
||||
// · 现有库存 rpt.jst_inventoryInfo(wms_co_id = 15870495 品牌仓)实时库存;
|
||||
// 该仓对 MKH 成品只覆盖约 256 / 4408 条(2026-09-16 快照;成品总数口径见 ①),
|
||||
// 缺失时回退监控快照 jsStock
|
||||
// · 可用天数 = 现有库存 / 近7日日销
|
||||
// · 建议补货量 = ceil(3 × 近7日日销 − 现有库存),不足 0 取 0
|
||||
// ⚠️ 刻意**不用**源系统的 jsUseDays 列:实测它与 jsStock / sevenAvgSale 不同源
|
||||
// · 可用天数 = 现有库存 / 近15天日销
|
||||
// · 建议补货量 = ceil(3 × 近15天日销 − 现有库存),不足 0 取 0
|
||||
// ⚠️ 刻意**不用**源系统的 jsUseDays 列:实测它与 jsStock / fifteenAvgSale 不同源
|
||||
// (同一 SKU 可差 20 天以上,如 MKHDNZ0409260700488 库存 44 / 日销 0.67,
|
||||
// 除法 65.7 天而 jsUseDays 为 44),本页口径以业务给定的「近7日日销」为准。
|
||||
// 实测(2026-09-16 快照,当时成品口径尚未收紧为「正式 BOM 父项」):
|
||||
// 4408 个 MKH 成品中 114 个有日销、13 个可用天数 < 3 天。
|
||||
// 除法 65.7 天而 jsUseDays 为 44),本页口径以业务给定的「近15天日销」为准。
|
||||
//
|
||||
// ── 为什么是 15 天而不是业务最初要求的 30 天 ──
|
||||
// 2026-09-17 业务要求「近7日日销」改「近30天日销」,但实测全库**没有 30 天日销**:
|
||||
// · pm_stockMonitor 只有 threeAvgSale(3天) / sevenAvgSale(7天) / fifteenAvgSale(15天),
|
||||
// 且整表**只保留 1 天快照**(2026-09-16),攒不出 30 天历史
|
||||
// · rpt.dws_sn_sales_full_info_di(stat_date+skuid+goods_count,数据新鲜到 09-15)
|
||||
// 里 MKH 商品 **0 个**;目标仓 794 个 SKU 命中也是 0
|
||||
// · rpt.dws_order_sales_cost_detail_di 停在 2025-04(历史归档)
|
||||
// · rpt.jst_salesOutboundInfo 只有出库**单头**,无 SKU 明细;业务库无出库明细表
|
||||
// · rpt.op_BarcodeSalePrice30dDaily 名字带 30d,但只有 salePrice **没有销量**
|
||||
// → 经与业务确认,改用十五天口径 fifteenAvgSale(已实现的最长周期)。
|
||||
// ⚠️ 若日后上游补了 30 天字段,只需改 loadReplenishIndex 里的列名 + DAILY_SALE_DAYS。
|
||||
//
|
||||
// 实测(2026-09-16 品牌仓快照,当时成品口径尚未收紧为「正式 BOM 父项」):
|
||||
// 4408 个 MKH 成品中,7 天有销 114 个、15 天有销 170 个
|
||||
// 实测(2026-09-17 切换当天,同一份 09-16 快照,7 天 vs 15 天):
|
||||
// 有销商品 114 → 170 ← 周期拉长,更多商品进得了「有销」集合
|
||||
// 补货预警 25 → 19 ← 反而变少:15 天日销普遍低于 7 天(近期销量走低),
|
||||
// 可用天数 = 库存 ÷ 日销 随之变大,跌破 3 天的变少
|
||||
// 缺货 9 → 10
|
||||
// 已排产 11 → 13
|
||||
// 订单驱动 14 → 8 ← 更多商品自己有日销了,不再需要靠订单兜底
|
||||
// ⚠️ 「预警变少」不是 bug,是口径变化的正常结果;若业务想要 30 天,需上游先补数。
|
||||
//
|
||||
// ── ⑧ 在途加工单(2026-09-17 业务确认口径)────────────────────────
|
||||
// 规则:**只要该 MKH 商品存在未完成的加工单,就不再列入补货预警**
|
||||
@ -107,7 +128,7 @@
|
||||
// 响应会膨胀到几十 MB,而商品清单本身用不到明细。
|
||||
//
|
||||
// ── ⑩ 待发货订单(2026-09-17 业务确认口径)────────────────────────
|
||||
// 规则:**近 7 日日销 = 0、但当前存在未发货订单的 MKH 成品,同样列入补货预警**。
|
||||
// 规则:**近 15 天日销 = 0、但当前存在未发货订单的 MKH 成品,同样列入补货预警**。
|
||||
// 背景:日销取自监控快照(日更)的滚动均值,新上的 MKH 成品「昨天没卖、今天来了订单」
|
||||
// 时快照仍是 0,会被判成「无销量」而完全排除在预警之外,导致漏备货。
|
||||
// · 数据源:业务库 rpt.jst_Orders(订单**主表**,实时更新,实测最新到今天)
|
||||
@ -128,8 +149,10 @@
|
||||
// 缺货时记 stockDays = 0(保证排在最前),预警(库存已够发)时记 null。
|
||||
// ⚠️ 与「在途加工单」的优先级:**先判已排产**(见 ⑧)——
|
||||
// 无日销但有订单、且工厂已开加工单的商品,仍标「已排产」不预警,避免重复提示。
|
||||
// 实测(2026-09-17):未发货且含 MKH 成品的订单 75 单 → 48 个成品编码 / 71 件,
|
||||
// 实测(2026-09-17,7 天口径下):未发货且含 MKH 成品的订单 75 单 → 48 个成品编码 / 71 件,
|
||||
// 其中 30 个本来就有日销、**18 个是本次新增**(日销=0,靠订单才进预警)。
|
||||
// ⚠️ 改 15 天口径后实测:订单驱动从 14 个降到 **8 个**
|
||||
// (日销基数 114 → 170,原本靠订单兜底的商品自己有日销了),与预期一致。
|
||||
//
|
||||
// ── ⑪ 配件不足 / 可组装数(2026-09-17 业务确认口径)────────────────
|
||||
// 规则:**缺货 / 预警的 MKH 成品,若其配件现有库存「可组装数 = 0」,则提示
|
||||
@ -152,8 +175,8 @@
|
||||
// · 判定范围:**缺货 + 预警都判**(业务确认),不改变原有预警级别与排序,
|
||||
// 只额外打标记;「已排产」商品本来就不预警,不参与判定。
|
||||
// · 无 BOM 配件明细的商品 → 可组装数未知(组件缺失 ≠ 配件不足),不做判定。
|
||||
// 实测(2026-09-17,并入最新 main 后复核):预警 25 个(缺货 9 + 预警 16)中 **10 个可组装数 = 0**
|
||||
// (缺货 6 + 预警 4);扣加工单占用前后仅 1 个商品变化(185 → 180 台),
|
||||
// 实测(2026-09-17,15 天口径):预警 19 个(缺货 10 + 预警 9)中 **12 个可组装数 = 0**
|
||||
// (缺货 7 + 预警 5);扣加工单占用前后仅 1 个商品变化(185 → 180 台),
|
||||
// 说明占用规模小但口径上必须扣。
|
||||
// ⚠️ 配件库存查询失败时**不做任何判定**(宁可少标也不误标),
|
||||
// 接口透出 componentStockDegraded=true。
|
||||
@ -192,7 +215,13 @@ const MKH_WHERE = `s.sku_id LIKE N'MKH%'
|
||||
// ── 补货预警参数(见文件头 ⑦)────────────────────────────────────
|
||||
// 品牌仓 wms_co_id(rpt.jst_warehouseInfo 查得)
|
||||
const BRAND_WMS_CO_ID = 15870495;
|
||||
const SALE_WINDOW_DAYS = 7;
|
||||
// 日销统计窗口(天)。见文件头 ⑦:业务最初要 30 天,但全库无 30 天日销数据,
|
||||
// 实测最长只有 fifteenAvgSale(15 天),经业务确认改用 15 天。
|
||||
// ⚠️ SALE_WINDOW_DAYS 决定页面文案与口径展示,DAILY_SALE_COLUMN 决定实际取数列,
|
||||
// 两者必须成对修改;下方 SQL 已按 DAILY_SALE_COLUMN 插值,改常量即可生效。
|
||||
const SALE_WINDOW_DAYS = 15;
|
||||
// 日销取数列(rpt.pm_stockMonitor)—— 必须与 SALE_WINDOW_DAYS 保持一致
|
||||
const DAILY_SALE_COLUMN = 'fifteenAvgSale';
|
||||
// 预警阈值:现有库存可用天数低于 3 天即需补货
|
||||
const REPLENISH_DAYS_THRESHOLD = 3;
|
||||
|
||||
@ -299,8 +328,8 @@ const SCOPES = Object.freeze({ WARNING: 'warning', SOLD: 'sold', ALL: 'all' });
|
||||
|
||||
/**
|
||||
* 解析展示范围:
|
||||
* warning —— 仅需补货(近7日日销 > 0 且 可用天数 < 3 天)
|
||||
* sold —— 有近7日日销的商品
|
||||
* warning —— 仅需补货(近15天日销 > 0 且 可用天数 < 3 天)
|
||||
* sold —— 有近15天日销的商品
|
||||
* all —— 全部 MKH 成品
|
||||
* 兼容旧参数 warningOnly=1(等价 scope=warning)。
|
||||
*/
|
||||
@ -776,13 +805,13 @@ const sumQty = (components) => components.reduce((sum, item) => sum + (toNumber(
|
||||
* 计算单个 MKH 成品的补货预警指标(纯函数,便于回归测试)。
|
||||
*
|
||||
* 口径(见文件头 ⑦ + ⑩):
|
||||
* ① 近7日日销 > 0 且 现有库存 < 3 天日销 → 预警
|
||||
* ② 近7日日销 = 0 但存在未发货订单 → 订单驱动预警(见文件头 ⑩)
|
||||
* ① 近15天日销 > 0 且 现有库存 < 3 天日销 → 预警
|
||||
* ② 近15天日销 = 0 但存在未发货订单 → 订单驱动预警(见文件头 ⑩)
|
||||
* 两者都不满足 → 无销量,不参与预警。
|
||||
* 已有未完成加工单的商品一律标「已排产」,两种口径都不预警。
|
||||
*
|
||||
* @param {object} row
|
||||
* row.sevenAvgSale 近7日日均发货量
|
||||
* row.avgSale 近15天日均发货量(取自 rpt.pm_stockMonitor.fifteenAvgSale)
|
||||
* row.realtimeStock 品牌仓实时库存(可为 null,表示该仓无记录)
|
||||
* row.monitorStock 监控快照库存(fallback)
|
||||
* row.safetyDays 源系统安全天数(仅展示)
|
||||
@ -792,7 +821,7 @@ const sumQty = (components) => components.reduce((sum, item) => sum + (toNumber(
|
||||
* row.pendingOrderQty 待发货订单量(未发货订单件数)
|
||||
*/
|
||||
function buildReplenishMetrics(row = {}) {
|
||||
const dailySale = Math.max(0, toNumber(row.sevenAvgSale) ?? 0);
|
||||
const dailySale = Math.max(0, toNumber(row.avgSale) ?? 0);
|
||||
const realtimeStock = toNumber(row.realtimeStock);
|
||||
const monitorStock = toNumber(row.monitorStock) ?? 0;
|
||||
const hasRealtime = realtimeStock !== null;
|
||||
@ -828,7 +857,7 @@ function buildReplenishMetrics(row = {}) {
|
||||
monitorDate: toDateText(row.monitorDate),
|
||||
};
|
||||
|
||||
// 既无近7日日销、也无未发货订单 → 不参与预警
|
||||
// 既无近15天日销、也无未发货订单 → 不参与预警
|
||||
if (dailySale <= 0 && !orderDriven) {
|
||||
return {
|
||||
...base,
|
||||
@ -1179,7 +1208,7 @@ async function loadReplenishIndex() {
|
||||
const pending = Promise.all([
|
||||
// 监控快照:同一天只有一行,按 date DESC 取首条即可
|
||||
cloudDatabase.query(
|
||||
`SELECT m.sku_id AS skuId, m.sevenAvgSale AS sevenAvgSale, m.jsStock AS monitorStock,
|
||||
`SELECT m.sku_id AS skuId, m.${DAILY_SALE_COLUMN} AS fifteenAvgSale, m.jsStock AS monitorStock,
|
||||
m.safetyDays AS safetyDays, m.suggestStock AS suggestStock, m.date AS monitorDate
|
||||
FROM dbo.pm_stockMonitor m
|
||||
WHERE m.warehouse = @brandWarehouse AND m.sku_id LIKE N'MKH%'
|
||||
@ -1218,7 +1247,8 @@ async function loadReplenishIndex() {
|
||||
seenMonitor.add(key);
|
||||
if (!monitorDate) monitorDate = toDateText(row.monitorDate);
|
||||
bySku.set(key, buildReplenishMetrics({
|
||||
sevenAvgSale: row.sevenAvgSale,
|
||||
// 日销取数见文件头 ⑦:十五天口径(业务要 30 天但全库无此数据)
|
||||
avgSale: row.fifteenAvgSale,
|
||||
// 实时库存缺失(而非为 0)时才回退监控快照
|
||||
realtimeStock: latestStock.has(key) ? latestStock.get(key) : null,
|
||||
monitorStock: row.monitorStock,
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
// 需求来源:《模块化库存监控.xlsx》
|
||||
// 一级功能:模块化库存监控 二级功能:配件库存监控
|
||||
// 表头:图片 | 预警 | SKU编码 | 名称 | 当日入库库存 | 当日出库库存 | 实时库存 |
|
||||
// 仓位 | 近7日日均发货量 | 库存可用天数 | 所在仓库
|
||||
// 仓位 | 近15天日均发货量 | 库存可用天数 | 所在仓库
|
||||
// 数据范围:仅取「简尚-崔桥工厂半成品包装项目仓」仓库下的 SKU 编码
|
||||
//
|
||||
// 数据来源(业务库 JSReportReplica / schema=rpt,2026-09-12 实测确认):
|
||||
@ -14,7 +14,7 @@
|
||||
// 库存 rpt.jst_inventoryInfo —— 实时库存 qty;按 wms_co_id 区分仓库
|
||||
// 目标仓库 wms_co_id = 15718196(rpt.jst_warehouseInfo 查得)
|
||||
// 监控 rpt.pm_stockMonitor —— 日更快照,目标仓每天 794 行,覆盖全部 SKU:
|
||||
// sevenAvgSale 近7日日均发货量
|
||||
// fifteenAvgSale 近15天日均发货量(2026-09-17 由 7 天改 15 天,见下)
|
||||
// jsUseDays 库存可用天数
|
||||
// safetyDays 安全天数(默认 15)
|
||||
// suggestStock 建议备货量
|
||||
@ -76,7 +76,7 @@ const SORTABLE_FIELDS = Object.freeze({
|
||||
productName: 's.name',
|
||||
category: 's.category',
|
||||
realtimeStock: 'realtimeStock',
|
||||
avgShip7d: 'm.sevenAvgSale',
|
||||
avgShip15d: 'm.fifteenAvgSale',
|
||||
stockDays: 'm.jsUseDays',
|
||||
warningLevel: 'warningOrder',
|
||||
mkhStatus: 'mkhStatus',
|
||||
@ -108,7 +108,7 @@ OUTER APPLY (
|
||||
ORDER BY i.updateTime DESC
|
||||
) inv
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 m2.sevenAvgSale, m2.jsUseDays, m2.safetyDays, m2.suggestStock,
|
||||
SELECT TOP 1 m2.fifteenAvgSale, m2.jsUseDays, m2.safetyDays, m2.suggestStock,
|
||||
m2.date, m2.jsStock, m2.nearSecondSale
|
||||
FROM dbo.pm_stockMonitor m2
|
||||
WHERE m2.sku_id = s.sku_id AND m2.warehouse = @warehouseName
|
||||
@ -352,7 +352,7 @@ const mapRow = (row = {}) => {
|
||||
outQtyToday: toNumber(row.outQtyToday) ?? 0,
|
||||
realtimeStock,
|
||||
location: toText(row.location),
|
||||
avgShip7d: toNumber(row.avgShip7d),
|
||||
avgShip15d: toNumber(row.avgShip15d),
|
||||
stockDays,
|
||||
safetyDays,
|
||||
suggestStock: toNumber(row.suggestStock),
|
||||
@ -368,7 +368,7 @@ const buildProjection = () => `
|
||||
ISNULL(pin.inQty, 0) AS inQtyToday,
|
||||
ISNULL(m.nearSecondSale, 0) AS outQtyToday,
|
||||
COALESCE(NULLIF(LTRIM(RTRIM(s.bin)), N''), NULLIF(LTRIM(RTRIM(s.other_bin)), N'')) AS location,
|
||||
m.sevenAvgSale AS avgShip7d,
|
||||
m.fifteenAvgSale AS avgShip15d,
|
||||
m.jsUseDays AS stockDays,
|
||||
m.safetyDays AS safetyDays,
|
||||
m.suggestStock AS suggestStock,
|
||||
|
||||
@ -190,17 +190,17 @@ test('pack inventory without bin/bin_group falls back to the box id, never to a
|
||||
assert.equal(locations.has('PART-2'), false);
|
||||
});
|
||||
|
||||
// ── 补货预警:近 7 日日销 > 0 且 现有库存 < 3 天日销 ────────────────
|
||||
// ── 补货预警:近 15 天日销 > 0 且 现有库存 < 3 天日销 ───────────────
|
||||
const metrics = (row) => _private.buildReplenishMetrics(row);
|
||||
|
||||
test('replenish: stock below 3 days of sales is flagged as stockout or warning', () => {
|
||||
const stockout = metrics({ sevenAvgSale: 2.55, realtimeStock: 0, monitorStock: 0 });
|
||||
const stockout = metrics({ avgSale: 2.55, realtimeStock: 0, monitorStock: 0 });
|
||||
assert.equal(stockout.stockDays, 0);
|
||||
assert.equal(stockout.warningLevel, '缺货');
|
||||
assert.equal(stockout.hasWarning, true);
|
||||
assert.equal(stockout.replenishQty, 8); // ceil(3 * 2.55 - 0) = ceil(7.65)
|
||||
|
||||
const warning = metrics({ sevenAvgSale: 2, realtimeStock: 1, monitorStock: 1 });
|
||||
const warning = metrics({ avgSale: 2, realtimeStock: 1, monitorStock: 1 });
|
||||
assert.equal(warning.stockDays, 0.5);
|
||||
assert.equal(warning.warningLevel, '预警');
|
||||
assert.equal(warning.hasWarning, true);
|
||||
@ -208,34 +208,34 @@ test('replenish: stock below 3 days of sales is flagged as stockout or warning',
|
||||
});
|
||||
|
||||
test('replenish: the threshold is exactly 3 days of sales', () => {
|
||||
const atThreshold = metrics({ sevenAvgSale: 2, realtimeStock: 6, monitorStock: 6 });
|
||||
const atThreshold = metrics({ avgSale: 2, realtimeStock: 6, monitorStock: 6 });
|
||||
assert.equal(atThreshold.stockDays, 3);
|
||||
assert.equal(atThreshold.warningLevel, '正常');
|
||||
assert.equal(atThreshold.hasWarning, false);
|
||||
assert.equal(atThreshold.replenishQty, 0);
|
||||
|
||||
const below = metrics({ sevenAvgSale: 2, realtimeStock: 5.9, monitorStock: 5.9 });
|
||||
const below = metrics({ avgSale: 2, realtimeStock: 5.9, monitorStock: 5.9 });
|
||||
assert.equal(below.hasWarning, true);
|
||||
});
|
||||
|
||||
test('replenish: stock prefers the brand-warehouse realtime value, snapshot only as fallback', () => {
|
||||
const realtime = metrics({ sevenAvgSale: 1, realtimeStock: 4, monitorStock: 9 });
|
||||
const realtime = metrics({ avgSale: 1, realtimeStock: 4, monitorStock: 9 });
|
||||
assert.equal(realtime.stock, 4);
|
||||
assert.equal(realtime.stockSource, 'realtime');
|
||||
|
||||
// 实时库存「无记录」(null)才回退监控快照
|
||||
const fallback = metrics({ sevenAvgSale: 1, realtimeStock: null, monitorStock: 9 });
|
||||
const fallback = metrics({ avgSale: 1, realtimeStock: null, monitorStock: 9 });
|
||||
assert.equal(fallback.stock, 9);
|
||||
assert.equal(fallback.stockSource, 'monitor');
|
||||
|
||||
// 实时库存为 0 是有效值,不得被快照覆盖
|
||||
const zero = metrics({ sevenAvgSale: 1, realtimeStock: 0, monitorStock: 9 });
|
||||
const zero = metrics({ avgSale: 1, realtimeStock: 0, monitorStock: 9 });
|
||||
assert.equal(zero.stock, 0);
|
||||
assert.equal(zero.warningLevel, '缺货');
|
||||
});
|
||||
|
||||
test('replenish: products without 7-day sales never warn', () => {
|
||||
const noSale = metrics({ sevenAvgSale: 0, realtimeStock: 0, monitorStock: 0 });
|
||||
const noSale = metrics({ avgSale: 0, realtimeStock: 0, monitorStock: 0 });
|
||||
assert.equal(noSale.hasWarning, false);
|
||||
assert.equal(noSale.warningLevel, '无销量');
|
||||
assert.equal(noSale.stockDays, null);
|
||||
@ -286,7 +286,7 @@ test('replenish sort puts the most urgent first and no-sale products last', () =
|
||||
test('replenish: a pending work order suppresses the replenish warning', () => {
|
||||
// 库存为 0、本该判「缺货」,但半成品包装项目仓已有未完成加工单
|
||||
const scheduled = metrics({
|
||||
sevenAvgSale: 2,
|
||||
avgSale: 2,
|
||||
realtimeStock: 0,
|
||||
monitorStock: 0,
|
||||
pendingWorkOrderQty: 20,
|
||||
@ -307,13 +307,13 @@ test('replenish: a pending work order suppresses the replenish warning', () => {
|
||||
assert.equal(scheduled.stockDays, 0);
|
||||
|
||||
// 同样库存但无加工单时仍然预警 —— 确认抑制效果确实来自加工单
|
||||
const bare = metrics({ sevenAvgSale: 2, realtimeStock: 0, monitorStock: 0 });
|
||||
const bare = metrics({ avgSale: 2, realtimeStock: 0, monitorStock: 0 });
|
||||
assert.equal(bare.hasWarning, true);
|
||||
assert.equal(bare.warningLevel, '缺货');
|
||||
});
|
||||
|
||||
test('replenish: pending work order fields default to empty without any order', () => {
|
||||
const none = metrics({ sevenAvgSale: 1, realtimeStock: 0 });
|
||||
const none = metrics({ avgSale: 1, realtimeStock: 0 });
|
||||
assert.equal(none.hasPendingWorkOrder, false);
|
||||
assert.equal(none.pendingWorkOrderQty, 0);
|
||||
assert.equal(none.pendingWorkOrderCount, 0);
|
||||
@ -322,7 +322,7 @@ test('replenish: pending work order fields default to empty without any order',
|
||||
|
||||
// 数量为 0 但确实挂着加工单(未入库)时,同样按「已排产」处理
|
||||
const zeroQty = metrics({
|
||||
sevenAvgSale: 1,
|
||||
avgSale: 1,
|
||||
realtimeStock: 0,
|
||||
pendingWorkOrderQty: 0,
|
||||
pendingWorkOrderCount: 2,
|
||||
@ -334,7 +334,7 @@ test('replenish: pending work order fields default to empty without any order',
|
||||
|
||||
test('replenish: products without sales stay "no sale" even with a pending work order', () => {
|
||||
const noSale = metrics({
|
||||
sevenAvgSale: 0,
|
||||
avgSale: 0,
|
||||
realtimeStock: 0,
|
||||
pendingWorkOrderQty: 5,
|
||||
pendingWorkOrderCount: 1,
|
||||
@ -394,7 +394,7 @@ test('pending order: only MKH finished products count, combination codes are exc
|
||||
test('replenish: no sales but unshipped orders still trigger the warning', () => {
|
||||
// 库存不够发货 → 缺货,补货量 = 待发货件数 − 现有库存
|
||||
const notEnough = metrics({
|
||||
sevenAvgSale: 0, realtimeStock: 1, monitorStock: 1, pendingOrderQty: 4, pendingOrderCount: 2,
|
||||
avgSale: 0, realtimeStock: 1, monitorStock: 1, pendingOrderQty: 4, pendingOrderCount: 2,
|
||||
});
|
||||
assert.equal(notEnough.hasWarning, true);
|
||||
assert.equal(notEnough.warningLevel, '缺货');
|
||||
@ -407,7 +407,7 @@ test('replenish: no sales but unshipped orders still trigger the warning', () =>
|
||||
|
||||
// 库存刚好够发 → 预警(不再缺货),补货量为 0
|
||||
const exactlyEnough = metrics({
|
||||
sevenAvgSale: 0, realtimeStock: 4, monitorStock: 4, pendingOrderQty: 4,
|
||||
avgSale: 0, realtimeStock: 4, monitorStock: 4, pendingOrderQty: 4,
|
||||
});
|
||||
assert.equal(exactlyEnough.warningLevel, '预警');
|
||||
assert.equal(exactlyEnough.hasWarning, true);
|
||||
@ -418,7 +418,7 @@ test('replenish: no sales but unshipped orders still trigger the warning', () =>
|
||||
|
||||
test('replenish: order-driven warning does not override the pending work order exemption', () => {
|
||||
const scheduled = metrics({
|
||||
sevenAvgSale: 0,
|
||||
avgSale: 0,
|
||||
realtimeStock: 0,
|
||||
pendingOrderQty: 5,
|
||||
pendingOrderCount: 1,
|
||||
@ -434,19 +434,19 @@ test('replenish: order-driven warning does not override the pending work order e
|
||||
});
|
||||
|
||||
test('replenish: pending order fields default to empty without any unshipped order', () => {
|
||||
const none = metrics({ sevenAvgSale: 1, realtimeStock: 0 });
|
||||
const none = metrics({ avgSale: 1, realtimeStock: 0 });
|
||||
assert.equal(none.pendingOrderQty, 0);
|
||||
assert.equal(none.pendingOrderCount, 0);
|
||||
assert.equal(none.pendingOrderEarliest, null);
|
||||
assert.equal(none.orderDriven, false);
|
||||
|
||||
// 无日销且无订单 → 仍是「无销量」,不参与预警
|
||||
const noSale = metrics({ sevenAvgSale: 0, realtimeStock: 0, pendingOrderQty: 0 });
|
||||
const noSale = metrics({ avgSale: 0, realtimeStock: 0, pendingOrderQty: 0 });
|
||||
assert.equal(noSale.warningLevel, '无销量');
|
||||
assert.equal(noSale.orderDriven, false);
|
||||
|
||||
// 有日销的商品照旧走原口径,但待发货量也要带上(页面新增列)
|
||||
const withSale = metrics({ sevenAvgSale: 2, realtimeStock: 0, pendingOrderQty: 3 });
|
||||
const withSale = metrics({ avgSale: 2, realtimeStock: 0, pendingOrderQty: 3 });
|
||||
assert.equal(withSale.warningLevel, '缺货');
|
||||
assert.equal(withSale.orderDriven, false);
|
||||
assert.equal(withSale.pendingOrderQty, 3);
|
||||
|
||||
@ -83,7 +83,7 @@ test('notification carries recipient, idempotency key and shortfall details', ()
|
||||
const all = [...fresh, alert('MKHDNZ0409260700486', '预警', { dailySale: 2, stock: 1, replenishQty: 5 })];
|
||||
const snapshot = {
|
||||
monitorDate: '2026-09-16',
|
||||
saleWindowDays: 7,
|
||||
saleWindowDays: 15,
|
||||
replenishDaysThreshold: 3,
|
||||
outOfStockCount: 1,
|
||||
warningOnlyCount: 1,
|
||||
|
||||
@ -71,7 +71,7 @@ const PRODUCT_EXPORT_COLUMNS = [
|
||||
{ title: 'MKH商品编码', dataIndex: 'skuId' },
|
||||
{ title: '商品名称', dataIndex: 'productName' },
|
||||
{ title: '预警', dataIndex: 'warningLevel' },
|
||||
{ title: '近7日日销', dataIndex: 'dailySale' },
|
||||
{ title: '近15天日销', dataIndex: 'dailySale' },
|
||||
{ title: '现有库存', dataIndex: 'stock' },
|
||||
{ title: '在途加工量', dataIndex: 'pendingWorkOrderQty' },
|
||||
{ title: '待发货订单', dataIndex: 'pendingOrderQty' },
|
||||
@ -512,7 +512,7 @@ const MkhBomData = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '近7日日销',
|
||||
title: '近15天日销',
|
||||
dataIndex: 'dailySale',
|
||||
width: 106,
|
||||
align: 'right',
|
||||
@ -562,7 +562,7 @@ const MkhBomData = () => {
|
||||
`聚水潭未发货订单 ${row.pendingOrderCount || 0} 单,合计 ${number(value)} 件`,
|
||||
row.pendingOrderEarliest ? `最早一单下单于 ${row.pendingOrderEarliest}` : '',
|
||||
'已取消 / 已合并单据不核算',
|
||||
row.orderDriven ? '该商品近 7 日暂无日销,按待发货订单量列入补货预警' : '',
|
||||
row.orderDriven ? '该商品近 15 天暂无日销,按待发货订单量列入补货预警' : '',
|
||||
].filter(Boolean).join(';')}
|
||||
>
|
||||
<span className="mkh-order">{number(value)}</span>
|
||||
@ -577,9 +577,9 @@ const MkhBomData = () => {
|
||||
sorter: true,
|
||||
render: (value, row) => {
|
||||
if (value === null || value === undefined) {
|
||||
// 订单驱动(近7日无日销、按待发货订单预警)与「无销量」都算不出天数,但含义不同
|
||||
// 订单驱动(近15天无日销、按待发货订单预警)与「无销量」都算不出天数,但含义不同
|
||||
return row.orderDriven
|
||||
? <Tooltip title="近 7 日暂无日销,无日销基数可算天数,按待发货订单量预警"><span className="mkh-muted">按订单</span></Tooltip>
|
||||
? <Tooltip title="近 15 天暂无日销,无日销基数可算天数,按待发货订单量预警"><span className="mkh-muted">按订单</span></Tooltip>
|
||||
: <span className="mkh-muted">无销量</span>;
|
||||
}
|
||||
return <span className={row.hasWarning ? 'mkh-days--warn' : undefined}>{number(value, 2)}</span>;
|
||||
@ -717,7 +717,7 @@ const MkhBomData = () => {
|
||||
<div className="mkh-header__heading">
|
||||
<Title level={3}>MKH-BOM 资料</Title>
|
||||
<Text type="secondary">
|
||||
模块化(MKH 开头)成品的配件明细与补货预警:近 7 日日销核算,库存低于 3 天日销即预警
|
||||
模块化(MKH 开头)成品的配件明细与补货预警:近 15 天日销核算,库存低于 3 天日销即预警
|
||||
</Text>
|
||||
<Text type="secondary" className="mkh-note">
|
||||
数据口径:MKH 商品编码取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”,并且必须是聚水潭正式 BOM 的父项;
|
||||
@ -725,11 +725,11 @@ const MkhBomData = () => {
|
||||
无指定仓库商品档案的 BOM 组合编码不会进入列表;配件仓位只取 rpt.jst_packInventoryInfo,
|
||||
优先显示具体仓位,没有具体仓位时显示箱号,表中没有有效记录时显示「未维护」。
|
||||
<br />
|
||||
补货预警:近7日日销取聚水潭库存监控快照(监控日 {data.monitorDate || '—'}),现有库存优先取品牌仓实时库存、
|
||||
该仓无记录时回退监控快照库存;可用天数 = 现有库存 ÷ 近7日日销,低于 3 天即预警,缺货单独标识;
|
||||
建议补货量 = 3 × 近7日日销 − 现有库存(向上取整)。无日销且无订单的商品不参与预警。
|
||||
补货预警:近15天日销取聚水潭库存监控快照(监控日 {data.monitorDate || '—'}),现有库存优先取品牌仓实时库存、
|
||||
该仓无记录时回退监控快照库存;可用天数 = 现有库存 ÷ 近15天日销,低于 3 天即预警,缺货单独标识;
|
||||
建议补货量 = 3 × 近15天日销 − 现有库存(向上取整)。无日销且无订单的商品不参与预警。
|
||||
<br />
|
||||
待发货订单:近7日暂无日销、但聚水潭存在「未发货订单」的 MKH 成品同样列入补货预警
|
||||
待发货订单:近15天暂无日销、但聚水潭存在「未发货订单」的 MKH 成品同样列入补货预警
|
||||
(新品刚接到订单时监控快照的日销还是 0,避免漏备货)。未发货 = 订单无发货时间且状态非「取消 / 已合并」;
|
||||
预警级别按「现有库存 < 待发货件数 → 缺货,否则预警」,建议补货量 = 待发货件数 − 现有库存,
|
||||
可用天数因无日销基数而显示「按订单」。
|
||||
@ -793,7 +793,7 @@ const MkhBomData = () => {
|
||||
className="mkh-alert"
|
||||
type="warning"
|
||||
showIcon
|
||||
message="待发货订单数据源暂时不可用,待发货量按 0 处理,可能遗漏「近7日无日销但有订单」的商品,请稍后点击「刷新数据」重试。"
|
||||
message="待发货订单数据源暂时不可用,待发货量按 0 处理,可能遗漏「近15天无日销但有订单」的商品,请稍后点击「刷新数据」重试。"
|
||||
action={<Button size="small" onClick={load}>刷新数据</Button>}
|
||||
/>
|
||||
) : null}
|
||||
@ -816,14 +816,14 @@ const MkhBomData = () => {
|
||||
message={`补货预警:${data.warningCount} 个 MKH 商品需要补货`}
|
||||
description={(
|
||||
<span>
|
||||
按近 {data.saleWindowDays || 7} 日日销核算(监控日 {data.monitorDate || '—'}):
|
||||
按近 {data.saleWindowDays || 15} 天日销核算(监控日 {data.monitorDate || '—'}):
|
||||
其中 <b>{data.outOfStockCount || 0}</b> 个已缺货,
|
||||
<b>{Math.max(0, (data.warningCount || 0) - (data.outOfStockCount || 0))}</b> 个可用天数不足 {data.replenishDaysThreshold || 3} 天。
|
||||
建议补货量 = {data.replenishDaysThreshold || 3} × 近7日日销 − 现有库存。点「只看预警」即为补货清单。
|
||||
建议补货量 = {data.replenishDaysThreshold || 3} × 近15天日销 − 现有库存。点「只看预警」即为补货清单。
|
||||
{(data.orderDrivenCount || 0) > 0 ? (
|
||||
<>
|
||||
<br />
|
||||
其中 <b>{data.orderDrivenCount}</b> 个近 7 日暂无日销,因聚水潭存在未发货订单
|
||||
其中 <b>{data.orderDrivenCount}</b> 个近 15 天暂无日销,因聚水潭存在未发货订单
|
||||
(待发货合计 {number(data.pendingOrderQtyTotal || 0)} 件)而列入预警,建议补货量按待发货件数核算。
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@ -233,8 +233,8 @@ const AccessoryStockPanel = () => {
|
||||
</Tooltip>),
|
||||
},
|
||||
{
|
||||
title: '近7日日均发货量',
|
||||
dataIndex: 'avgShip7d',
|
||||
title: '近15天日均发货量',
|
||||
dataIndex: 'avgShip15d',
|
||||
width: 138,
|
||||
align: 'right',
|
||||
sorter: true,
|
||||
@ -402,13 +402,15 @@ const ModularStockMonitor = () => (
|
||||
<div className="msm-header__heading">
|
||||
<Title level={3}>模块化库存监控</Title>
|
||||
<Text type="secondary">
|
||||
配件库存监控:仅统计「简尚-崔桥工厂半成品包装项目仓」下的 SKU 实时库存、近 7 日日均发货量与可用天数
|
||||
配件库存监控:仅统计「简尚-崔桥工厂半成品包装项目仓」下的 SKU 实时库存、近 15 天日均发货量与可用天数
|
||||
</Text>
|
||||
<Text type="secondary" className="msm-note">
|
||||
数据口径:实时库存取 jst_inventoryInfo(wms_co_id 15718196);近 7 日日均发货量、
|
||||
数据口径:实时库存取 jst_inventoryInfo(wms_co_id 15718196);近 15 天日均发货量、
|
||||
库存可用天数、安全天数取 pm_stockMonitor 最新快照(按完整 SKU 编码关联,794 个 SKU 全覆盖);
|
||||
当日入库取采购到货、当日出库取近一日发货量,均以监控日为基准;仓位上游未回传,暂显示「未维护」;
|
||||
图片取自「产品资料系统数据库」商品档案(业务库图片字段均为空)。
|
||||
注:日均发货量 2026-09-17 起由近 7 天改为近 15 天(业务原要求 30 天,但业务库无 30 天日销,最长只到 15 天);
|
||||
库存可用天数仍沿用监控表 jsUseDays(源系统口径),故两者并非同一时间窗。
|
||||
状态列:基于「产品资料系统数据库」判断该 SKU 是否已建立商品档案——已配置MKH = 资料库中可查到该 SKU;未配置MKH = 查不到。
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user