From 2745cf18ffafc015da6cde59ab928f5a0612ada0 Mon Sep 17 00:00:00 2001 From: youruo <15018737611@163.com> Date: Mon, 28 Sep 2026 09:55:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BA=93=E5=AD=98=E5=8A=A8=E9=94=80?= =?UTF-8?q?=E5=88=86=E6=9E=90=E3=80=8C=E9=94=80=E5=94=AE=3D0=E3=80=8D?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E6=8C=89=E4=BB=93=E4=BD=8D=E6=8B=86=E8=A1=8C?= =?UTF-8?q?=EF=BC=8C=E5=BA=93=E5=AD=98=E6=95=B0=E9=87=8F=E6=8C=89=E5=AE=9E?= =?UTF-8?q?=E9=99=85=E4=BB=93=E4=BD=8D=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版把同一 SKU 的多个仓位用「、」拼在一格里,库存数量仍是 SKU 总量, 无法直接按仓位盘点。现改为每个仓位独立一行: - bin CTE 改为按 (wms_co_id, sku_id, LTRIM(RTRIM(bin))) 分组, SUM(qty) 得到仓位级库存(各仓位行之和 = SKU 总库存,实测 240=60+69+35+76) - 导出行 = graded(SKU 级「销售=0」判定)LEFT JOIN 仓位级明细: 有仓位数据的 SKU 按仓位展开多行、库存数量取该仓位实际 qty(qty=0 的仓位行剔除); 仓位表未覆盖的 SKU 保留一行(仓位为空、库存取总库存),不丢数据 - 排序:仓库 → SKU 总库存倒序 → SKU → 仓位名称,同一 SKU 的各行保持相邻 - 前端按钮提示同步更新拆行与兜底规则说明 验证:后端 syntax 通过、单测 9/9(全量 389/390,唯一失败为既有 Windows 环境项); 前端 eslint/build 通过; 本地实测:单仓(睢宁 196 行,原 128 SKU 行)样例 SKU 正确拆为 4 行且合计 240; 全仓导出 6798 行(原 3650),有仓位行 6529、仓位为空兜底行 269,覆盖 26 仓 --- backend/src/services/stockSalesAnalysis.js | 33 +++++++++---------- backend/test/stockSalesAnalysis.test.js | 22 ++++++++----- .../src/pages/StockSalesAnalysis/index.jsx | 2 +- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/backend/src/services/stockSalesAnalysis.js b/backend/src/services/stockSalesAnalysis.js index 84b380d..fbe7ac8 100644 --- a/backend/src/services/stockSalesAnalysis.js +++ b/backend/src/services/stockSalesAnalysis.js @@ -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>} */ 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, - p.sku_id AS skuId, - LTRIM(RTRIM(p.bin)) AS b - FROM rpt.jst_packInventoryInfo p - WHERE ISNULL(p.bin, '') <> '' - AND ISNULL(p.qty, 0) <> 0 - ) d - GROUP BY d.wmsCoId, d.skuId + SELECT p.wms_co_id AS wmsCoId, + p.sku_id AS skuId, + 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 + 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, diff --git a/backend/test/stockSalesAnalysis.test.js b/backend/test/stockSalesAnalysis.test.js index cec796c..a4c69ca 100644 --- a/backend/test/stockSalesAnalysis.test.js +++ b/backend/test/stockSalesAnalysis.test.js @@ -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')); }); diff --git a/frontend/src/pages/StockSalesAnalysis/index.jsx b/frontend/src/pages/StockSalesAnalysis/index.jsx index 34550b7..95507ef 100644 --- a/frontend/src/pages/StockSalesAnalysis/index.jsx +++ b/frontend/src/pages/StockSalesAnalysis/index.jsx @@ -209,7 +209,7 @@ const StockSalesAnalysis = () => { - +