'use strict'; /** * 拣选库位动销分析 —— 业务库取数 * * 库存:rpt.jst_packInventoryInfo 按 wms_co_id + 仓位 汇总 qty(仓位为空的行不参与) * 类型:bin_group 为空 → 拣选位;否则取 bin_group(业务库目前只有「存货」) * 销量:rpt.jst_Orders × rpt.jst_OrderItem,按 send_date,排除 Delete/Cancelled * 分摊:SKU 销量 ÷ 该 SKU 占用的同类型仓位数,再按库位累加 * 保证「全部库位销量之和 = 全仓销量」 * 日销:库位销量 / 窗口天数 → 分档得到动销等级 */ const { PICKING_BIN_GRADE_RULES } = require('../config/pickingBinSalesAnalysis'); /** mssql 不支持数组参数,按位展开成 @p0,@p1... */ function buildInClause(values, prefix) { const params = {}; const clause = values .map((v, i) => { const key = `${prefix}${i}`; params[key] = v; return `@${key}`; }) .join(','); return { clause, params }; } /** * 日销分档表达式:从高档往低档依次命中,最后一档「销售=0」由 salesQty <= 0 兜底。 */ function buildGradeCase(salesExpr = 'ISNULL(x.salesQty, 0)', daysParam = '@days') { const branches = PICKING_BIN_GRADE_RULES .filter((rule) => rule.lower > 0) .map((rule) => { const daily = `(${salesExpr} / ${daysParam})`; const lower = `${daily} >= ${rule.lower}`; const upper = rule.upper == null ? null : `${daily} < ${rule.upper}`; const cond = upper ? `${lower} AND ${upper}` : lower; return `WHEN ${cond} THEN '${rule.key}'`; }); return ` CASE ${branches.join('\n ')} WHEN ${salesExpr} > 0 THEN 'slow2' ELSE 'zero' END`; } /** * 拉取各仓库每个仓位的库存、销量、日销与动销等级。 * * @param {object} cloudDb 业务库实例 * @param {string[]} wmsCoIds 仓库编码 * @param {{shipFrom:string, shipTo:string, days:number, binTypes?:string[]}} options * @returns {Promise>} */ async function fetchPickingBinRows(cloudDb, wmsCoIds, options = {}) { if (!cloudDb || typeof cloudDb.query !== 'function') { throw new TypeError('fetchPickingBinRows 的第一个参数必须是业务库实例'); } const { shipFrom, shipTo, days, binTypes } = options; if (!Array.isArray(wmsCoIds) || wmsCoIds.length === 0) return []; if (!shipFrom || !shipTo || !days) return []; const binIn = buildInClause(wmsCoIds, 'w'); const shipIn = buildInClause(wmsCoIds, 'v'); const gradeCase = buildGradeCase(); // 仓位类型过滤(不传 = 全部类型) let typeFilter = ''; const typeParams = {}; if (Array.isArray(binTypes) && binTypes.length > 0) { const t = buildInClause(binTypes, 't'); Object.assign(typeParams, t.params); typeFilter = `AND a.binType IN (${t.clause})`; } const sqlText = ` WITH bin AS ( SELECT p.wms_co_id AS wmsCoId, LTRIM(RTRIM(p.bin)) AS binName, CASE WHEN ISNULL(p.bin_group, '') = '' THEN N'${'拣选'}' ELSE LTRIM(RTRIM(p.bin_group)) END AS binType, p.sku_id AS skuId, SUM(CAST(ISNULL(p.qty, 0) AS DECIMAL(18, 2))) AS binSkuQty FROM rpt.jst_packInventoryInfo p WHERE p.wms_co_id IN (${binIn.clause}) AND ISNULL(p.bin, '') <> '' AND ISNULL(p.qty, 0) <> 0 GROUP BY p.wms_co_id, LTRIM(RTRIM(p.bin)), CASE WHEN ISNULL(p.bin_group, '') = '' THEN N'${'拣选'}' ELSE LTRIM(RTRIM(p.bin_group)) END, p.sku_id ), binSpan AS ( SELECT wmsCoId, skuId, binType, COUNT(DISTINCT binName) AS binCount FROM bin GROUP BY wmsCoId, skuId, binType ), ship AS ( 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_OrderItem oi JOIN rpt.jst_Orders 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') GROUP BY o.wms_co_id, oi.sku_id ), binSales AS ( SELECT b.wmsCoId, b.binName, b.binType, SUM(CAST(ISNULL(s.salesQty, 0) AS DECIMAL(18, 4)) / NULLIF(sp.binCount, 0)) AS salesQty FROM bin b JOIN binSpan sp ON sp.wmsCoId = b.wmsCoId AND sp.skuId = b.skuId AND sp.binType = b.binType LEFT JOIN ship s ON s.wmsCoId = b.wmsCoId AND s.skuId = b.skuId GROUP BY b.wmsCoId, b.binName, b.binType ), binAgg AS ( SELECT wmsCoId, binName, binType, SUM(binSkuQty) AS stockQty, COUNT(DISTINCT skuId) AS skuCount FROM bin GROUP BY wmsCoId, binName, binType ) SELECT a.wmsCoId, a.binName, a.binType, CAST(a.stockQty AS DECIMAL(18, 2)) AS stockQty, CAST(a.skuCount AS INT) AS skuCount, CAST(ISNULL(x.salesQty, 0) AS DECIMAL(18, 2)) AS salesQty, CAST(ISNULL(x.salesQty, 0) / @days AS DECIMAL(18, 4)) AS dailySales, ${gradeCase} AS gradeKey FROM binAgg a LEFT JOIN binSales x ON x.wmsCoId = a.wmsCoId AND x.binName = a.binName AND x.binType = a.binType WHERE ABS(ISNULL(a.stockQty, 0)) > 0.0001 ${typeFilter} ORDER BY a.wmsCoId, a.binType, a.binName`; const rows = await cloudDb.query( sqlText, { ...binIn.params, ...shipIn.params, ...typeParams, shipFrom, shipTo, days, }, { requestTimeout: 120000 } ); return rows.map((r) => ({ wmsCoId: String(r.wmsCoId), binName: r.binName || '', binType: r.binType || '', stockQty: Number(r.stockQty) || 0, skuCount: Number(r.skuCount) || 0, salesQty: Number(r.salesQty) || 0, dailySales: Number(r.dailySales) || 0, gradeKey: r.gradeKey || 'zero', })); } module.exports = { buildGradeCase, buildInClause, fetchPickingBinRows, };