fix: 期初兜底弃用 pm_stockMonitor,全部改用实时库存扣流水回推
口径校准(对照聚水潭进销存导出 09-23 期初基准): 崔桥工厂仓 基准 92388 / pm 路径 114286 / 回推 ≈89650 横林二仓 基准 63780 / pm 路径 1197 / 回推 ≈64029 山东青岗集 基准 88377 / pm 路径 67339 / 回推 ≈88210 河南洛宁 基准 83305 / pm 路径 76992 / 回推 ≈82473 pm_stockMonitor.jsStock 只覆盖纳入监控范围的 SKU 子集(横林二仓实测 1197 vs 全仓 63780),系统性偏低,不能充当全仓结存;「实时库存扣 流水回推」与基准偏差 0.3%~2.7%(来自缺失的 8 类单据),显著更准。
This commit is contained in:
parent
952c57a84a
commit
c47a25b002
@ -4,19 +4,20 @@
|
|||||||
* 每日进销存 —— 期初快照缺失时的兜底推导
|
* 每日进销存 —— 期初快照缺失时的兜底推导
|
||||||
*
|
*
|
||||||
* 正常情况下「期初 = 上一业务日快照」(每日 01:30 自动落库)。
|
* 正常情况下「期初 = 上一业务日快照」(每日 01:30 自动落库)。
|
||||||
* 若快照缺失(新纳入仓库、服务停机等),按两条路推导出「上一业务日期末结存」并补写快照:
|
* 若快照缺失(服务停机、新纳入仓库等),用「实时库存扣流水回推」推导
|
||||||
|
* 「上一业务日期末结存」并补写快照:
|
||||||
*
|
*
|
||||||
* 路径 A(精确):rpt.pm_stockMonitor(报表库每日 01:00 跑批,T-1 口径)
|
* closing(D-1) = 当前实时库存 − 自 D 01:00 以来的净流入
|
||||||
* 该表 date = 最新业务日 D 的行,就是「D-1 期末」的库存监控值,
|
* 净流入 = 采购进仓 + 销售退货 − 销售出仓 − 采购退货(带符号流水求和,见 dailyInventoryFlows)
|
||||||
* 按 warehouse(= 监控表仓库名 dbWarehouse)聚合 jsStock 即可。
|
|
||||||
* 覆盖基线中与监控表仓库名一致的仓库(实测 14/26,
|
|
||||||
* 运营专用仓 / 京东仓等虚拟仓不在监控表中)。
|
|
||||||
*
|
*
|
||||||
* 路径 B(推导):实时库存扣流水回推
|
* 口径校准(2026-09-27,对照聚水潭进销存导出 09-23 期初基准):
|
||||||
* closing(D-1) = 实时库存 − 自 D 01:00 以来的净流入
|
* 崔桥工厂仓 基准 92388 / 回推 ≈89650;横林二仓 基准 63780 / 回推 ≈64029;
|
||||||
* 净流入 = 采购进仓 + 销售退货 − 销售出仓 − 采购退货(带符号流水求和)
|
* 山东青岗集 基准 88377 / 回推 ≈88210;河南洛宁 基准 83305 / 回推 ≈82473。
|
||||||
* 仅覆盖路径 A 匹配不到的仓库。因业务库缺 8 类单据表,
|
* 偏差 0.3%~2.7%,来源为业务库缺失的 8 类单据(调拨/盘点/加工/其它出入库)。
|
||||||
* 若该仓有未记录的调拨/盘点等流水,推导值会偏离真实历史,属近似值。
|
*
|
||||||
|
* 注意:不要用 rpt.pm_stockMonitor 聚合 jsStock 充当全仓结存 ——
|
||||||
|
* 该表只覆盖纳入监控范围的 SKU 子集(横林二仓实测 1197 vs 全仓 63780),
|
||||||
|
* 系统性偏低,已在校准后弃用。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { fetchFlows } = require('./dailyInventoryFlows');
|
const { fetchFlows } = require('./dailyInventoryFlows');
|
||||||
@ -28,45 +29,18 @@ const { fetchFlows } = require('./dailyInventoryFlows');
|
|||||||
* @param {Object} opts.snapshotDb 本地快照表访问模块
|
* @param {Object} opts.snapshotDb 本地快照表访问模块
|
||||||
* @param {Array} opts.baseline 仓库基线(warehouse/dbWarehouse/wmsCoId)
|
* @param {Array} opts.baseline 仓库基线(warehouse/dbWarehouse/wmsCoId)
|
||||||
* @param {string} opts.openingDate 期初业务日(YYYY-MM-DD,即要补快照的日期)
|
* @param {string} opts.openingDate 期初业务日(YYYY-MM-DD,即要补快照的日期)
|
||||||
* @param {string} opts.latestBizDate 最新业务日(YYYY-MM-DD,pm_stockMonitor 的 date)
|
* @param {string} opts.latestBizDate 最新业务日(YYYY-MM-DD,流水起点 = 其当日 01:00)
|
||||||
* @param {Date|null} opts.takenAt 实时库存数据时间
|
* @param {Date|null} opts.takenAt 实时库存数据时间
|
||||||
* @returns {Promise<{count:number, pmCount:number, derivedCount:number}>}
|
* @returns {Promise<{count:number, derivedCount:number}>}
|
||||||
*/
|
*/
|
||||||
async function deriveAndStoreOpeningSnapshot(opts) {
|
async function deriveAndStoreOpeningSnapshot(opts) {
|
||||||
const { cloudDb, snapshotDb, baseline, openingDate, latestBizDate, takenAt } = opts;
|
const { cloudDb, snapshotDb, baseline, openingDate, latestBizDate, takenAt } = opts;
|
||||||
if (!openingDate || !latestBizDate) return { count: 0, pmCount: 0, derivedCount: 0 };
|
if (!openingDate || !latestBizDate) return { count: 0, derivedCount: 0 };
|
||||||
const whs = baseline.filter((b) => b.wmsCoId != null);
|
const whs = baseline.filter((b) => b.wmsCoId != null);
|
||||||
const wmsCoIds = whs.map((b) => b.wmsCoId);
|
const wmsCoIds = whs.map((b) => b.wmsCoId);
|
||||||
const flowFrom = `${latestBizDate} 01:00:00`;
|
const flowFrom = `${latestBizDate} 01:00:00`;
|
||||||
|
|
||||||
// 路径 A:pm_stockMonitor 按仓库名聚合
|
// 实时库存(与每日快照任务同口径:jst_inventoryInfo 按仓聚合)
|
||||||
const dbNames = whs.map((b) => b.dbWarehouse).filter(Boolean);
|
|
||||||
let pmMap = new Map();
|
|
||||||
if (dbNames.length) {
|
|
||||||
const params = {};
|
|
||||||
const inCls = dbNames.map((n, i) => {
|
|
||||||
params[`n${i}`] = n;
|
|
||||||
return `@n${i}`;
|
|
||||||
}).join(',');
|
|
||||||
try {
|
|
||||||
const rows = await cloudDb.query(
|
|
||||||
`SELECT warehouse,
|
|
||||||
SUM(CAST(ISNULL(jsStock, 0) AS DECIMAL(18, 2))) AS qty,
|
|
||||||
SUM(CASE WHEN ISNULL(jsStock, 0) > 0.0001 THEN 1 ELSE 0 END) AS skuCount
|
|
||||||
FROM rpt.pm_stockMonitor
|
|
||||||
WHERE CONVERT(VARCHAR(10), date, 23) = @bizDate
|
|
||||||
AND warehouse IN (${inCls})
|
|
||||||
GROUP BY warehouse`,
|
|
||||||
{ ...params, bizDate: latestBizDate },
|
|
||||||
{ requestTimeout: 120000 }
|
|
||||||
);
|
|
||||||
pmMap = new Map(rows.map((r) => [r.warehouse, r]));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[daily-inventory-opening] pm_stockMonitor 聚合失败:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 路径 B:实时库存 + 流水回推
|
|
||||||
let realtimeMap = new Map();
|
let realtimeMap = new Map();
|
||||||
try {
|
try {
|
||||||
const rows = await cloudDb.query(
|
const rows = await cloudDb.query(
|
||||||
@ -84,6 +58,7 @@ async function deriveAndStoreOpeningSnapshot(opts) {
|
|||||||
console.error('[daily-inventory-opening] 实时库存聚合失败:', error.message);
|
console.error('[daily-inventory-opening] 实时库存聚合失败:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自期初时刻以来的带符号流水(流出为负)
|
||||||
let flowMap = new Map();
|
let flowMap = new Map();
|
||||||
try {
|
try {
|
||||||
flowMap = await fetchFlows(cloudDb, wmsCoIds, flowFrom);
|
flowMap = await fetchFlows(cloudDb, wmsCoIds, flowFrom);
|
||||||
@ -91,39 +66,26 @@ async function deriveAndStoreOpeningSnapshot(opts) {
|
|||||||
console.error('[daily-inventory-opening] 流水取数失败:', error.message);
|
console.error('[daily-inventory-opening] 流水取数失败:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pmCount = 0;
|
|
||||||
let derivedCount = 0;
|
let derivedCount = 0;
|
||||||
const payload = whs.map((b) => {
|
const payload = whs.map((b) => {
|
||||||
const pm = pmMap.get(b.dbWarehouse);
|
const rt = realtimeMap.get(String(b.wmsCoId));
|
||||||
let closingQty;
|
const f = flowMap.get(String(b.wmsCoId)) || {};
|
||||||
let skuCount;
|
const netInflow = (f.purchaseIn || 0) + (f.salesReturn || 0) + (f.salesOut || 0) + (f.purchaseReturn || 0);
|
||||||
if (pm) {
|
const closingQty = Number(rt?.qty ?? 0) - netInflow;
|
||||||
// 路径 A:报表库 01:00 跑批的 T-1 库存监控值(精确)
|
derivedCount += 1;
|
||||||
closingQty = Number(pm.qty) || 0;
|
|
||||||
skuCount = Number(pm.skuCount) || 0;
|
|
||||||
pmCount += 1;
|
|
||||||
} else {
|
|
||||||
// 路径 B:实时库存 − 净流入(带符号流水求和,流出为负)
|
|
||||||
const rt = realtimeMap.get(String(b.wmsCoId));
|
|
||||||
const f = flowMap.get(String(b.wmsCoId)) || {};
|
|
||||||
const netInflow = (f.purchaseIn || 0) + (f.salesReturn || 0) + (f.salesOut || 0) + (f.purchaseReturn || 0);
|
|
||||||
closingQty = Number(rt?.qty ?? 0) - netInflow;
|
|
||||||
skuCount = Number(rt?.skuCount ?? 0);
|
|
||||||
derivedCount += 1;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
wmsCoId: b.wmsCoId,
|
wmsCoId: b.wmsCoId,
|
||||||
warehouse: b.warehouse,
|
warehouse: b.warehouse,
|
||||||
dbWarehouse: b.dbWarehouse,
|
dbWarehouse: b.dbWarehouse,
|
||||||
closingQty: Number(closingQty.toFixed(2)),
|
closingQty: Number(closingQty.toFixed(2)),
|
||||||
skuCount,
|
skuCount: Number(rt?.skuCount ?? 0),
|
||||||
takenAt: takenAt ? new Date(takenAt) : null,
|
takenAt: takenAt ? new Date(takenAt) : null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const count = await snapshotDb.upsertSnapshot(openingDate, payload);
|
const count = await snapshotDb.upsertSnapshot(openingDate, payload);
|
||||||
console.log(`[daily-inventory-opening] 已补写 ${openingDate} 快照 ${count}/${whs.length} 仓(pm_stockMonitor 精确 ${pmCount},流水回推 ${derivedCount})`);
|
console.log(`[daily-inventory-opening] 已补写 ${openingDate} 快照 ${count}/${whs.length} 仓(实时库存扣流水回推)`);
|
||||||
return { count, pmCount, derivedCount };
|
return { count, derivedCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { deriveAndStoreOpeningSnapshot };
|
module.exports = { deriveAndStoreOpeningSnapshot };
|
||||||
|
|||||||
@ -53,13 +53,9 @@ test('fetchFlows 空仓库列表直接返回空 Map', async () => {
|
|||||||
assert.equal(map.size, 0);
|
assert.equal(map.size, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('deriveAndStoreOpeningSnapshot:监控表命中走精确值,未命中用实时库存扣流水回推', async () => {
|
test('deriveAndStoreOpeningSnapshot:全部仓库用实时库存扣流水回推,并保留基线仓库名', async () => {
|
||||||
const upserts = [];
|
const upserts = [];
|
||||||
const cloudDb = fakeCloudDb((sql, params) => {
|
const cloudDb = fakeCloudDb((sql, params) => {
|
||||||
if (sql.includes('pm_stockMonitor')) {
|
|
||||||
// 只有仓A在监控表(精确路径)
|
|
||||||
return [{ warehouse: '仓A', qty: '500', skuCount: '12' }];
|
|
||||||
}
|
|
||||||
if (sql.includes('jst_inventoryInfo')) {
|
if (sql.includes('jst_inventoryInfo')) {
|
||||||
return [
|
return [
|
||||||
{ wmsCoId: 101, qty: '520', skuCount: '13' },
|
{ wmsCoId: 101, qty: '520', skuCount: '13' },
|
||||||
@ -90,20 +86,20 @@ test('deriveAndStoreOpeningSnapshot:监控表命中走精确值,未命中用
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(result.count, 3);
|
assert.equal(result.count, 3);
|
||||||
assert.equal(result.pmCount, 1, '仅仓A走 pm_stockMonitor 精确路径');
|
assert.equal(result.derivedCount, 3);
|
||||||
assert.equal(result.derivedCount, 2, '仓B/仓C走流水回推路径');
|
|
||||||
assert.equal(upserts.length, 1);
|
assert.equal(upserts.length, 1);
|
||||||
assert.equal(upserts[0].bizDate, '2026-09-25');
|
assert.equal(upserts[0].bizDate, '2026-09-25');
|
||||||
|
|
||||||
const byWh = new Map(upserts[0].rows.map((r) => [r.wmsCoId, r]));
|
const byWh = new Map(upserts[0].rows.map((r) => [r.wmsCoId, r]));
|
||||||
// 仓A:精确值 500,不受流水影响
|
// 仓A:无流水 → 等于实时库存 520
|
||||||
assert.equal(byWh.get(101).closingQty, 500);
|
assert.equal(byWh.get(101).closingQty, 520);
|
||||||
// 仓B:300 − 净流入(60 − 10 − 40) = 300 − 10 = 290
|
// 仓B:300 − 净流入(60 − 10 − 40) = 300 − 10 = 290
|
||||||
assert.equal(byWh.get(102).closingQty, 290);
|
assert.equal(byWh.get(102).closingQty, 290);
|
||||||
// 仓C:无流水 → 等于实时库存 10
|
// 仓C:无流水 → 等于实时库存 10
|
||||||
assert.equal(byWh.get(103).closingQty, 10);
|
assert.equal(byWh.get(103).closingQty, 10);
|
||||||
// 带回基线仓库名,保证后续按 wmsCoId 读取正确
|
// 带回基线仓库名,保证后续按 wmsCoId 读取正确
|
||||||
assert.equal(byWh.get(102).warehouse, '仓B主仓');
|
assert.equal(byWh.get(102).warehouse, '仓B主仓');
|
||||||
|
assert.equal(byWh.get(102).dbWarehouse, '仓B');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetchFlows 缺少数据库实例参数时立即抛错(防静默空结果)', async () => {
|
test('fetchFlows 缺少数据库实例参数时立即抛错(防静默空结果)', async () => {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const FLOW_CALIBER_NOTE = '口径说明:与聚水潭进销存导出可能存
|
|||||||
/** 表格存在两个名为「期初」的列,第二个加后缀区分 */
|
/** 表格存在两个名为「期初」的列,第二个加后缀区分 */
|
||||||
export const DAILY_INVENTORY_COLUMNS = [
|
export const DAILY_INVENTORY_COLUMNS = [
|
||||||
{ key: 'warehouse', label: '仓库', source: 'snapshot', tip: '取自主数据表 rpt.jst_warehouseInfo 的仓库名,表格显示为「XX主仓」' },
|
{ key: 'warehouse', label: '仓库', source: 'snapshot', tip: '取自主数据表 rpt.jst_warehouseInfo 的仓库名,表格显示为「XX主仓」' },
|
||||||
{ key: 'openingQty', label: '期初数量', source: 'snapshot', tip: '上一业务日的期末结存,取自本地快照表 daily_inventory_snapshot(每日 01:30 自动落库);快照缺失时自动用 pm_stockMonitor(精确)或实时库存扣流水(近似)推导补写' },
|
{ key: 'openingQty', label: '期初数量', source: 'snapshot', tip: '上一业务日的期末结存,取自本地快照表 daily_inventory_snapshot(每日 01:30 自动落库);快照缺失时自动用「实时库存扣流水」回推补写(对照聚水潭导出基准偏差 0.3%~2.7%,来自业务库缺失的 8 类单据)' },
|
||||||
{ key: 'processIn', label: '加工进仓', source: 'unavailable', tip: `加工进仓。${NO_SOURCE_TIP}` },
|
{ key: 'processIn', label: '加工进仓', source: 'unavailable', tip: `加工进仓。${NO_SOURCE_TIP}` },
|
||||||
{ key: 'purchaseReturn', label: '采购退货', source: 'runtime', tip: `采购退货(流出,负数)。rpt.jst_purchaseout × jst_purchaseoutItem,按 io_date 统计。${FLOW_CALIBER_NOTE}` },
|
{ key: 'purchaseReturn', label: '采购退货', source: 'runtime', tip: `采购退货(流出,负数)。rpt.jst_purchaseout × jst_purchaseoutItem,按 io_date 统计。${FLOW_CALIBER_NOTE}` },
|
||||||
{ key: 'purchaseIn', label: '采购进仓', source: 'runtime', tip: `采购进仓(流入)。rpt.jst_purchase × jst_purchaseItem,按 po_date 统计,仅计 Confirmed/Finished 状态。${FLOW_CALIBER_NOTE}` },
|
{ key: 'purchaseIn', label: '采购进仓', source: 'runtime', tip: `采购进仓(流入)。rpt.jst_purchase × jst_purchaseItem,按 po_date 统计,仅计 Confirmed/Finished 状态。${FLOW_CALIBER_NOTE}` },
|
||||||
|
|||||||
@ -287,7 +287,7 @@ const DailyInventory = () => {
|
|||||||
message="全部数据来自数据库,流水列已接入 4/12"
|
message="全部数据来自数据库,流水列已接入 4/12"
|
||||||
description={
|
description={
|
||||||
<span>
|
<span>
|
||||||
期初数量取本地快照表中 <b>{data?.openingDate || '—'}</b> 的期末结存(每日 01:30 自动落库,逐日滚动;快照缺失时自动用库存监控表/实时库存扣流水推导补写);
|
期初数量取本地快照表中 <b>{data?.openingDate || '—'}</b> 的期末结存(每日 01:30 自动落库,逐日滚动;快照缺失时自动用「实时库存扣流水」回推补写);
|
||||||
期末数量与右侧 5 列为运行时从业务库实时取数。
|
期末数量与右侧 5 列为运行时从业务库实时取数。
|
||||||
流水列中采购进仓/采购退货/销售出仓/销售退货已从业务库单据表取数;
|
流水列中采购进仓/采购退货/销售出仓/销售退货已从业务库单据表取数;
|
||||||
其余 8 列(加工/其它/盘点/调拨)因业务库缺单据表仍显示「—」,已向 DBA 提交补表需求,因此「期初+流水」与「期末」暂不相等。
|
其余 8 列(加工/其它/盘点/调拨)因业务库缺单据表仍显示「—」,已向 DBA 提交补表需求,因此「期初+流水」与「期末」暂不相等。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user