forked from lunanting/cangku
fix(mkh): 在途加工单换用聚水潭原生三表(po_id 关联),修「页面 9 单 vs 聚水潭 18 单」
## 问题 页面「在途加工单 / 已排产」只显示 9 个,且全是 2026-11-03 的僵尸单; 业务在聚水潭看到的是 18 个,且那 9 个早已加工入库完成。 ## 根因 旧数据源是链接服务器快照表 `LS_JS_DMP(192.168.1.209).JSDataMiddlePlatform.dbo.bi_processing_work_orders`: - 表内**无任何时间列**,实测按加工日期自 2026-11-03 10:49 起 **8 天 0 行** (1,370 行 / MAX(Id)=1370 不再增长,而此前每天都有 5~34 行); - 且**数据陈旧不会报错** —— `workOrderDegraded` 只反映「查询失败」, 停更期间恒为 false,页面完全无告警。 结果:9 行残留「已确认/未入库」把对应成品永久锁成「已排产」压住补货预警, 而 11-04 之后新开的加工单一条都没进库(在途量按 0 算,缺货会被误报)。 ## 改动 数据源改为聚水潭原生加工单三表(业务库本实例,随主库小时级刷新),主键 `po_id` 关联: - `rpt.jst_manufactureInfo` 单头:status / wms_co_id(加工仓) / receipt_wms_co_id(收货仓) - `rpt.jst_manufactureItem` 成品明细:sku_id / qty / io_qty(已入库) / delivery_date - `rpt.jst_manufactureRaw` 原料明细(本次不参与在途核算,配件占用仍走 BOM 单台用量) 口径保持业务确认的语义,只换算字段: - 加工仓:中文名 `加工仓储方` → `wms_co_id = 15718196`(ID 比名称稳定) - 作废:`'作废'` → `'Cancelled'` - 完成:`'全部入库'` → `'Finished'` - 剩余量:`数量 − 已入库数量` → `qty − io_qty` - ETA:`预计加工完成时间` → `delivery_date`(实测 18 单全能解析) - 查询从 `getPool()` 四段名改为 `cloudDatabase.query()`(表名已带 rpt. 前缀,不受 dbo→rpt 重写影响) ## 验证 - 真实库实跑(生产 13333 隧道)SQL 与服务层 `loadPendingWorkOrderIndex()`: 命中 **18 个 SKU / 18 单 / 在途 175 件**,`degraded=false`,ETA 2026-11-24 ~ 11-25; 热态耗时 **0.7~1.5s**(冷启动 5s,结果进 10 分钟缓存)。 - 状态一致性:`Finished` 1,149 单的 1,173 行明细**零剩余**;`Cancelled` 178 单 io_qty 全 0; `Confirmed` 18 单 io_qty 全 0 → 剩余量判据自身即可区分完成,状态只用来剔作废。 - `node --test test/mkhBomService.test.js` **56/56 通过**;全量 505 项 502 通过, 3 项失败均为既有环境问题(secretStore Windows 权限、cloudDatabase 负缓存 TTL、 WST 用例在并发下的时序抖动,单独跑均通过)。 - `check-syntax` 196 文件通过。 ## 风险与回退 - 只换数据源、不动预警优先级与结余口径,接口字段名全部保持不变(`pendingWorkOrderQty/Count/Eta/No`), 前端零改动。 - 回退:单文件还原 `mkhBomService.js` 即可,无数据迁移、无 DDL。
This commit is contained in:
parent
54ad6aad89
commit
c5a8b592a1
@ -240,24 +240,27 @@
|
||||
// ⚠️ 配件库存**不适用本口径**(配件仍只算 15718196)——新仓一个配件都没有,
|
||||
// 横林三仓/沭阳仓虽有 MKH 配件但归属别的项目,并入会让「配件不足」漏报。
|
||||
//
|
||||
// ── ⑧ 在途加工单(2026-09-17 业务确认口径)────────────────────────
|
||||
// ── ⑧ 在途加工单(2026-09-17 业务确认口径;2026-11-11 换数据源)──────────
|
||||
// 规则:**只要该 MKH 商品存在未完成的加工单,就不再列入补货预警**
|
||||
// (工厂已经在做了,不重复提示补货),改标为「已排产」。
|
||||
// · 数据源:业务库同实例的链接服务器
|
||||
// LS_JS_DMP → JSDataMiddlePlatform.dbo.bi_processing_work_orders
|
||||
// (聚水潭「组装加工」加工单,一张扁平的 加工单 × 商品 明细表)
|
||||
// · 过滤:加工仓储方 = '简尚-崔桥工厂半成品包装项目仓'
|
||||
// AND 状态 <> '作废' ← 作废单据一律不核算
|
||||
// AND max(数量 − 已入库数量, 0) > 0 ← 只认真实剩余量
|
||||
// · 在途加工量 = Σ max(数量 − 已入库数量, 0),按 商品编码 归集。
|
||||
// 不再依赖「收货状态」文本:上游状态字段延迟时,已入库数量已追平的单据必须立即退出在途。
|
||||
// ⚠️ 为什么走 getPool 而不是 cloudDatabase.query:
|
||||
// query() 会经 rewriteReportReplicaSql() 把 `dbo.xxx` 重写成 `rpt.xxx`,
|
||||
// 跨库四段名 `LS_JS_DMP.JSDataMiddlePlatform.dbo.t` 会被改坏成
|
||||
// `LS_JS_DMP.JSDataMiddlePlatform.rpt.t`。getPool() 不经过该重写。
|
||||
// 实测(2026-09-16):全表 679 行,其中 完成 498(全部入库)/ 作废 176 /
|
||||
// 已确认 3 / 待审核 2;属半成品包装仓且未完成且非作废的只有 5 单 40 件。
|
||||
// ⚠️ 作废单在半成品包装仓有 79 单 740 件,若不过滤会虚增约 18 倍在途量。
|
||||
// · 数据源(2026-11-11 起):聚水潭原生加工单三表,主键 `po_id` 关联
|
||||
// rpt.jst_manufactureInfo 加工单单头:status / wms_co_id(加工仓) / receipt_wms_co_id(收货仓) / po_date
|
||||
// rpt.jst_manufactureItem 加工单**成品**明细:sku_id / qty(计划数) / io_qty(已入库) / delivery_date
|
||||
// rpt.jst_manufactureRaw 加工单**原料**明细:qty(暂未参与在途核算,配件占用走 BOM 单台用量)
|
||||
// · 过滤:i.wms_co_id = 15718196(简尚-崔桥工厂半成品包装项目仓,产出入品牌仓 15870495)
|
||||
// AND i.status <> 'Cancelled' ← 作废单不核算(实测 178 单 7,515 件,不过滤会虚增几十倍)
|
||||
// AND it.qty > ISNULL(it.io_qty,0) ← 只认真实剩余量
|
||||
// · 在途加工量 = Σ(qty − io_qty),按成品编码 sku_id 归集;ETA 取 delivery_date 最小值。
|
||||
// 状态值域实测(2026-11-11):Finished 1,149 单 / Cancelled 178 / **Confirmed 18**;
|
||||
// Finished 的 1,173 行明细**零剩余**(io_qty 已追平),Confirmed 的 18 行 io_qty 全 0。
|
||||
// → 无需依赖状态文本判「完成」,剩余量判据自身就够;状态只用来剔作废。
|
||||
// ⚠️ **换源原因**:旧源是链接服务器 `LS_JS_DMP`(192.168.1.209) 的快照表
|
||||
// `JSDataMiddlePlatform.dbo.bi_processing_work_orders`,自 2026-11-03 10:49 起**停更 8 天**
|
||||
// (1,370 行 / MAX(Id)=1370 不再增长,而此前每天都有 5~34 行),且**不会报错** ——
|
||||
// `workOrderDegraded` 只反映「查询失败」,数据陈旧永远是 false,
|
||||
// 于是页面长期显示 9 个 11-03 的僵尸单(业务在聚水潭看到的是 18 个新单)。
|
||||
// 换成本实例内的原生三表后随主库小时级刷新,不再依赖那台内网机器的同步任务。
|
||||
// 实测(2026-11-11):18 单 / 18 个成品 / 在途 175 件,ETA 全可解析(11-24 ~ 11-25)。
|
||||
//
|
||||
// ── ⑨ 导出(2026-09-17)──────────────────────────────────────────
|
||||
// 导出与列表**共用同一套筛选/排序口径**(filterRows + sortRows),区别只有两点:
|
||||
@ -465,15 +468,25 @@ const WARNING_LEVEL = Object.freeze({
|
||||
});
|
||||
|
||||
// ── 在途加工单参数(见文件头 ⑧)──────────────────────────────────
|
||||
// 加工仓储方:加工单的加工地,即半成品包装项目仓(产出入品牌仓)
|
||||
// 加工仓储方:加工单的加工地,即半成品包装项目仓(产出入品牌仓)。仅用于文案展示。
|
||||
const WORK_ORDER_WAREHOUSE = '简尚-崔桥工厂半成品包装项目仓';
|
||||
// 作废状态:作废单据不参与在途量核算
|
||||
const WORK_ORDER_VOID_STATUS = '作废';
|
||||
// 收货状态:该值表示已全部入库,等价于「已完成」
|
||||
const WORK_ORDER_RECEIVED_STATUS = '全部入库';
|
||||
// 加工单表位于同一实例的链接服务器上,四段名必须走 getPool 查询(原因见文件头 ⑧)
|
||||
const WORK_ORDER_TABLE = 'LS_JS_DMP.JSDataMiddlePlatform.dbo.bi_processing_work_orders';
|
||||
// 链接服务器不可用时的降级缓存时长:短一点,避免长时间带着错误在途量
|
||||
// 加工仓 wms_co_id(半成品包装项目仓):SQL 侧按它过滤。
|
||||
// ⚠️ 用 ID 而不是中文名 —— 名称可能被上游改写,ID 稳定;实测该仓加工单收货仓恒为品牌仓 15870495。
|
||||
const WORK_ORDER_WMS_CO_ID = 15718196;
|
||||
// 作废状态:作废单据不参与在途量核算(聚水潭加工单 status 值域 Confirmed / Finished / Cancelled)
|
||||
const WORK_ORDER_VOID_STATUS = 'Cancelled';
|
||||
// 完成状态:已全部入库
|
||||
const WORK_ORDER_RECEIVED_STATUS = 'Finished';
|
||||
// 聚水潭原生加工单三表(rpt schema 视图,主键 po_id):
|
||||
// Info = 单头(status / wms_co_id / po_date)
|
||||
// Item = 成品明细(sku_id / qty / io_qty 已入库 / delivery_date 预计到货)
|
||||
// Raw = 原料明细(暂未参与在途核算,配件占用走 BOM 单台用量)
|
||||
// ⚠️ 2026-11-11 换源:旧的链接服务器 LS_JS_DMP 快照表自 11-03 起停更 8 天且**不会报错**
|
||||
// (`workOrderDegraded` 只反映查询失败),导致页面 9 个僵尸单 + 新单全丢。
|
||||
// 这三张表在业务库本实例内,可走 cloudDatabase.query()(表名已带 rpt.,不会被重写坏)。
|
||||
const WORK_ORDER_INFO_TABLE = 'rpt.jst_manufactureInfo';
|
||||
const WORK_ORDER_ITEM_TABLE = 'rpt.jst_manufactureItem';
|
||||
// 加工单不可用时的降级缓存时长:短一点,避免长时间带着错误在途量
|
||||
const WORK_ORDER_DEGRADED_TTL_MS = 60 * 1000;
|
||||
|
||||
// ── 订单参数(见文件头 ⑩ 待发货 / ⑦-a 近15天已发货)──────────────
|
||||
@ -1392,8 +1405,10 @@ async function loadMkhProductIndex() {
|
||||
/**
|
||||
* 未完成加工单(在途量)索引 —— 进程内缓存 { bySku: Map<归一化编码, 在途指标>, degraded }。
|
||||
* 只保留「半成品包装项目仓加工、未完成、非作废」的加工单,口径见文件头 ⑧。
|
||||
* ⚠️ 该数据源是链接服务器,取不到时**降级为空索引**(补货预警照常跑),
|
||||
* 并缩短缓存避免长时间带着错误在途量。degraded=true 会被透出到接口。
|
||||
* 数据源:聚水潭原生加工单三表 —— jst_manufactureInfo(单头)+ jst_manufactureItem(成品明细),
|
||||
* PO 关联键 po_id;剩余量 = qty − io_qty(已入库数量)。
|
||||
* ⚠️ 取不到时**降级为空索引**(补货预警照常跑),并缩短缓存避免长时间带着错误在途量。
|
||||
* degraded=true 会被透出到接口。
|
||||
*/
|
||||
async function loadPendingWorkOrderIndex() {
|
||||
const now = Date.now();
|
||||
@ -1403,43 +1418,26 @@ async function loadPendingWorkOrderIndex() {
|
||||
if (workOrderIndexCache?.pending) return workOrderIndexCache.pending;
|
||||
|
||||
const pending = (async () => {
|
||||
// ⚠️ 必须走 getPool:cloudDatabase.query() 会把 `dbo.xxx` 重写成 `rpt.xxx`,
|
||||
// 跨库四段名会被改坏(见文件头 ⑧)。
|
||||
const pool = await cloudDatabase.getPool();
|
||||
const request = pool.request();
|
||||
request.input('workOrderWarehouse', WORK_ORDER_WAREHOUSE);
|
||||
request.input('voidStatus', WORK_ORDER_VOID_STATUS);
|
||||
const result = await request.query(`
|
||||
SELECT [商品编码] AS skuId,
|
||||
SUM(CASE
|
||||
WHEN CAST([数量] AS DECIMAL(18, 4)) > CAST([已入库数量] AS DECIMAL(18, 4))
|
||||
THEN CAST([数量] AS DECIMAL(18, 4)) - CAST([已入库数量] AS DECIMAL(18, 4))
|
||||
ELSE 0
|
||||
END) AS pendingQty,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN CAST([数量] AS DECIMAL(18, 4)) > CAST([已入库数量] AS DECIMAL(18, 4))
|
||||
THEN [加工单号]
|
||||
END) AS orderCount,
|
||||
MIN(CASE
|
||||
WHEN CAST([数量] AS DECIMAL(18, 4)) > CAST([已入库数量] AS DECIMAL(18, 4))
|
||||
THEN TRY_CONVERT(DATETIME, [预计加工完成时间], 111)
|
||||
END) AS eta,
|
||||
MIN(CASE
|
||||
WHEN CAST([数量] AS DECIMAL(18, 4)) > CAST([已入库数量] AS DECIMAL(18, 4))
|
||||
THEN CAST([加工单号] AS NVARCHAR(40))
|
||||
END) AS firstOrderNo
|
||||
FROM ${WORK_ORDER_TABLE} WITH (NOLOCK)
|
||||
WHERE [加工仓储方] = @workOrderWarehouse
|
||||
AND [状态] <> @voidStatus
|
||||
GROUP BY [商品编码]
|
||||
HAVING SUM(CASE
|
||||
WHEN CAST([数量] AS DECIMAL(18, 4)) > CAST([已入库数量] AS DECIMAL(18, 4))
|
||||
THEN CAST([数量] AS DECIMAL(18, 4)) - CAST([已入库数量] AS DECIMAL(18, 4))
|
||||
ELSE 0
|
||||
END) > 0`);
|
||||
// 表名已带 rpt. 前缀,`query()` 的 dbo→rpt 重写不会碰它,无需 getPool。
|
||||
// Finished 单的 io_qty 已追平(实测 1,173 行零剩余),靠 `qty > io_qty` 自然退出,不依赖状态文本。
|
||||
const rows = await cloudDatabase.query(
|
||||
`SELECT it.sku_id AS skuId,
|
||||
SUM(it.qty - ISNULL(it.io_qty, 0)) AS pendingQty,
|
||||
COUNT(DISTINCT i.po_id) AS orderCount,
|
||||
MIN(TRY_CONVERT(DATETIME, it.delivery_date, 111)) AS eta,
|
||||
MIN(CAST(i.po_id AS NVARCHAR(40))) AS firstOrderNo
|
||||
FROM ${WORK_ORDER_ITEM_TABLE} it WITH (NOLOCK)
|
||||
JOIN ${WORK_ORDER_INFO_TABLE} i WITH (NOLOCK) ON i.po_id = it.po_id
|
||||
WHERE i.wms_co_id = @workOrderWmsCoId
|
||||
AND i.status <> @voidStatus
|
||||
AND it.qty > ISNULL(it.io_qty, 0)
|
||||
GROUP BY it.sku_id`,
|
||||
{ workOrderWmsCoId: WORK_ORDER_WMS_CO_ID, voidStatus: WORK_ORDER_VOID_STATUS },
|
||||
{ requestTimeout: 180000 }
|
||||
);
|
||||
|
||||
const bySku = new Map();
|
||||
(result.recordset || []).forEach((row) => {
|
||||
(rows || []).forEach((row) => {
|
||||
const key = normalizeCode(row.skuId);
|
||||
if (!key) return;
|
||||
const pendingQty = Math.max(0, toNumber(row.pendingQty) ?? 0);
|
||||
@ -2349,7 +2347,7 @@ async function getMkhBomList(query = {}) {
|
||||
operationalRefreshedAt: operationalRefreshedAt
|
||||
? new Date(operationalRefreshedAt).toISOString()
|
||||
: null,
|
||||
// 加工单数据源(链接服务器)不可用时为 true,此时在途量按 0 处理
|
||||
// 加工单数据源不可用(查询失败)时为 true,此时在途量按 0 处理
|
||||
workOrderDegraded: replenishIndex.workOrderDegraded === true,
|
||||
workOrderWarehouse: WORK_ORDER_WAREHOUSE,
|
||||
// 待发货订单数据源不可用时为 true,此时待发货量按 0 处理(退化为旧口径)
|
||||
@ -2733,8 +2731,11 @@ module.exports = {
|
||||
SCOPES,
|
||||
WARNING_LEVEL,
|
||||
WORK_ORDER_WAREHOUSE,
|
||||
WORK_ORDER_WMS_CO_ID,
|
||||
WORK_ORDER_VOID_STATUS,
|
||||
WORK_ORDER_RECEIVED_STATUS,
|
||||
WORK_ORDER_INFO_TABLE,
|
||||
WORK_ORDER_ITEM_TABLE,
|
||||
EXPORT_MODES,
|
||||
buildProductExportRows,
|
||||
buildComponentExportRows,
|
||||
|
||||
@ -395,8 +395,12 @@ test('replenish: products without sales stay "no sale" even with a pending work
|
||||
|
||||
test('pending work order filters keep the business-confirmed source and exclusions', () => {
|
||||
assert.equal(_private.WORK_ORDER_WAREHOUSE, '简尚-崔桥工厂半成品包装项目仓');
|
||||
assert.equal(_private.WORK_ORDER_VOID_STATUS, '作废');
|
||||
assert.equal(_private.WORK_ORDER_RECEIVED_STATUS, '全部入库');
|
||||
// 2026-11-11 换源:聚水潭原生加工单三表(po_id 关联),状态值域为英文
|
||||
assert.equal(_private.WORK_ORDER_WMS_CO_ID, 15718196);
|
||||
assert.equal(_private.WORK_ORDER_VOID_STATUS, 'Cancelled');
|
||||
assert.equal(_private.WORK_ORDER_RECEIVED_STATUS, 'Finished');
|
||||
assert.equal(_private.WORK_ORDER_INFO_TABLE, 'rpt.jst_manufactureInfo');
|
||||
assert.equal(_private.WORK_ORDER_ITEM_TABLE, 'rpt.jst_manufactureItem');
|
||||
assert.equal(_private.WARNING_LEVEL.SCHEDULED, '已排产');
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user