'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 };