feat: 补货预警-日销为0有代发订单时按库存÷代发件数算可用天数预警
- ⑩订单驱动分支改为:可用天数 = 现有库存 ÷ 代发(待发货)订单件数 · 可用天数 = 0(无库存)→ 缺货 · 0 < 可用天数 < 3 天 → 预警(建议补到 3 天缓冲:3×代发件数−现有库存) · 可用天数 ≥ 3 天 → 库存充足,不预警(阈值语义与日销分支一致) - buildReplenishMetrics 订单驱动分支不再把 stockDays 记 null,统一如实记录 - 同步前端「可用天数」列/导出/数据口径说明文案(不再显示「按订单」) - 更新 mkhBomService.test.js 对应用例,单测 40/40 通过
This commit is contained in:
parent
3e02c3e393
commit
8ba052649d
@ -143,16 +143,25 @@
|
||||
// (实测对应聚水潭 status=Delivering 配货中 33 单 / Question 问题件 29 单 / Split 4 单;
|
||||
// 已发货的 Sent 恒有 send_date;Cancelled/Merged 是取消与被合并单,不能算需求)
|
||||
// · 待发货订单量 = Σ 该成品在未发货订单中的件数(同一单多次出现会累加)
|
||||
// · 预警级别(业务确认):现有库存 < 待发货订单量 → 缺货;否则 → 预警
|
||||
// · 建议补货量 = max(0, 待发货订单量 − 现有库存)
|
||||
// ⚠️ 该分支无法算「可用天数」(日销为 0,除数为零):
|
||||
// 缺货时记 stockDays = 0(保证排在最前),预警(库存已够发)时记 null。
|
||||
// · 预警判定(2026-09-17 业务补充口径):日销为 0,但仍用「可用天数」衡量风险 ——
|
||||
// 可用天数 = 现有库存 ÷ 代发(待发货)订单件数
|
||||
// · 可用天数 = 0(无库存) → 缺货
|
||||
// · 0 < 可用天数 < 3 天 → 预警
|
||||
// · 可用天数 ≥ 3 天 → 库存充足,不预警(阈值语义与日销分支一致)
|
||||
// ⚠️ 代发订单件数恒 > 0(orderDriven 已保证 pendingOrderQty > 0),不会出现除零;
|
||||
// 可用天数统一如实记录(保留 2 位小数),不再像以前那样缺货记 0 / 够发记 null。
|
||||
// · 建议补货量:
|
||||
// 缺货 → max(0, 代发订单件数 − 现有库存)(先补到能覆盖整笔订单)
|
||||
// 预警(0<天数<3) → max(0, 3 × 代发订单件数 − 现有库存)(补到 3 天缓冲)
|
||||
// 正常(天数≥3) → 0
|
||||
// ⚠️ 与「在途加工单」的优先级:**先判已排产**(见 ⑧)——
|
||||
// 无日销但有订单、且工厂已开加工单的商品,仍标「已排产」不预警,避免重复提示。
|
||||
// 实测(2026-09-17,7 天口径下):未发货且含 MKH 成品的订单 75 单 → 48 个成品编码 / 71 件,
|
||||
// 其中 30 个本来就有日销、**18 个是本次新增**(日销=0,靠订单才进预警)。
|
||||
// ⚠️ 改 15 天口径后实测:订单驱动从 14 个降到 **8 个**
|
||||
// (日销基数 114 → 170,原本靠订单兜底的商品自己有日销了),与预期一致。
|
||||
// ⚠️ 同日又叠加「可用天数 ≤ 3 天」阈值(2026-09-17 补充):代发件数大、库存也能撑 3 天
|
||||
// 以上的商品不再预警,订单驱动预警数会在此 8 个基础上进一步减少(具体数以接口为准)。
|
||||
//
|
||||
// ── ⑪ 配件不足 / 可组装数(2026-09-17 业务确认口径)────────────────
|
||||
// 规则:**缺货 / 预警的 MKH 成品,若其配件现有库存「可组装数 = 0」,则提示
|
||||
@ -806,7 +815,8 @@ const sumQty = (components) => components.reduce((sum, item) => sum + (toNumber(
|
||||
*
|
||||
* 口径(见文件头 ⑦ + ⑩):
|
||||
* ① 近15天日销 > 0 且 现有库存 < 3 天日销 → 预警
|
||||
* ② 近15天日销 = 0 但存在未发货订单 → 订单驱动预警(见文件头 ⑩)
|
||||
* ② 近15天日销 = 0 且有代发(待发货)订单 → 订单驱动预警(见文件头 ⑩):
|
||||
* 可用天数 = 现有库存 ÷ 代发订单件数,< 3 天预警、= 0(无库存)缺货、≥ 3 天不预警
|
||||
* 两者都不满足 → 无销量,不参与预警。
|
||||
* 已有未完成加工单的商品一律标「已排产」,两种口径都不预警。
|
||||
*
|
||||
@ -882,17 +892,37 @@ function buildReplenishMetrics(row = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// 订单驱动(见文件头 ⑩):日销为 0 算不出可用天数,改按「能否覆盖待发货」判级
|
||||
// 订单驱动(见文件头 ⑩):日销为 0 但有代发(待发货)订单、有库存
|
||||
// → 用「现有库存 ÷ 代发订单件数」估算可用天数,≤ 3 天即补货预警
|
||||
// ⚠️ 阈值语义与日销分支一致:可用天数 < 3 天为「预警」、= 0(无库存)为「缺货」、
|
||||
// ≥ 3 天视为库存充足不预警;代发订单件数恒 > 0(orderDriven 已保证 pendingOrderQty > 0)
|
||||
if (orderDriven) {
|
||||
const notEnough = stock < pendingOrderQty;
|
||||
const orderStockDays = stock / pendingOrderQty;
|
||||
let level;
|
||||
let replenishQty;
|
||||
if (orderStockDays <= 0) {
|
||||
// 完全没货(库存 ≤ 0):缺货,建议补到能覆盖整笔代发订单
|
||||
level = WARNING_LEVEL.OUT_OF_STOCK;
|
||||
replenishQty = Math.max(0, pendingOrderQty - stock);
|
||||
} else if (orderStockDays < REPLENISH_DAYS_THRESHOLD) {
|
||||
// 有货但可用天数不足 3 天:预警,建议补到 3 天缓冲(3 × 代发件数 − 现有库存)
|
||||
level = WARNING_LEVEL.WARNING;
|
||||
replenishQty = Math.max(0, Math.ceil(REPLENISH_DAYS_THRESHOLD * pendingOrderQty - stock));
|
||||
} else {
|
||||
// 可用天数 ≥ 3 天:库存充足,不预警
|
||||
level = WARNING_LEVEL.NORMAL;
|
||||
replenishQty = 0;
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
// 缺货记 0(保证排在最前);库存已够发货时记 null(无日销,算不出天数)
|
||||
stockDays: notEnough ? 0 : null,
|
||||
replenishQty: Math.max(0, pendingOrderQty - stock),
|
||||
warningLevel: notEnough ? WARNING_LEVEL.OUT_OF_STOCK : WARNING_LEVEL.WARNING,
|
||||
warningOrder: notEnough ? 1 : 2,
|
||||
hasWarning: true,
|
||||
// 可用天数统一记录(保留 2 位小数,用于排序与展示)
|
||||
stockDays: Number(orderStockDays.toFixed(2)),
|
||||
replenishQty,
|
||||
warningLevel: level,
|
||||
// 排序权重:缺货 1 < 预警 2 < 正常 3(与日销分支一致)
|
||||
warningOrder: level === WARNING_LEVEL.OUT_OF_STOCK ? 1
|
||||
: (level === WARNING_LEVEL.WARNING ? 2 : 3),
|
||||
hasWarning: level === WARNING_LEVEL.OUT_OF_STOCK || level === WARNING_LEVEL.WARNING,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -391,29 +391,55 @@ test('pending order: only MKH finished products count, combination codes are exc
|
||||
assert.deepEqual([..._private.PENDING_ORDER_EXCLUDED_STATUS], ['Cancelled', 'Merged']);
|
||||
});
|
||||
|
||||
test('replenish: no sales but unshipped orders still trigger the warning', () => {
|
||||
// 库存不够发货 → 缺货,补货量 = 待发货件数 − 现有库存
|
||||
const notEnough = metrics({
|
||||
avgSale: 0, realtimeStock: 1, monitorStock: 1, pendingOrderQty: 4, pendingOrderCount: 2,
|
||||
test('replenish: no sales but unshipped orders warn by available days (stock / order qty)', () => {
|
||||
// 可用天数 = 现有库存 ÷ 代发订单件数;≤ 3 天即预警,= 0(无库存)为缺货。
|
||||
// 完全没货:可用天数 0 → 缺货,补货量 = 代发件数 − 现有库存
|
||||
const outOfStock = metrics({
|
||||
avgSale: 0, realtimeStock: 0, monitorStock: 0, pendingOrderQty: 4, pendingOrderCount: 2,
|
||||
});
|
||||
assert.equal(notEnough.hasWarning, true);
|
||||
assert.equal(notEnough.warningLevel, '缺货');
|
||||
assert.equal(notEnough.replenishQty, 3);
|
||||
assert.equal(notEnough.orderDriven, true);
|
||||
// 无日销基数 → 缺货时记 0,保证排在预警清单最前
|
||||
assert.equal(notEnough.stockDays, 0);
|
||||
assert.equal(notEnough.pendingOrderQty, 4);
|
||||
assert.equal(notEnough.pendingOrderCount, 2);
|
||||
assert.equal(outOfStock.hasWarning, true);
|
||||
assert.equal(outOfStock.warningLevel, '缺货');
|
||||
assert.equal(outOfStock.orderDriven, true);
|
||||
assert.equal(outOfStock.stockDays, 0);
|
||||
assert.equal(outOfStock.replenishQty, 4);
|
||||
assert.equal(outOfStock.pendingOrderQty, 4);
|
||||
assert.equal(outOfStock.pendingOrderCount, 2);
|
||||
|
||||
// 库存刚好够发 → 预警(不再缺货),补货量为 0
|
||||
const exactlyEnough = metrics({
|
||||
// 有货但只够 0.25 天 → 预警(不再因「库存 < 订单件数」直接判缺货)
|
||||
const short = metrics({
|
||||
avgSale: 0, realtimeStock: 1, monitorStock: 1, pendingOrderQty: 4,
|
||||
});
|
||||
assert.equal(short.warningLevel, '预警');
|
||||
assert.equal(short.hasWarning, true);
|
||||
assert.equal(short.stockDays, 0.25);
|
||||
// 补到 3 天缓冲:ceil(3 × 4 − 1) = 11
|
||||
assert.equal(short.replenishQty, 11);
|
||||
|
||||
// 可用天数正好 1 天 → 仍预警,补到 3 天缓冲:ceil(3 × 4 − 4) = 8
|
||||
const oneDay = metrics({
|
||||
avgSale: 0, realtimeStock: 4, monitorStock: 4, pendingOrderQty: 4,
|
||||
});
|
||||
assert.equal(exactlyEnough.warningLevel, '预警');
|
||||
assert.equal(exactlyEnough.hasWarning, true);
|
||||
assert.equal(exactlyEnough.replenishQty, 0);
|
||||
// 库存已够发时算不出可用天数,记 null(页面显示「按订单」)
|
||||
assert.equal(exactlyEnough.stockDays, null);
|
||||
assert.equal(oneDay.warningLevel, '预警');
|
||||
assert.equal(oneDay.stockDays, 1);
|
||||
assert.equal(oneDay.replenishQty, 8);
|
||||
|
||||
// 可用天数正好 3 天(库存=12,件数=4)→ 阈值取「< 3」,故不预警
|
||||
const atThreshold = metrics({
|
||||
avgSale: 0, realtimeStock: 12, monitorStock: 12, pendingOrderQty: 4,
|
||||
});
|
||||
assert.equal(atThreshold.warningLevel, '正常');
|
||||
assert.equal(atThreshold.hasWarning, false);
|
||||
assert.equal(atThreshold.stockDays, 3);
|
||||
assert.equal(atThreshold.replenishQty, 0);
|
||||
|
||||
// 可用天数超 3 天(库存=13,件数=4 → 3.25 天)→ 库存充足,不预警,但仍标记 orderDriven
|
||||
const enough = metrics({
|
||||
avgSale: 0, realtimeStock: 13, monitorStock: 13, pendingOrderQty: 4,
|
||||
});
|
||||
assert.equal(enough.warningLevel, '正常');
|
||||
assert.equal(enough.hasWarning, false);
|
||||
assert.equal(enough.orderDriven, true);
|
||||
assert.equal(enough.stockDays, 3.25);
|
||||
});
|
||||
|
||||
test('replenish: order-driven warning does not override the pending work order exemption', () => {
|
||||
|
||||
@ -78,8 +78,8 @@ const PRODUCT_EXPORT_COLUMNS = [
|
||||
{
|
||||
title: '可用天数',
|
||||
dataIndex: 'stockDays',
|
||||
// 无销量商品 stockDays 为 null,导出成文字而非空白,一眼能分辨;
|
||||
// 订单驱动(日销=0 但有未发货订单)同样是 null,口径不同要分开说明
|
||||
// 无销量且无代发订单的商品 stockDays 为 null,导出成文字而非空白;
|
||||
// 订单驱动分支(日销=0 但有代发/待发货订单)现已如实给出可用天数(库存÷代发件数),不会进这里
|
||||
exportValue: (value, row) => {
|
||||
if (value !== null && value !== undefined) return value;
|
||||
return row.orderDriven ? '无日销·按订单' : '无销量';
|
||||
@ -562,7 +562,7 @@ const MkhBomData = () => {
|
||||
`聚水潭未发货订单 ${row.pendingOrderCount || 0} 单,合计 ${number(value)} 件`,
|
||||
row.pendingOrderEarliest ? `最早一单下单于 ${row.pendingOrderEarliest}` : '',
|
||||
'已取消 / 已合并单据不核算',
|
||||
row.orderDriven ? '该商品近 15 天暂无日销,按待发货订单量列入补货预警' : '',
|
||||
row.orderDriven ? '该商品近 15 天暂无日销,按代发(待发货)订单量列入补货预警,可用天数 = 库存 ÷ 代发件数' : '',
|
||||
].filter(Boolean).join(';')}
|
||||
>
|
||||
<span className="mkh-order">{number(value)}</span>
|
||||
@ -577,12 +577,19 @@ const MkhBomData = () => {
|
||||
sorter: true,
|
||||
render: (value, row) => {
|
||||
if (value === null || value === undefined) {
|
||||
// 订单驱动(近15天无日销、按待发货订单预警)与「无销量」都算不出天数,但含义不同
|
||||
return row.orderDriven
|
||||
? <Tooltip title="近 15 天暂无日销,无日销基数可算天数,按待发货订单量预警"><span className="mkh-muted">按订单</span></Tooltip>
|
||||
: <span className="mkh-muted">无销量</span>;
|
||||
// 真正算不出天数:无日销且无代发订单 → 无销量;订单驱动分支现已如实给出天数,不会进这里
|
||||
return <span className="mkh-muted">无销量</span>;
|
||||
}
|
||||
return <span className={row.hasWarning ? 'mkh-days--warn' : undefined}>{number(value, 2)}</span>;
|
||||
const text = <span className={row.hasWarning ? 'mkh-days--warn' : undefined}>{number(value, 2)}</span>;
|
||||
if (row.orderDriven) {
|
||||
// 近 15 天无日销,可用天数按「库存 ÷ 代发订单件数」估算,≤ 3 天即预警
|
||||
return (
|
||||
<Tooltip title="近 15 天暂无日销,可用天数 = 现有库存 ÷ 代发(待发货)订单件数(≤ 3 天即预警)">
|
||||
{text}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -729,10 +736,10 @@ const MkhBomData = () => {
|
||||
该仓无记录时回退监控快照库存;可用天数 = 现有库存 ÷ 近15天日销,低于 3 天即预警,缺货单独标识;
|
||||
建议补货量 = 3 × 近15天日销 − 现有库存(向上取整)。无日销且无订单的商品不参与预警。
|
||||
<br />
|
||||
待发货订单:近15天暂无日销、但聚水潭存在「未发货订单」的 MKH 成品同样列入补货预警
|
||||
待发货订单(代发):近15天暂无日销、但聚水潭存在「未发货订单」的 MKH 成品同样列入补货预警
|
||||
(新品刚接到订单时监控快照的日销还是 0,避免漏备货)。未发货 = 订单无发货时间且状态非「取消 / 已合并」;
|
||||
预警级别按「现有库存 < 待发货件数 → 缺货,否则预警」,建议补货量 = 待发货件数 − 现有库存,
|
||||
可用天数因无日销基数而显示「按订单」。
|
||||
可用天数 = 现有库存 ÷ 代发(待发货)订单件数,< 3 天预警、= 0(无库存)缺货、≥ 3 天不预警;
|
||||
建议补货量:缺货时按「代发件数 − 现有库存」、预警时按「3 × 代发件数 − 现有库存」补到 3 天缓冲。
|
||||
<br />
|
||||
在途加工单:「{data.workOrderWarehouse || '简尚-崔桥工厂半成品包装项目仓'}」中已存在且未完成的加工单已纳入核算
|
||||
(作废单据不核算);只要该 MKH 商品存在未完成加工单,即视为已排产、不再列入补货预警。
|
||||
@ -823,8 +830,8 @@ const MkhBomData = () => {
|
||||
{(data.orderDrivenCount || 0) > 0 ? (
|
||||
<>
|
||||
<br />
|
||||
其中 <b>{data.orderDrivenCount}</b> 个近 15 天暂无日销,因聚水潭存在未发货订单
|
||||
(待发货合计 {number(data.pendingOrderQtyTotal || 0)} 件)而列入预警,建议补货量按待发货件数核算。
|
||||
其中 <b>{data.orderDrivenCount}</b> 个近 15 天暂无日销,按代发(待发货)订单列入预警
|
||||
(待发货合计 {number(data.pendingOrderQtyTotal || 0)} 件),可用天数 = 库存 ÷ 代发件数、≤ 3 天即预警。
|
||||
</>
|
||||
) : null}
|
||||
{(data.scheduledCount || 0) > 0 ? (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user