业务库只能还原 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>
102 lines
3.5 KiB
JavaScript
102 lines
3.5 KiB
JavaScript
'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);
|
||
});
|