Compare commits
2 Commits
7505a04e37
...
e71607386d
| Author | SHA1 | Date | |
|---|---|---|---|
| e71607386d | |||
| 0afcb26119 |
153
backend/scripts/validateDailyInventoryFromBiz.js
Normal file
153
backend/scripts/validateDailyInventoryFromBiz.js
Normal file
@ -0,0 +1,153 @@
|
||||
'use strict';
|
||||
/**
|
||||
* 用业务库(JSReportReplica)反算流水,校验已导入的聚水潭官方进销存数据。
|
||||
* 用法(先起隧道): cd backend && node scripts/validateDailyInventoryFromBiz.js 2026-10-06
|
||||
*
|
||||
* 注意:业务库 jst_inventoryInfo 是 T-1 同步快照,只能精确校验「昨天」的期末;
|
||||
* 更早的历史日会带自然变动(实测 10-05 差 0.1%~1.2%),属正常。
|
||||
*/
|
||||
const mysql = require('mysql2/promise');
|
||||
const sql = require('mssql');
|
||||
|
||||
const BIZ_DATE = process.argv[2] || '2026-10-06';
|
||||
|
||||
const SYS = {
|
||||
host: '127.0.0.1',
|
||||
port: 13307,
|
||||
user: 'cangku_67c48c6660be787f',
|
||||
password: 'wIJugzi26RQHF4J4BgnwxCxxecp157goR93M_IXEhqc',
|
||||
database: 'shujuku2_system',
|
||||
};
|
||||
|
||||
// 业务库能还原的 4 个流水列(与 dailyInventoryFlows.js 口径一致,这里加单日上界)
|
||||
const FLOWS = [
|
||||
{
|
||||
field: 'purchase_in',
|
||||
label: '采购进仓',
|
||||
sql: `SELECT p.wms_co_id AS wmsCoId, SUM(CAST(ISNULL(pi.qty,0) AS DECIMAL(18,2))) AS qty
|
||||
FROM rpt.jst_purchase p JOIN rpt.jst_purchaseItem pi ON pi.po_id = p.po_id
|
||||
WHERE p.wms_co_id IN (%W%) AND p.po_date >= @from AND p.po_date < @to
|
||||
AND ISNULL(p.status,'') IN ('Confirmed','Finished') GROUP BY p.wms_co_id`,
|
||||
},
|
||||
{
|
||||
field: 'purchase_return',
|
||||
label: '采购退货',
|
||||
sql: `SELECT o.wms_co_id AS wmsCoId, SUM(CAST(ISNULL(oi.qty,0) AS DECIMAL(18,2))) AS qty
|
||||
FROM rpt.jst_purchaseout o JOIN rpt.jst_purchaseoutItem oi ON oi.io_id = o.io_id
|
||||
WHERE o.wms_co_id IN (%W%) AND o.io_date >= @from AND o.io_date < @to
|
||||
GROUP BY o.wms_co_id`,
|
||||
},
|
||||
{
|
||||
field: 'sales_out',
|
||||
label: '销售出仓',
|
||||
sql: `SELECT o.wms_co_id AS wmsCoId, SUM(CAST(ISNULL(oi.qty,0) AS DECIMAL(18,2))) AS qty
|
||||
FROM rpt.jst_Orders o JOIN rpt.jst_OrderItem oi ON oi.o_id = o.o_id
|
||||
WHERE o.wms_co_id IN (%W%) AND o.send_date >= @from AND o.send_date < @to
|
||||
AND ISNULL(o.status,'') NOT IN ('Delete','Cancelled') GROUP BY o.wms_co_id`,
|
||||
},
|
||||
{
|
||||
field: 'sales_return',
|
||||
label: '销售退货',
|
||||
sql: `SELECT r.wms_co_id AS wmsCoId, SUM(CAST(ISNULL(ri.r_qty,0) AS DECIMAL(18,2))) AS qty
|
||||
FROM rpt.jst_returnAndRefundInfo r JOIN rpt.jst_returnAndRefundItem ri ON ri.as_id = r.as_id
|
||||
WHERE r.wms_co_id IN (%W%) AND r.as_date >= @from AND r.as_date < @to
|
||||
AND ISNULL(r.type,'') IN (N'普通退货',N'拒收退货',N'换货',N'维修') GROUP BY r.wms_co_id`,
|
||||
},
|
||||
];
|
||||
|
||||
(async () => {
|
||||
const sys = await mysql.createConnection(SYS);
|
||||
const [official] = await sys.query(
|
||||
'SELECT wms_co_id, warehouse, opening_qty, closing_qty, purchase_in, purchase_return, sales_out, sales_return FROM daily_inventory_official WHERE DATE(biz_date)=? ORDER BY closing_qty DESC',
|
||||
[BIZ_DATE]
|
||||
);
|
||||
if (!official.length) {
|
||||
console.log('官方表无 ' + BIZ_DATE + ' 数据');
|
||||
await sys.end();
|
||||
return;
|
||||
}
|
||||
const ids = official.map((r) => String(r.wms_co_id)).filter(Boolean);
|
||||
const clause = ids.map((_, i) => '@w' + i).join(',');
|
||||
const wparams = {};
|
||||
ids.forEach((v, i) => (wparams['w' + i] = v));
|
||||
|
||||
// 业务库凭据从系统库 database_configs 现取(不落盘)
|
||||
const [cfgRows] = await sys.query(
|
||||
"SELECT host, port, database_name, username, password FROM database_configs WHERE name='cloud_database_public' LIMIT 1"
|
||||
);
|
||||
const biz = cfgRows[0];
|
||||
await sql.connect({
|
||||
server: biz.host,
|
||||
port: Number(biz.port),
|
||||
database: biz.database_name,
|
||||
user: biz.username,
|
||||
password: biz.password,
|
||||
options: { encrypt: false, trustServerCertificate: true },
|
||||
connectionTimeout: 20000,
|
||||
requestTimeout: 180000,
|
||||
});
|
||||
|
||||
const from = BIZ_DATE + ' 00:00:00';
|
||||
const d = new Date(BIZ_DATE + 'T00:00:00Z');
|
||||
d.setUTCDate(d.getUTCDate() + 1);
|
||||
const to = d.toISOString().slice(0, 10) + ' 00:00:00';
|
||||
|
||||
const computed = {};
|
||||
for (const f of FLOWS) {
|
||||
const req = new sql.Request();
|
||||
req.input('from', sql.DateTime, new Date(from));
|
||||
req.input('to', sql.DateTime, new Date(to));
|
||||
ids.forEach((v, i) => req.input('w' + i, sql.NVarChar, v));
|
||||
const r = await req.query(f.sql.replace('%W%', clause));
|
||||
r.recordset.forEach((row) => {
|
||||
computed[String(row.wmsCoId)] = computed[String(row.wmsCoId)] || {};
|
||||
computed[String(row.wmsCoId)][f.field] = Number(row.qty) || 0;
|
||||
});
|
||||
}
|
||||
|
||||
console.log('业务库口径反算 vs 聚水潭官方导出 —— ' + BIZ_DATE);
|
||||
console.log('(业务库仅能还原 4/12 列;采购退货/销售出仓为流出,官方值取绝对值比较)\n');
|
||||
const pad = (s, n) => String(s).padEnd(n);
|
||||
console.log(
|
||||
pad('仓库', 26) + pad('采购进仓 官方/业务库', 26) + pad('采购退货 官方/业务库', 26) + pad('销售出仓 官方/业务库', 26) + '销售退货 官方/业务库'
|
||||
);
|
||||
let diffs = { purchase_in: 0, sales_out: 0, purchase_return: 0, sales_return: 0 };
|
||||
for (const o of official) {
|
||||
const c = computed[String(o.wms_co_id)] || {};
|
||||
const cells = ['purchase_in', 'purchase_return', 'sales_out', 'sales_return'].map((k) => {
|
||||
const ov = Math.abs(Number(o[k]) || 0);
|
||||
const cv = Math.abs(Number(c[k]) || 0);
|
||||
if (Math.abs(ov - cv) > 0.005) diffs[k]++;
|
||||
return pad(ov + ' / ' + cv, 26);
|
||||
});
|
||||
console.log(pad(o.warehouse, 26) + cells.join(''));
|
||||
}
|
||||
console.log('\n不一致仓数:' + JSON.stringify(diffs) + ' / 共 ' + official.length + ' 仓');
|
||||
|
||||
// 期末 vs 业务库实时库存(仅作量级参考,业务库只有实时库存)
|
||||
const req2 = new sql.Request();
|
||||
ids.forEach((v, i) => req2.input('w' + i, sql.NVarChar, v));
|
||||
const inv = await req2.query(
|
||||
`SELECT wms_co_id AS wmsCoId, SUM(CAST(ISNULL(qty,0) AS DECIMAL(18,2))) AS qty
|
||||
FROM rpt.jst_inventoryInfo WHERE wms_co_id IN (${clause}) GROUP BY wms_co_id`
|
||||
);
|
||||
const invMap = {};
|
||||
inv.recordset.forEach((r) => (invMap[String(r.wmsCoId)] = Number(r.qty) || 0));
|
||||
console.log('\n【参考】官方' + BIZ_DATE + '期末 vs 业务库实时库存(业务库无历史快照,仅看量级)');
|
||||
official
|
||||
.slice()
|
||||
.sort((a, b) => Number(b.closing_qty) - Number(a.closing_qty))
|
||||
.slice(0, 8)
|
||||
.forEach((o) => {
|
||||
const live = invMap[String(o.wms_co_id)] || 0;
|
||||
const off = Number(o.closing_qty) || 0;
|
||||
const pct = off ? (((live - off) / off) * 100).toFixed(1) : '-';
|
||||
console.log(pad(o.warehouse, 26) + '期末 ' + off + ' | 实时 ' + live + ' | 差 ' + pct + '%');
|
||||
});
|
||||
|
||||
await sql.close();
|
||||
await sys.end();
|
||||
})().catch((e) => {
|
||||
console.log('FAIL', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user