口径校准(对照聚水潭进销存导出 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 类单据),显著更准。
92 lines
4.1 KiB
JavaScript
92 lines
4.1 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 每日进销存 —— 期初快照缺失时的兜底推导
|
||
*
|
||
* 正常情况下「期初 = 上一业务日快照」(每日 01:30 自动落库)。
|
||
* 若快照缺失(服务停机、新纳入仓库等),用「实时库存扣流水回推」推导
|
||
* 「上一业务日期末结存」并补写快照:
|
||
*
|
||
* closing(D-1) = 当前实时库存 − 自 D 01:00 以来的净流入
|
||
* 净流入 = 采购进仓 + 销售退货 − 销售出仓 − 采购退货(带符号流水求和,见 dailyInventoryFlows)
|
||
*
|
||
* 口径校准(2026-09-27,对照聚水潭进销存导出 09-23 期初基准):
|
||
* 崔桥工厂仓 基准 92388 / 回推 ≈89650;横林二仓 基准 63780 / 回推 ≈64029;
|
||
* 山东青岗集 基准 88377 / 回推 ≈88210;河南洛宁 基准 83305 / 回推 ≈82473。
|
||
* 偏差 0.3%~2.7%,来源为业务库缺失的 8 类单据(调拨/盘点/加工/其它出入库)。
|
||
*
|
||
* 注意:不要用 rpt.pm_stockMonitor 聚合 jsStock 充当全仓结存 ——
|
||
* 该表只覆盖纳入监控范围的 SKU 子集(横林二仓实测 1197 vs 全仓 63780),
|
||
* 系统性偏低,已在校准后弃用。
|
||
*/
|
||
|
||
const { fetchFlows } = require('./dailyInventoryFlows');
|
||
|
||
/**
|
||
* 推导 openingDate 的期末快照并补写。
|
||
* @param {Object} opts
|
||
* @param {Object} opts.cloudDb 业务库(mssql)
|
||
* @param {Object} opts.snapshotDb 本地快照表访问模块
|
||
* @param {Array} opts.baseline 仓库基线(warehouse/dbWarehouse/wmsCoId)
|
||
* @param {string} opts.openingDate 期初业务日(YYYY-MM-DD,即要补快照的日期)
|
||
* @param {string} opts.latestBizDate 最新业务日(YYYY-MM-DD,流水起点 = 其当日 01:00)
|
||
* @param {Date|null} opts.takenAt 实时库存数据时间
|
||
* @returns {Promise<{count:number, derivedCount:number}>}
|
||
*/
|
||
async function deriveAndStoreOpeningSnapshot(opts) {
|
||
const { cloudDb, snapshotDb, baseline, openingDate, latestBizDate, takenAt } = opts;
|
||
if (!openingDate || !latestBizDate) return { count: 0, derivedCount: 0 };
|
||
const whs = baseline.filter((b) => b.wmsCoId != null);
|
||
const wmsCoIds = whs.map((b) => b.wmsCoId);
|
||
const flowFrom = `${latestBizDate} 01:00:00`;
|
||
|
||
// 实时库存(与每日快照任务同口径:jst_inventoryInfo 按仓聚合)
|
||
let realtimeMap = new Map();
|
||
try {
|
||
const rows = await cloudDb.query(
|
||
`SELECT i.wms_co_id AS wmsCoId,
|
||
ISNULL(SUM(CAST(i.qty AS DECIMAL(18, 2))), 0) AS qty,
|
||
SUM(CASE WHEN ABS(ISNULL(i.qty, 0)) > 0.0001 THEN 1 ELSE 0 END) AS skuCount
|
||
FROM rpt.jst_inventoryInfo i
|
||
WHERE i.wms_co_id IN (${wmsCoIds.map((_, i) => `@w${i}`).join(',')})
|
||
GROUP BY i.wms_co_id`,
|
||
Object.fromEntries(wmsCoIds.map((id, i) => [`w${i}`, id])),
|
||
{ requestTimeout: 120000 }
|
||
);
|
||
realtimeMap = new Map(rows.map((r) => [String(r.wmsCoId), r]));
|
||
} catch (error) {
|
||
console.error('[daily-inventory-opening] 实时库存聚合失败:', error.message);
|
||
}
|
||
|
||
// 自期初时刻以来的带符号流水(流出为负)
|
||
let flowMap = new Map();
|
||
try {
|
||
flowMap = await fetchFlows(cloudDb, wmsCoIds, flowFrom);
|
||
} catch (error) {
|
||
console.error('[daily-inventory-opening] 流水取数失败:', error.message);
|
||
}
|
||
|
||
let derivedCount = 0;
|
||
const payload = whs.map((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);
|
||
const closingQty = Number(rt?.qty ?? 0) - netInflow;
|
||
derivedCount += 1;
|
||
return {
|
||
wmsCoId: b.wmsCoId,
|
||
warehouse: b.warehouse,
|
||
dbWarehouse: b.dbWarehouse,
|
||
closingQty: Number(closingQty.toFixed(2)),
|
||
skuCount: Number(rt?.skuCount ?? 0),
|
||
takenAt: takenAt ? new Date(takenAt) : null,
|
||
};
|
||
});
|
||
|
||
const count = await snapshotDb.upsertSnapshot(openingDate, payload);
|
||
console.log(`[daily-inventory-opening] 已补写 ${openingDate} 快照 ${count}/${whs.length} 仓(实时库存扣流水回推)`);
|
||
return { count, derivedCount };
|
||
}
|
||
|
||
module.exports = { deriveAndStoreOpeningSnapshot };
|