数据补充(全部来自数据库,无人工填数): - 流水列接入 4/12:采购进仓(jst_purchase×Item,po_date,Confirmed/Finished)、 采购退货(jst_purchaseout×Item,io_date)、销售出仓(jst_Orders×OrderItem,send_date, 与周转率发货口径一致)、销售退货(jst_returnAndRefund×Item,as_date,实收 r_qty)。 其余 8 列业务库确无单据表(已核实全库表清单),保持「—」待 DBA 建表 - 期初快照缺失时自动兜底:优先用 rpt.pm_stockMonitor(报表库 01:00 跑批 T-1 库存, 按 dbWarehouse 聚合 jsStock,精确);监控表没有的虚拟仓(运营专用/京东仓)用 实时库存扣自期初时刻以来的带符号流水回推(近似),推导结果补写快照逐日滚动 缺陷修复: - 合计行仓库名显示 NaN:isTotal 渲染分支拦截了所有列,'合计' 被强转数字; 现仓库名原样显示,无数据来源的流水列合计也显示「—」而非误导性 0 - 合计行:某列全部仓库均无数据时合计置空,避免 0 误读 新增 4 项单元测试(流水符号约定、参数展开、期初双路径推导)。
108 lines
4.2 KiB
JavaScript
108 lines
4.2 KiB
JavaScript
'use strict';
|
||
|
||
const test = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
|
||
const {
|
||
AVAILABLE_FLOW_FIELDS,
|
||
buildInClause,
|
||
fetchFlows,
|
||
} = require('../src/services/dailyInventoryFlows');
|
||
const { deriveAndStoreOpeningSnapshot } = require('../src/services/dailyInventoryOpening');
|
||
|
||
const BASELINE = [
|
||
{ warehouse: '仓A主仓', dbWarehouse: '仓A', wmsCoId: 101 },
|
||
{ warehouse: '仓B主仓', dbWarehouse: '仓B', wmsCoId: 102 },
|
||
{ warehouse: '仓C主仓', dbWarehouse: null, wmsCoId: 103 },
|
||
];
|
||
|
||
/** 构造一个按 SQL 关键字路由的假业务库 */
|
||
function fakeCloudDb(handler) {
|
||
return { query: async (sql, params) => handler(String(sql), params || {}) };
|
||
}
|
||
|
||
test('buildInClause 展开参数且不为 async', () => {
|
||
const { clause, params } = buildInClause([1, 2], 'w');
|
||
assert.equal(clause, '@w0,@w1');
|
||
assert.deepEqual(params, { w0: 1, w1: 2 });
|
||
});
|
||
|
||
test('fetchFlows 查询四类单据并按符号约定返回(流入正、流出负)', async () => {
|
||
const seen = [];
|
||
const db = fakeCloudDb((sql, params) => {
|
||
seen.push(sql);
|
||
if (sql.includes('jst_purchaseItem')) return [{ wmsCoId: 101, qty: '100' }];
|
||
if (sql.includes('jst_purchaseoutItem')) return [{ wmsCoId: 101, qty: '30' }];
|
||
if (sql.includes('jst_OrderItem')) return [{ wmsCoId: 101, qty: '50' }];
|
||
if (sql.includes('jst_returnAndRefundItem')) return [{ wmsCoId: 101, qty: '8' }];
|
||
return [];
|
||
});
|
||
const map = await fetchFlows(db, [101, 102], '2026-09-26 01:00:00');
|
||
const f = map.get('101');
|
||
assert.equal(f.purchaseIn, 100, '采购进仓为流入正数');
|
||
assert.equal(f.purchaseReturn, -30, '采购退货为流出负数');
|
||
assert.equal(f.salesOut, -50, '销售出仓为流出负数');
|
||
assert.equal(f.salesReturn, 8, '销售退货为流入正数');
|
||
assert.equal(seen.length, 4, '应发出 4 条单据查询');
|
||
assert.ok(seen.every((s) => s.includes('@from')), '每条查询都带 @from 参数');
|
||
assert.deepEqual(AVAILABLE_FLOW_FIELDS.sort(), ['purchaseIn', 'purchaseReturn', 'salesOut', 'salesReturn']);
|
||
});
|
||
|
||
test('fetchFlows 空仓库列表直接返回空 Map', async () => {
|
||
const map = await fetchFlows(fakeCloudDb(() => []), [], '2026-09-26 01:00:00');
|
||
assert.equal(map.size, 0);
|
||
});
|
||
|
||
test('deriveAndStoreOpeningSnapshot:监控表命中走精确值,未命中用实时库存扣流水回推', async () => {
|
||
const upserts = [];
|
||
const cloudDb = fakeCloudDb((sql, params) => {
|
||
if (sql.includes('pm_stockMonitor')) {
|
||
// 只有仓A在监控表(精确路径)
|
||
return [{ warehouse: '仓A', qty: '500', skuCount: '12' }];
|
||
}
|
||
if (sql.includes('jst_inventoryInfo')) {
|
||
return [
|
||
{ wmsCoId: 101, qty: '520', skuCount: '13' },
|
||
{ wmsCoId: 102, qty: '300', skuCount: '9' },
|
||
{ wmsCoId: 103, qty: '10', skuCount: '2' },
|
||
];
|
||
}
|
||
if (sql.includes('jst_purchaseItem')) return [{ wmsCoId: 102, qty: '60' }];
|
||
if (sql.includes('jst_purchaseoutItem')) return [{ wmsCoId: 102, qty: '10' }];
|
||
if (sql.includes('jst_OrderItem')) return [{ wmsCoId: 102, qty: '40' }];
|
||
if (sql.includes('jst_returnAndRefundItem')) return [];
|
||
return [];
|
||
});
|
||
const snapshotDb = {
|
||
async upsertSnapshot(bizDate, rows) {
|
||
upserts.push({ bizDate, rows });
|
||
return rows.length;
|
||
},
|
||
};
|
||
|
||
const result = await deriveAndStoreOpeningSnapshot({
|
||
cloudDb,
|
||
snapshotDb,
|
||
baseline: BASELINE,
|
||
openingDate: '2026-09-25',
|
||
latestBizDate: '2026-09-26',
|
||
takenAt: null,
|
||
});
|
||
|
||
assert.equal(result.count, 3);
|
||
assert.equal(result.pmCount, 1, '仅仓A走 pm_stockMonitor 精确路径');
|
||
assert.equal(result.derivedCount, 2, '仓B/仓C走流水回推路径');
|
||
assert.equal(upserts.length, 1);
|
||
assert.equal(upserts[0].bizDate, '2026-09-25');
|
||
|
||
const byWh = new Map(upserts[0].rows.map((r) => [r.wmsCoId, r]));
|
||
// 仓A:精确值 500,不受流水影响
|
||
assert.equal(byWh.get(101).closingQty, 500);
|
||
// 仓B:300 − 净流入(60 − 10 − 40) = 300 − 10 = 290
|
||
assert.equal(byWh.get(102).closingQty, 290);
|
||
// 仓C:无流水 → 等于实时库存 10
|
||
assert.equal(byWh.get(103).closingQty, 10);
|
||
// 带回基线仓库名,保证后续按 wmsCoId 读取正确
|
||
assert.equal(byWh.get(102).warehouse, '仓B主仓');
|
||
});
|