diff --git a/backend/src/config/pickingBinSalesAnalysis.js b/backend/src/config/pickingBinSalesAnalysis.js index 5097f69..550652d 100644 --- a/backend/src/config/pickingBinSalesAnalysis.js +++ b/backend/src/config/pickingBinSalesAnalysis.js @@ -13,7 +13,10 @@ * 库存数量:rpt.jst_packInventoryInfo 按 wms_co_id + 仓位 + sku_id 汇总 qty * 统计范围:只统计真实拣选仓位(pack_type=Bin 且 bin_group 为空); * 箱记录(pack_type=Pack)与存货位(bin_group=存货)不纳入 - * 销量: rpt.jst_Orders × rpt.jst_OrderItem,按 send_date,排除 Delete/Cancelled + * 销量: 4 个来源分支,均按 send_date 窗口(详见 services/pickingBinSalesAnalysis.js): + * ① jst_Orders 普通行;② jst_Orders 组合装行 → jst_comboProductItem 展开为子件; + * ③ jst_qmcust_orderlist(全民客户 B2B 分销)普通行(排除拆单父单 Split); + * ④ qmcust 组合装行 → 同样展开。 * 日销: 商品窗口销量 / 窗口天数(整档,不按仓位分摊) * 等级: 严格按商品日销判定,同一商品占多少个仓位都是同一等级 * diff --git a/backend/src/services/pickingBinSalesAnalysis.js b/backend/src/services/pickingBinSalesAnalysis.js index 9eaec7e..013cb8d 100644 --- a/backend/src/services/pickingBinSalesAnalysis.js +++ b/backend/src/services/pickingBinSalesAnalysis.js @@ -6,7 +6,12 @@ * 库存:rpt.jst_packInventoryInfo 按 wms_co_id + 仓位 + sku_id 汇总 qty(仓位为空的行不参与) * 范围:只统计真实拣选仓位(pack_type=Bin 且 bin_group 为空); * 箱记录(pack_type=Pack)与存货位(bin_group=存货)不纳入 - * 销量:rpt.jst_Orders × rpt.jst_OrderItem,按 send_date,排除 Delete/Cancelled + * 销量(4 个来源分支,均按 send_date 窗口): + * 1) rpt.jst_Orders × jst_OrderItem 普通行(sku_type<>combine),排除 Delete/Cancelled + * 2) rpt.jst_Orders 组合装行(sku_id 为 ZH-... 拼接串)→ JOIN jst_comboProductItem 展开为子件×数量 + * 3) rpt.jst_qmcust_orderlist(全民客户 B2B 分销)普通行,排除 Delete/Cancelled/Split/Question + * (Split 是拆单父单,与拆出的子单重复,必须排除) + * 4) rpt.jst_qmcust_orderlist 组合装行 → 同样按映射表展开 * 粒度:仓位 × 商品编码(一个仓位有 N 个商品就输出 N 行),列表与导出共用 * * 【日销与动销等级口径】按「商品日销」判定,不做仓位分摊: @@ -110,6 +115,7 @@ async function fetchPickingBinSkuRows(cloudDb, wmsCoIds, options = {}) { GROUP BY wmsCoId, skuId, binType ), ship AS ( + -- 1) 线上店铺订单:普通商品行(sku_type<>combine,组合装行在下一分支展开) SELECT o.wms_co_id AS wmsCoId, oi.sku_id AS skuId, SUM(CAST(ISNULL(oi.qty, 0) AS DECIMAL(18, 2))) AS salesQty @@ -119,7 +125,49 @@ async function fetchPickingBinSkuRows(cloudDb, wmsCoIds, options = {}) { AND o.send_date >= @shipFrom AND o.send_date < @shipTo AND ISNULL(o.status, '') NOT IN ('Delete', 'Cancelled') + AND ISNULL(oi.sku_type, 'normal') <> 'combine' GROUP BY o.wms_co_id, oi.sku_id + UNION ALL + -- 2) 线上店铺订单:组合装行(sku_id 为 ZH-子件A-N-子件B-M 拼接串)→ 按映射表展开为子件 + SELECT o.wms_co_id AS wmsCoId, + ci.src_sku_id AS skuId, + SUM(CAST(ISNULL(oi.qty, 0) * ISNULL(ci.qty, 0) AS DECIMAL(18, 2))) AS salesQty + FROM rpt.jst_OrderItem oi + JOIN rpt.jst_Orders o ON o.o_id = oi.o_id + JOIN rpt.jst_comboProductItem ci ON ci.combine_sku_id = oi.sku_id + WHERE o.wms_co_id IN (${shipIn.clause}) + AND o.send_date >= @shipFrom + AND o.send_date < @shipTo + AND ISNULL(o.status, '') NOT IN ('Delete', 'Cancelled') + AND oi.sku_type = 'combine' + GROUP BY o.wms_co_id, ci.src_sku_id + UNION ALL + -- 3) 全民客户(B2B 分销)订单:普通商品行(拆单父单 Split 与子单重复,需排除) + SELECT o.wms_co_id AS wmsCoId, + oi.sku_id AS skuId, + SUM(CAST(ISNULL(oi.qty, 0) AS DECIMAL(18, 2))) AS salesQty + FROM rpt.jst_qmcust_orderlist_item oi + JOIN rpt.jst_qmcust_orderlist o ON o.o_id = oi.o_id + WHERE o.wms_co_id IN (${shipIn.clause}) + AND o.send_date >= @shipFrom + AND o.send_date < @shipTo + AND ISNULL(o.status, '') NOT IN ('Delete', 'Cancelled', 'Split', 'Question') + AND ISNULL(oi.sku_type, 'normal') <> 'combine' + GROUP BY o.wms_co_id, oi.sku_id + UNION ALL + -- 4) 全民客户订单:组合装行展开为子件 + SELECT o.wms_co_id AS wmsCoId, + ci.src_sku_id AS skuId, + SUM(CAST(ISNULL(oi.qty, 0) * ISNULL(ci.qty, 0) AS DECIMAL(18, 2))) AS salesQty + FROM rpt.jst_qmcust_orderlist_item oi + JOIN rpt.jst_qmcust_orderlist o ON o.o_id = oi.o_id + JOIN rpt.jst_comboProductItem ci ON ci.combine_sku_id = oi.sku_id + WHERE o.wms_co_id IN (${shipIn.clause}) + AND o.send_date >= @shipFrom + AND o.send_date < @shipTo + AND ISNULL(o.status, '') NOT IN ('Delete', 'Cancelled', 'Split', 'Question') + AND oi.sku_type = 'combine' + GROUP BY o.wms_co_id, ci.src_sku_id ), binAgg AS ( SELECT wmsCoId, diff --git a/backend/test/pickingBinSalesAnalysis.test.js b/backend/test/pickingBinSalesAnalysis.test.js index 3e923e5..b6e5fa5 100644 --- a/backend/test/pickingBinSalesAnalysis.test.js +++ b/backend/test/pickingBinSalesAnalysis.test.js @@ -137,6 +137,25 @@ test('fetchPickingBinSkuRows 只统计真实拣选仓位,排除 Pack 箱记录 assert.equal(captured.params.binTypes, undefined); }); +test('销量覆盖 4 个来源:线上普通行 + 组合装展开 + 全民客户普通行 + 全民客户组合装展开', async () => { + let captured = null; + const db = { + query: async (sqlText, params) => { captured = { sqlText, params }; return []; }, + }; + await fetchPickingBinSkuRows(db, [101], { shipFrom: 'a', shipTo: 'b', days: 30 }); + + // ① 线上店铺订单普通行 + assert.ok(captured.sqlText.includes('FROM rpt.jst_OrderItem'), '缺少 jst_OrderItem 普通行分支'); + // ② 线上订单组合装行按映射表展开为子件 + assert.ok(captured.sqlText.includes('JOIN rpt.jst_comboProductItem'), '缺少组合装展开分支'); + assert.ok(captured.sqlText.includes("oi.sku_type = 'combine'"), '缺少组合装行识别条件'); + // ③ 全民客户(B2B 分销)渠道普通行,排除拆单父单 Split 与问题单 + assert.ok(captured.sqlText.includes('FROM rpt.jst_qmcust_orderlist_item'), '缺少 qmcust 分支'); + assert.ok(captured.sqlText.includes("'Split', 'Question'"), 'qmcust 未排除拆单父单/问题单'); + // 四个分支共用同一发货窗口 + assert.equal(captured.sqlText.split('o.send_date >= @shipFrom').length, 5); +}); + test('gradeKey 缺失时兜底为 zero', async () => { const db = { query: async () => [{ wmsCoId: 101, binName: 'X', binType: '拣选', skuId: 'S1', stockQty: 5 }] }; const rows = await fetchPickingBinSkuRows(db, [101], { shipFrom: 'a', shipTo: 'b', days: 15 }); diff --git a/frontend/src/constants/pickingBinSalesAnalysis.js b/frontend/src/constants/pickingBinSalesAnalysis.js index 10e40ae..26c75f9 100644 --- a/frontend/src/constants/pickingBinSalesAnalysis.js +++ b/frontend/src/constants/pickingBinSalesAnalysis.js @@ -29,7 +29,7 @@ export const PICKING_BIN_COLUMNS = [ { key: 'salesQty', label: '商品销量', - tip: '近 N 天该商品的销量(整档,不按仓位分摊):同一商品占几个仓位,这几行显示的销量都一样,所以这一列不能直接按行求和', + tip: '近 N 天该商品的销量(整档,不按仓位分摊):含线上订单、组合装子件拆分、全民客户(B2B分销)渠道;同一商品占几个仓位,这几行显示的销量都一样,所以这一列不能直接按行求和', }, { key: 'dailySales', @@ -85,6 +85,7 @@ export const PICKING_BIN_DESCRIPTION = [ ' 注意它小于全仓销量:jst_packInventoryInfo 只覆盖部分 SKU 的仓位数据,无仓位记录的 SKU 销量不计入任何库位。', '', '数据来源:库存取 rpt.jst_packInventoryInfo(按仓库 + 仓位 + 商品汇总);', - '销量取 rpt.jst_Orders × rpt.jst_OrderItem(按 send_date,排除 Delete/Cancelled),与「每日进销存」发货口径一致。', + '销量按 send_date 窗口统计 4 个来源:① jst_Orders 普通商品行;② jst_Orders 组合装行(ZH-拼接编码)按组合装映射展开为子件;', + '③ jst_qmcust_orderlist 全民客户(B2B分销)普通行(排除拆单父单 Split);④ qmcust 组合装行同样展开。', '统计范围:只取 pack_type=Bin 且 bin_group 为空的真实拣选仓位,箱记录(pack_type=Pack)和存货位(bin_group=存货)不参与统计。', ];