cangku/backend/test/dailyInventoryOfficial.test.js
youruo 0e77a70c33 feat: 每日进销存支持导入聚水潭官方进销存校正数据
业务库只能还原 4/12 个流水列且口径与聚水潭不一致(采购按单据日期、销售按订单发货日期,
jst_salesOutboundInfo.qty 全为 0),调拨/盘点/加工/其它出入库无单据表,故以官方导出为准:

- 新增本地表 daily_inventory_official(biz_date + warehouse 存期初/12 流水列/期末)
- 新增解析与导入服务:xlsx 解析、仓库 ID 映射、「期初+流水=期末」恒等式校验
- GET /api/daily-inventory 支持 ?date= 指定业务日,官方值优先、缺失回退推算并返回来源标记
- 新增 POST /api/daily-inventory/official-import(xlsx 上传/JSON)与 GET /official 查询
- 导入后回写本地库存快照(官方期初→上一日期末、官方期末→当日期末),使推算也对齐
- 前端:业务日切换、官方/推算来源标记、导入弹窗、口径说明同步更新
- 仓库清单补入「简尚-横林三仓主仓」(聚水潭导出中存在)
- 命令行导入:npm run inventory:import-official -- <日期> <xlsx>
2026-09-27 14:16:06 +08:00

102 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const XLSX = require('xlsx');
const {
normalizeRecords,
parseWorkbook,
checkBalance,
} = require('../src/services/dailyInventoryOfficial');
const RECORDS = [
{
一级功能按键: '大货库存监控',
二级功能按键: '每日进销存',
仓库: '简尚-崔桥工厂仓主仓',
期初数量: 90811,
加工进仓: 0,
采购退货: 0,
采购进仓: 0,
其它进仓: 0,
其它退货: 0,
销售出仓: -3252,
盘点: 0,
加工出仓: 0,
调拨出: -881,
销售退货: 0,
调拨入: 4144,
期初: 0,
其它出库: 0,
期末数量: 90822,
},
{
仓库: '简尚-新外贸仓主仓',
期初数量: 5165,
销售出仓: -1743,
期末数量: 3422,
},
];
test('normalizeRecords 解析官方导出并映射仓库 ID', () => {
const { rows, warnings } = normalizeRecords(RECORDS);
assert.equal(rows.length, 2);
assert.deepEqual(warnings, []);
const [cuiqiao] = rows;
assert.equal(cuiqiao.warehouse, '简尚-崔桥工厂仓主仓');
assert.equal(cuiqiao.wmsCoId, 15038426); // 来自项目仓库清单
assert.equal(cuiqiao.openingQty, 90811);
assert.equal(cuiqiao.closingQty, 90822);
assert.equal(cuiqiao.flows.salesOut, -3252);
assert.equal(cuiqiao.flows.transferIn, 4144);
assert.equal(cuiqiao.flows.otherIn, 0); // 导出里显式为 0 的列保留 0
// 导出里没有出现的流水列保持 null,不能被当成 0
assert.equal(rows[1].flows.transferOut, null);
assert.equal(rows[1].flows.stocktaking, null);
// 导出里的第二个「期初」列不参与映射
assert.equal(Object.prototype.hasOwnProperty.call(cuiqiao.flows, 'openingQty'), false);
});
test('normalizeRecords 跳过无仓库名的行,未匹配仓库的 wmsCoId 为 null', () => {
const { rows } = normalizeRecords([
{ 仓库: '', 期初数量: 1, 期末数量: 1 },
{ 仓库: '某某新仓主仓', 期初数量: 10, 期末数量: 10 },
]);
assert.equal(rows.length, 1);
assert.equal(rows[0].warehouse, '某某新仓主仓');
assert.equal(rows[0].wmsCoId, null);
});
test('checkBalance 校验「期初 + 流水 = 期末」,不闭合的仓库会被列出', () => {
const { rows } = normalizeRecords(RECORDS);
assert.deepEqual(checkBalance(rows), []);
const broken = checkBalance([{
warehouse: '差异仓', openingQty: 100, closingQty: 90, flows: { salesOut: -5 },
}]);
assert.equal(broken.length, 1);
assert.equal(broken[0].diff, 5);
});
test('parseWorkbook 能从 xlsx 缓冲区还原仓库行(表头不在首行也能定位)', () => {
const grid = [
['说明', '这一行是导出时附加的标题'],
['一级功能按键', '二级功能按键', '仓库', '期初数量', '销售出仓', '调拨入', '期末数量'],
['大货库存监控', '每日进销存', '简尚-崔桥工厂仓主仓', 90811, -3252, 4144, 90822],
['', '', '简尚-新外贸仓主仓', 5165, -1743, 0, 3422],
];
const ws = XLSX.utils.aoa_to_sheet(grid);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Sheet2');
const buffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
const { rows, sheetName } = parseWorkbook(buffer);
assert.equal(sheetName, 'Sheet2');
assert.equal(rows.length, 2);
assert.equal(rows[0].warehouse, '简尚-崔桥工厂仓主仓');
assert.equal(rows[0].flows.transferIn, 4144);
assert.equal(rows[1].closingQty, 3422);
});