'use strict'; /** * 仓库库存动销分析 —— 业务库取数 * * 一次查询覆盖全部仓库 × 全部动销等级: * 库存件数:rpt.jst_inventoryInfo 实时库存(按 wms_co_id + sku_id 汇总) * 销量: rpt.jst_OrderItem × rpt.jst_Orders,按 send_date,排除 Delete/Cancelled * (与「每日进销存」的发货口径一致) * 日销: 销量 / 统计窗口天数 → 分档得到动销等级 * * 只统计「有库存或有销量」的 SKU:库存为 0 且窗口内无销量的 SKU 已下架, * 计入「销售=0」会虚增分母,故整体剔除。 */ const { SALES_GRADE_RULES } = require('../config/stockSalesAnalysis'); /** 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` 兜底(销量为 NULL 视同 0)。 */ function buildGradeCase(salesExpr = 'ISNULL(b.salesQty, 0)', daysParam = '@days') { const branches = SALES_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`; } /** * 每个仓库 × 每个动销等级的 SKU 数、库存件数、销量。 * @returns {Promise>} */ async function fetchGradeRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days }) { if (!cloudDb || typeof cloudDb.query !== 'function') { throw new TypeError('fetchGradeRows 的第一个参数必须是业务库实例'); } if (!Array.isArray(wmsCoIds) || wmsCoIds.length === 0) return []; if (!shipFrom || !shipTo || !days) return []; const invIn = buildInClause(wmsCoIds, 'w'); const shipIn = buildInClause(wmsCoIds, 'v'); const gradeCase = buildGradeCase(); const sqlText = ` WITH inv AS ( SELECT i.wms_co_id AS wmsCoId, i.sku_id AS skuId, SUM(CAST(ISNULL(i.qty, 0) AS DECIMAL(18, 2))) AS stockQty FROM rpt.jst_inventoryInfo i WHERE i.wms_co_id IN (${invIn.clause}) GROUP BY i.wms_co_id, i.sku_id ), 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 ), base AS ( SELECT u.wmsCoId, u.skuId, SUM(u.stockQty) AS stockQty, SUM(u.salesQty) AS salesQty FROM ( SELECT wmsCoId, skuId, stockQty, CAST(0 AS DECIMAL(18, 2)) AS salesQty FROM inv UNION ALL SELECT wmsCoId, skuId, CAST(0 AS DECIMAL(18, 2)) AS stockQty, salesQty FROM ship ) u GROUP BY u.wmsCoId, u.skuId ), graded AS ( SELECT b.wmsCoId, b.stockQty, b.salesQty, ${gradeCase} AS gradeKey FROM base b WHERE ABS(ISNULL(b.stockQty, 0)) > 0.0001 OR ABS(ISNULL(b.salesQty, 0)) > 0.0001 ) SELECT g.wmsCoId, g.gradeKey, COUNT(*) AS skuCount, CAST(SUM(ISNULL(g.stockQty, 0)) AS DECIMAL(18, 2)) AS stockQty, CAST(SUM(ISNULL(g.salesQty, 0)) AS DECIMAL(18, 2)) AS salesQty, CAST(SUM(ISNULL(g.salesQty, 0) / @days) AS DECIMAL(18, 4)) AS dailySales FROM graded g GROUP BY g.wmsCoId, g.gradeKey`; const rows = await cloudDb.query(sqlText, { ...invIn.params, ...shipIn.params, shipFrom, shipTo, days, }, { requestTimeout: 180000 }); return rows.map((r) => ({ wmsCoId: String(r.wmsCoId), gradeKey: String(r.gradeKey), skuCount: Number(r.skuCount) || 0, stockQty: Number(r.stockQty) || 0, salesQty: Number(r.salesQty) || 0, dailySales: Number(r.dailySales) || 0, })); } /** * 「销售=0」档的 SKU 级明细(导出用)。 * 口径与 fetchGradeRows 完全一致:同一分档表达式命中 gradeKey='zero', * 即统计窗口内销量 <= 0 且「有库存或有销量」的 SKU。 * 系统仓位取自 rpt.jst_packInventoryInfo.bin(一个 SKU 可能占多个仓位,按名称排序后用「、」连接; * 库存为 0 的仓位记录已排除)。该表只覆盖部分仓库,未覆盖到的 SKU 返回空串。 * @returns {Promise>} */ async function fetchZeroSaleSkuRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days }) { if (!cloudDb || typeof cloudDb.query !== 'function') { throw new TypeError('fetchZeroSaleSkuRows 的第一个参数必须是业务库实例'); } if (!Array.isArray(wmsCoIds) || wmsCoIds.length === 0) return []; if (!shipFrom || !shipTo || !days) return []; const invIn = buildInClause(wmsCoIds, 'w'); const shipIn = buildInClause(wmsCoIds, 'v'); const gradeCase = buildGradeCase(); 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 ), inv AS ( SELECT i.wms_co_id AS wmsCoId, i.sku_id AS skuId, SUM(CAST(ISNULL(i.qty, 0) AS DECIMAL(18, 2))) AS stockQty FROM rpt.jst_inventoryInfo i WHERE i.wms_co_id IN (${invIn.clause}) GROUP BY i.wms_co_id, i.sku_id ), 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 ), base AS ( SELECT u.wmsCoId, u.skuId, SUM(u.stockQty) AS stockQty, SUM(u.salesQty) AS salesQty FROM ( SELECT wmsCoId, skuId, stockQty, CAST(0 AS DECIMAL(18, 2)) AS salesQty FROM inv UNION ALL SELECT wmsCoId, skuId, CAST(0 AS DECIMAL(18, 2)) AS stockQty, salesQty FROM ship ) u GROUP BY u.wmsCoId, u.skuId ), graded AS ( SELECT b.wmsCoId, b.skuId, b.stockQty, b.salesQty, ${gradeCase} AS gradeKey FROM base b WHERE ABS(ISNULL(b.stockQty, 0)) > 0.0001 OR ABS(ISNULL(b.salesQty, 0)) > 0.0001 ) SELECT g.wmsCoId, g.skuId, ISNULL(s.name, '') AS skuName, ISNULL(bn.binName, '') AS binName, CAST(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`; const rows = await cloudDb.query(sqlText, { ...invIn.params, ...shipIn.params, shipFrom, shipTo, days, }, { requestTimeout: 180000 }); return rows.map((r) => ({ wmsCoId: String(r.wmsCoId), skuId: String(r.skuId ?? ''), skuName: r.skuName == null ? '' : String(r.skuName), binName: r.binName == null ? '' : String(r.binName), stockQty: Number(r.stockQty) || 0, salesQty: Number(r.salesQty) || 0, })); } module.exports = { buildInClause, buildGradeCase, fetchGradeRows, fetchZeroSaleSkuRows, };