/** * 每日进销存 —— 聚水潭「官方进销存导出」校正数据 * * 背景:业务库(MSSQL / JSReportReplica)只能还原 4/12 个流水列,且口径与 * 聚水潭进销存报表不一致(采购按单据日期 vs 实际入库、销售出仓按订单发货 vs 出库单等), * 其余 8 个流水列(调拨/盘点/加工/其它出入库)业务库根本没有对应单据表。 * 因此「每日进销存」以聚水潭官方导出的进销存报表为准: * 官方有值 → 直接采用(source = official) * 官方无值 → 回退业务库自动推算(source = derived) * * 存储:本地 MySQL 表 daily_inventory_official,主键 (biz_date, warehouse)。 */ const { getPool } = require('./dailyInventorySnapshotDb'); const TABLE = 'daily_inventory_official'; /** 流水列 field(与 dailyInventoryBaseline.FLOW_COLUMNS 一致)→ 数据库列名 */ const FLOW_DB_COLUMNS = { processIn: 'process_in', purchaseReturn: 'purchase_return', purchaseIn: 'purchase_in', otherIn: 'other_in', otherReturn: 'other_return', salesOut: 'sales_out', stocktaking: 'stocktaking', processOut: 'process_out', transferOut: 'transfer_out', salesReturn: 'sales_return', transferIn: 'transfer_in', otherOut: 'other_out', }; const CREATE_SQL = ` CREATE TABLE IF NOT EXISTS ${TABLE} ( id BIGINT AUTO_INCREMENT PRIMARY KEY, biz_date DATE NOT NULL COMMENT '业务日期', wms_co_id BIGINT NULL COMMENT '聚水潭仓库 ID(未知仓为空)', warehouse VARCHAR(128) NOT NULL COMMENT '仓库名(聚水潭主仓)', opening_qty DECIMAL(18,2) NULL COMMENT '期初数量', closing_qty DECIMAL(18,2) NULL COMMENT '期末数量', process_in DECIMAL(18,2) NULL COMMENT '加工进仓', purchase_return DECIMAL(18,2) NULL COMMENT '采购退货', purchase_in DECIMAL(18,2) NULL COMMENT '采购进仓', other_in DECIMAL(18,2) NULL COMMENT '其它进仓', other_return DECIMAL(18,2) NULL COMMENT '其它退货', sales_out DECIMAL(18,2) NULL COMMENT '销售出仓', stocktaking DECIMAL(18,2) NULL COMMENT '盘点', process_out DECIMAL(18,2) NULL COMMENT '加工出仓', transfer_out DECIMAL(18,2) NULL COMMENT '调拨出', sales_return DECIMAL(18,2) NULL COMMENT '销售退货', transfer_in DECIMAL(18,2) NULL COMMENT '调拨入', other_out DECIMAL(18,2) NULL COMMENT '其它出库', source VARCHAR(32) NOT NULL DEFAULT 'jst-export' COMMENT '数据来源', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_date_wh (biz_date, warehouse), KEY idx_date (biz_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日进销存-聚水潭官方进销存校正数据';`; async function ensureTable() { await getPool().query(CREATE_SQL); } const num = (v) => { if (v == null || v === '') return null; const n = Number(v); return Number.isFinite(n) ? n : null; }; /** * 写入某业务日的官方进销存(幂等,重复导入覆盖) * @param {string} bizDate * @param {Array<{warehouse:string, wmsCoId?:number|null, openingQty?:number|null, closingQty?:number|null, flows?:Object}>} rows * @param {string} source */ async function upsertOfficial(bizDate, rows, source = 'jst-export') { if (!bizDate || !rows.length) return 0; await ensureTable(); const conn = await getPool().getConnection(); try { for (const r of rows) { const flows = r.flows || {}; await conn.query( `INSERT INTO ${TABLE} (biz_date, wms_co_id, warehouse, opening_qty, closing_qty, process_in, purchase_return, purchase_in, other_in, other_return, sales_out, stocktaking, process_out, transfer_out, sales_return, transfer_in, other_out, source) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE wms_co_id = VALUES(wms_co_id), opening_qty = VALUES(opening_qty), closing_qty = VALUES(closing_qty), process_in = VALUES(process_in), purchase_return = VALUES(purchase_return), purchase_in = VALUES(purchase_in), other_in = VALUES(other_in), other_return = VALUES(other_return), sales_out = VALUES(sales_out), stocktaking = VALUES(stocktaking), process_out = VALUES(process_out), transfer_out = VALUES(transfer_out), sales_return = VALUES(sales_return), transfer_in = VALUES(transfer_in), other_out = VALUES(other_out), source = VALUES(source)`, [ bizDate, r.wmsCoId ?? null, r.warehouse, num(r.openingQty), num(r.closingQty), num(flows.processIn), num(flows.purchaseReturn), num(flows.purchaseIn), num(flows.otherIn), num(flows.otherReturn), num(flows.salesOut), num(flows.stocktaking), num(flows.processOut), num(flows.transferOut), num(flows.salesReturn), num(flows.transferIn), num(flows.otherOut), source, ] ); } return rows.length; } finally { conn.release(); } } /** 删除某业务日的官方数据(重新导入前清理) */ async function deleteOfficial(bizDate) { if (!bizDate) return 0; await ensureTable(); const [res] = await getPool().query(`DELETE FROM ${TABLE} WHERE biz_date = ?`, [bizDate]); return res.affectedRows || 0; } /** 读取某业务日的官方进销存:key = 仓库名 */ async function fetchOfficial(bizDate) { const map = new Map(); if (!bizDate) return map; try { await ensureTable(); const cols = Object.values(FLOW_DB_COLUMNS).map((c) => `o.${c} AS ${c}`).join(', '); const [rows] = await getPool().query( `SELECT o.wms_co_id AS wmsCoId, o.warehouse AS warehouse, o.opening_qty AS openingQty, o.closing_qty AS closingQty, o.source AS source, ${cols} FROM ${TABLE} o WHERE o.biz_date = ?`, [bizDate] ); const dbToField = Object.fromEntries(Object.entries(FLOW_DB_COLUMNS).map(([f, c]) => [c, f])); rows.forEach((r) => { const flows = {}; for (const [col, field] of Object.entries(dbToField)) flows[field] = num(r[col]); map.set(String(r.warehouse), { warehouse: r.warehouse, wmsCoId: r.wmsCoId == null ? null : Number(r.wmsCoId), openingQty: num(r.openingQty), closingQty: num(r.closingQty), source: r.source, flows, }); }); } catch (error) { console.error('[daily-inventory-official] 读取官方数据失败:', error.message); } return map; } /** 已有官方数据的业务日(倒序) */ async function listOfficialDates(limit = 30) { try { await ensureTable(); const [rows] = await getPool().query( `SELECT DISTINCT biz_date AS d FROM ${TABLE} ORDER BY biz_date DESC LIMIT ?`, [Number(limit) || 30] ); return rows.map((r) => (typeof r.d === 'string' ? r.d.slice(0, 10) : String(r.d).slice(0, 10))); } catch (error) { console.error('[daily-inventory-official] 读取官方日期失败:', error.message); return []; } } module.exports = { TABLE, CREATE_SQL, FLOW_DB_COLUMNS, ensureTable, upsertOfficial, deleteOfficial, fetchOfficial, listOfficialDates, };