Merge pull request 'feat: 库存动销分析「销售=0」导出按仓位拆行,库存数量按实际仓位统计' (#51) from codex/ssa-zero-export-bin-0928 into main
This commit is contained in:
commit
aeac4bcc81
@ -139,8 +139,11 @@ async function fetchGradeRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days }) {
|
||||
* 「销售=0」档的 SKU 级明细(导出用)。
|
||||
* 口径与 fetchGradeRows 完全一致:同一分档表达式命中 gradeKey='zero',
|
||||
* 即统计窗口内销量 <= 0 且「有库存或有销量」的 SKU。
|
||||
* 系统仓位取自 rpt.jst_packInventoryInfo.bin(一个 SKU 可能占多个仓位,按名称排序后用「、」连接;
|
||||
* 库存为 0 的仓位记录已排除)。该表只覆盖部分仓库,未覆盖到的 SKU 返回空串。
|
||||
*
|
||||
* 系统仓位取自 rpt.jst_packInventoryInfo.bin(jst_sku.bin 全为空、库存表无仓位字段)。
|
||||
* 每个仓位独立一行,库存数量按该仓位的实际 qty 统计(各仓位行之和 = SKU 总库存):
|
||||
* - 有仓位数据的 SKU:按 (wms_co_id, sku_id, bin) 展开,qty=0 的仓位行剔除;
|
||||
* - 无仓位数据的 SKU(仓位表未覆盖的仓库):保留一行,仓位为空、库存取 SKU 总库存,避免丢数。
|
||||
* @returns {Promise<Array<{wmsCoId:string, skuId:string, skuName:string, binName:string, stockQty:number, salesQty:number}>>}
|
||||
*/
|
||||
async function fetchZeroSaleSkuRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days }) {
|
||||
@ -156,18 +159,14 @@ async function fetchZeroSaleSkuRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days
|
||||
|
||||
const sqlText = `
|
||||
WITH bin AS (
|
||||
SELECT d.wmsCoId,
|
||||
d.skuId,
|
||||
STRING_AGG(CAST(d.b AS NVARCHAR(MAX)), '、') WITHIN GROUP (ORDER BY d.b) AS binName
|
||||
FROM (
|
||||
SELECT DISTINCT p.wms_co_id AS wmsCoId,
|
||||
SELECT p.wms_co_id AS wmsCoId,
|
||||
p.sku_id AS skuId,
|
||||
LTRIM(RTRIM(p.bin)) AS b
|
||||
LTRIM(RTRIM(p.bin)) AS b,
|
||||
SUM(CAST(ISNULL(p.qty, 0) AS DECIMAL(18, 2))) AS binQty
|
||||
FROM rpt.jst_packInventoryInfo p
|
||||
WHERE ISNULL(p.bin, '') <> ''
|
||||
AND ISNULL(p.qty, 0) <> 0
|
||||
) d
|
||||
GROUP BY d.wmsCoId, d.skuId
|
||||
GROUP BY p.wms_co_id, p.sku_id, LTRIM(RTRIM(p.bin))
|
||||
),
|
||||
inv AS (
|
||||
SELECT i.wms_co_id AS wmsCoId,
|
||||
@ -214,14 +213,14 @@ async function fetchZeroSaleSkuRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days
|
||||
SELECT g.wmsCoId,
|
||||
g.skuId,
|
||||
ISNULL(s.name, '') AS skuName,
|
||||
ISNULL(bn.binName, '') AS binName,
|
||||
CAST(g.stockQty AS DECIMAL(18, 2)) AS stockQty,
|
||||
ISNULL(bn.b, '') AS binName,
|
||||
CAST(COALESCE(bn.binQty, g.stockQty) AS DECIMAL(18, 2)) AS stockQty,
|
||||
CAST(g.salesQty AS DECIMAL(18, 2)) AS salesQty
|
||||
FROM graded g
|
||||
LEFT JOIN rpt.jst_sku s ON s.sku_id = g.skuId
|
||||
LEFT JOIN bin bn ON bn.wmsCoId = g.wmsCoId AND bn.skuId = g.skuId
|
||||
WHERE g.gradeKey = 'zero'
|
||||
ORDER BY g.wmsCoId, g.stockQty DESC, g.skuId`;
|
||||
ORDER BY g.wmsCoId, g.stockQty DESC, g.skuId, bn.b`;
|
||||
|
||||
const rows = await cloudDb.query(sqlText, {
|
||||
...invIn.params,
|
||||
|
||||
@ -94,7 +94,8 @@ test('fetchZeroSaleSkuRows 复用同一分档口径并关联 SKU 名称', async
|
||||
query: async (sqlText, params) => {
|
||||
captured = { sqlText, params };
|
||||
return [
|
||||
{ wmsCoId: 101, skuId: 'SKU-A', skuName: '款式A', binName: 'A01-01-1、A02-03-1', stockQty: 12, salesQty: 0 },
|
||||
{ wmsCoId: 101, skuId: 'SKU-A', skuName: '款式A', binName: 'A01-01-1', stockQty: 7, salesQty: 0 },
|
||||
{ wmsCoId: 101, skuId: 'SKU-A', skuName: '款式A', binName: 'A02-03-1', stockQty: 5, salesQty: 0 },
|
||||
{ wmsCoId: 101, skuId: 'SKU-B', skuName: '款式B', binName: null, stockQty: 3, salesQty: 0 },
|
||||
];
|
||||
},
|
||||
@ -106,14 +107,16 @@ test('fetchZeroSaleSkuRows 复用同一分档口径并关联 SKU 名称', async
|
||||
days: 30,
|
||||
});
|
||||
|
||||
assert.equal(rows.length, 2);
|
||||
// 每个仓位独立一行;无仓位的 SKU 保留一行(仓位为空、库存取总库存)
|
||||
assert.equal(rows.length, 3);
|
||||
assert.equal(rows[0].wmsCoId, '101');
|
||||
assert.equal(rows[0].skuName, '款式A');
|
||||
assert.equal(rows[0].stockQty, 12);
|
||||
|
||||
// 系统仓位:多仓位拼接成一个字符串,缺失时归一化为空串而不是 null
|
||||
assert.equal(rows[0].binName, 'A01-01-1、A02-03-1');
|
||||
assert.equal(rows[1].binName, '');
|
||||
assert.equal(rows[0].binName, 'A01-01-1');
|
||||
assert.equal(rows[0].stockQty, 7);
|
||||
assert.equal(rows[1].binName, 'A02-03-1');
|
||||
assert.equal(rows[1].stockQty, 5);
|
||||
assert.equal(rows[2].binName, '');
|
||||
assert.equal(rows[2].stockQty, 3);
|
||||
|
||||
// 口径必须与 fetchGradeRows 一致:同一段 gradeCase 表达式 + 同样的「有库存或有销量」过滤
|
||||
assert.ok(captured.sqlText.includes("ELSE 'zero'"));
|
||||
@ -123,9 +126,10 @@ test('fetchZeroSaleSkuRows 复用同一分档口径并关联 SKU 名称', async
|
||||
assert.equal(captured.params.w0, 101);
|
||||
assert.equal(captured.params.days, 30);
|
||||
|
||||
// 系统仓位:来自 rpt.jst_packInventoryInfo,按仓库+SKU 聚合,排除无仓位与零库存记录
|
||||
// 系统仓位:来自 rpt.jst_packInventoryInfo,按「仓库+SKU+仓位」聚合出仓位级库存,
|
||||
// 每仓位一行(COALESCE 兜底无仓位 SKU 取总库存),排除无仓位与零库存记录
|
||||
assert.ok(captured.sqlText.includes('rpt.jst_packInventoryInfo'));
|
||||
assert.ok(captured.sqlText.includes('STRING_AGG'));
|
||||
assert.ok(captured.sqlText.includes('COALESCE(bn.binQty, g.stockQty)'));
|
||||
assert.ok(captured.sqlText.includes('binName'));
|
||||
});
|
||||
|
||||
|
||||
@ -209,7 +209,7 @@ const StockSalesAnalysis = () => {
|
||||
<Tooltip title="跳过 5 分钟缓存,重新扫描订单明细与库存明细重算(约 5~20 秒)">
|
||||
<Button onClick={() => load({ days, force: true })} loading={loading}>强制重算</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="导出当前筛选下「销售=0」档的 SKU 明细(仓库 / SKU编码 / SKU名称 / 系统仓位 / 库存数量),跟随统计天数与仓库筛选;一个 SKU 占多个仓位时用「、」连接,仓位数据未覆盖的 SKU 留空">
|
||||
<Tooltip title="导出当前筛选下「销售=0」档的 SKU 明细(仓库 / SKU编码 / SKU名称 / 系统仓位 / 库存数量),跟随统计天数与仓库筛选;每个仓位独立一行,库存数量为该仓位的实际数量,仓位数据未覆盖的 SKU 保留一行(仓位留空、库存为总量)">
|
||||
<Button icon={<DownloadOutlined />} onClick={handleExportZero} loading={exporting}>导出销售=0</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user