- 新增 fetchPickingBinSkuRows:仓位 × 商品编码 粒度明细 库存数量 = 该商品在此仓位的实际库存;销量 = 商品销量 ÷ 其占用同类仓位数(贡献) - 导出列:仓库 / 仓位 / 类型 / 商品编码 / 库存数量 / SKU数 / 销量 / 日销 / 动销等级 / 统计天数 / 统计基准日 - SKU数、动销等级仍为仓位粒度(同一仓位多行重复),列表页口径不变(仍用 bin 级聚合) - 同一次取数比对:7055 个仓位,列表与导出库存/销量 0 处差异,库存合计一致 497721 - 单测 11 条通过
172 lines
6.3 KiB
JavaScript
172 lines
6.3 KiB
JavaScript
'use strict';
|
||
|
||
const test = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
|
||
const {
|
||
buildGradeCase,
|
||
buildInClause,
|
||
fetchPickingBinRows,
|
||
fetchPickingBinSkuRows,
|
||
} = require('../src/services/pickingBinSalesAnalysis');
|
||
const {
|
||
PICKING_BIN_GRADE_RULES,
|
||
PICKING_BIN_DEFAULT_DAYS,
|
||
PICKING_BIN_WINDOW_OPTIONS,
|
||
PICKING_BIN_DEFAULT_TYPES,
|
||
BIN_TYPES,
|
||
} = require('../src/config/pickingBinSalesAnalysis');
|
||
|
||
test('buildInClause 按位展开参数', () => {
|
||
const { clause, params } = buildInClause([101, 102], 'w');
|
||
assert.equal(clause, '@w0,@w1');
|
||
assert.deepEqual(params, { w0: 101, w1: 102 });
|
||
});
|
||
|
||
test('buildGradeCase 覆盖全部正向等级,并以销售=0 兜底', () => {
|
||
const expr = buildGradeCase();
|
||
PICKING_BIN_GRADE_RULES.filter((rule) => rule.lower > 0).forEach((rule) => {
|
||
assert.ok(expr.includes(`THEN '${rule.key}'`), `${rule.key} 未出现在分档表达式中`);
|
||
});
|
||
assert.ok(expr.includes("THEN 'slow2'"));
|
||
assert.ok(expr.includes("ELSE 'zero'"));
|
||
});
|
||
|
||
test('默认窗口为 30 天,默认只看拣选位', () => {
|
||
assert.equal(PICKING_BIN_DEFAULT_DAYS, 30);
|
||
assert.ok(PICKING_BIN_WINDOW_OPTIONS.includes(30));
|
||
assert.deepEqual(PICKING_BIN_DEFAULT_TYPES, ['拣选']);
|
||
assert.deepEqual(BIN_TYPES, ['拣选', '存货']);
|
||
});
|
||
|
||
test('fetchPickingBinRows 首参不是数据库实例时抛错', async () => {
|
||
await assert.rejects(
|
||
() => fetchPickingBinRows({}, [101], { shipFrom: 'a', shipTo: 'b', days: 15 }),
|
||
TypeError
|
||
);
|
||
});
|
||
|
||
test('fetchPickingBinRows 无仓库或缺失窗口时直接返回空结果,不打库', async () => {
|
||
let called = 0;
|
||
const db = { query: async () => { called += 1; return []; } };
|
||
assert.deepEqual(await fetchPickingBinRows(db, [], { shipFrom: 'a', shipTo: 'b', days: 15 }), []);
|
||
assert.deepEqual(await fetchPickingBinRows(db, [101], { shipFrom: null, shipTo: 'b', days: 15 }), []);
|
||
assert.equal(called, 0);
|
||
});
|
||
|
||
test('fetchPickingBinRows 把仓库/类型/窗口参数一并传给业务库,并做分摊与分档', async () => {
|
||
let captured = null;
|
||
const db = {
|
||
query: async (sqlText, params) => {
|
||
captured = { sqlText, params };
|
||
return [
|
||
{ wmsCoId: 101, binName: 'XA01-17-12', binType: '拣选', stockQty: 21, skuCount: 1, salesQty: 9, dailySales: 0.6, gradeKey: 'slow2' },
|
||
];
|
||
},
|
||
};
|
||
|
||
const rows = await fetchPickingBinRows(db, [101, 102], {
|
||
shipFrom: '2026-09-23 01:00:00',
|
||
shipTo: '2026-10-08 01:00:00',
|
||
days: 15,
|
||
binTypes: ['拣选'],
|
||
});
|
||
|
||
assert.equal(rows.length, 1);
|
||
assert.deepEqual(rows[0], {
|
||
wmsCoId: '101',
|
||
binName: 'XA01-17-12',
|
||
binType: '拣选',
|
||
stockQty: 21,
|
||
skuCount: 1,
|
||
salesQty: 9,
|
||
dailySales: 0.6,
|
||
gradeKey: 'slow2',
|
||
});
|
||
|
||
// 参数必须带上两个仓库、发货窗口与天数,以及仓位类型过滤
|
||
assert.equal(captured.params.w0, 101);
|
||
assert.equal(captured.params.w1, 102);
|
||
assert.equal(captured.params.v0, 101);
|
||
assert.equal(captured.params.shipFrom, '2026-09-23 01:00:00');
|
||
assert.equal(captured.params.shipTo, '2026-10-08 01:00:00');
|
||
assert.equal(captured.params.days, 15);
|
||
assert.equal(captured.params.t0, '拣选');
|
||
|
||
// 分摊:SKU 销量除以其占用的同类型仓位数
|
||
assert.ok(captured.sqlText.includes('/ NULLIF(sp.binCount, 0)'));
|
||
// 仓位类型:bin_group 为空视为拣选位
|
||
assert.ok(captured.sqlText.includes("THEN N'拣选'"));
|
||
// 只统计有库存的仓位
|
||
assert.ok(captured.sqlText.includes('ISNULL(p.qty, 0) <> 0'));
|
||
});
|
||
|
||
test('fetchPickingBinRows 不传类型时不过滤 binType', async () => {
|
||
let captured = null;
|
||
const db = {
|
||
query: async (sqlText, params) => { captured = { sqlText, params }; return []; },
|
||
};
|
||
await fetchPickingBinRows(db, [101], {
|
||
shipFrom: 'a', shipTo: 'b', days: 15, binTypes: [],
|
||
});
|
||
assert.ok(!captured.sqlText.includes('a.binType IN'));
|
||
assert.equal(captured.params.t0, undefined);
|
||
});
|
||
|
||
test('gradeKey 缺失时兜底为 zero', async () => {
|
||
const db = { query: async () => [{ wmsCoId: 101, binName: 'X', binType: '拣选', stockQty: 5 }] };
|
||
const rows = await fetchPickingBinRows(db, [101], { shipFrom: 'a', shipTo: 'b', days: 15 });
|
||
assert.equal(rows[0].gradeKey, 'zero');
|
||
assert.equal(rows[0].salesQty, 0);
|
||
assert.equal(rows[0].dailySales, 0);
|
||
});
|
||
|
||
test('fetchPickingBinSkuRows 首参不是数据库实例时抛错', async () => {
|
||
await assert.rejects(
|
||
() => fetchPickingBinSkuRows({}, [101], { shipFrom: 'a', shipTo: 'b', days: 30 }),
|
||
TypeError
|
||
);
|
||
});
|
||
|
||
test('fetchPickingBinSkuRows 缺失必要窗口参数时返回空数组', async () => {
|
||
const db = { query: async () => [{ skuId: 'S1' }] };
|
||
assert.deepEqual(await fetchPickingBinSkuRows(db, [], { shipFrom: 'a', shipTo: 'b', days: 30 }), []);
|
||
assert.deepEqual(await fetchPickingBinSkuRows(db, [101], { shipFrom: null, shipTo: 'b', days: 30 }), []);
|
||
assert.deepEqual(await fetchPickingBinSkuRows(db, [101], { shipFrom: 'a', shipTo: 'b', days: 0 }), []);
|
||
});
|
||
|
||
test('fetchPickingBinSkuRows 按仓位×商品编码输出并保留仓位口径字段', async () => {
|
||
let captured = null;
|
||
const db = {
|
||
query: async (sqlText, params) => {
|
||
captured = { sqlText, params };
|
||
return [
|
||
{
|
||
wmsCoId: 101, binName: 'XA01-01-01', binType: '拣选', skuId: 'SKU1',
|
||
stockQty: 12, skuCount: 2, salesQty: 30, dailySales: 1,
|
||
binSalesQty: 45, gradeKey: 'slow1',
|
||
},
|
||
{
|
||
wmsCoId: 101, binName: 'XA01-01-01', binType: '拣选', skuId: 'SKU2',
|
||
stockQty: 8, skuCount: 2, salesQty: 15, dailySales: 0.5,
|
||
binSalesQty: 45, gradeKey: 'slow1',
|
||
},
|
||
];
|
||
},
|
||
};
|
||
const rows = await fetchPickingBinSkuRows(db, [101], { shipFrom: 'a', shipTo: 'b', days: 30 });
|
||
assert.equal(rows.length, 2);
|
||
assert.deepEqual(rows.map((r) => r.skuId), ['SKU1', 'SKU2']);
|
||
// 一个仓位两个 SKU 拆成两行,库存为各商品在该仓位的实际库存
|
||
assert.deepEqual(rows.map((r) => r.stockQty), [12, 8]);
|
||
// 动销等级仍属仓位,同一仓位多行相同
|
||
assert.equal(rows[0].gradeKey, 'slow1');
|
||
assert.equal(rows[1].gradeKey, 'slow1');
|
||
assert.equal(rows[0].binSalesQty, 45);
|
||
assert.equal(rows[0].skuCount, 2);
|
||
// SQL 含商品编码列与倍数分摊
|
||
assert.ok(captured.sqlText.includes('b.skuId'));
|
||
assert.ok(captured.sqlText.includes('/ NULLIF(sp.binCount, 0)'));
|
||
assert.equal(captured.params.days, 30);
|
||
});
|