feat: 拣选库位动销分析固定只统计拣选位,移除仓位类型切换
- 取数固定只取拣选位:bin CTE 直接过滤 bin_group 为空(更早过滤,存货位不再进入后续 CTE) 移除可变的 binType IN 过滤与 resolveBinTypes,binType 字段恒为拣选 - 接口 payload 的 binTypes / binTypeOptions 固定为 ['拣选'];缓存键不再含类型 - 列表与导出移除「类型」列(全为拣选,冗余);页面移除类型多选器、底部文案改为「统计范围:仅拣选位」 - 口径说明补充统计范围;Tooltip 同步 验证(基准日 2026-10-08、30 天): - 列表 11871 行 / 6821 个去重商品 / 库存 499141 / 去重商品销量 96847,明细类型集合仅为 ['拣选'] - 导出 11871 行,表头无「类型」列;分摊销量合计 96847 = 去重商品销量合计 96847(差异 -0.0017 浮点舍入) - 旧参数 binType=存货 被忽略(仍返回拣选且命中同一缓存);7 / 60 天窗口与单仓过滤均正常 - 单测 9/9 通过;后端语法检查 182 文件通过;前端 eslint 无报错、vite build 成功
This commit is contained in:
parent
8f12b889c0
commit
99fa402092
@ -6,8 +6,7 @@ const {
|
||||
PICKING_BIN_WINDOW_OPTIONS,
|
||||
PICKING_BIN_DEFAULT_DAYS,
|
||||
PICKING_BIN_TOTAL_LABEL,
|
||||
BIN_TYPES,
|
||||
PICKING_BIN_DEFAULT_TYPES,
|
||||
PICKING_BIN_BIN_TYPES,
|
||||
} = require('../config/pickingBinSalesAnalysis');
|
||||
const { fetchPickingBinSkuRows } = require('../services/pickingBinSalesAnalysis');
|
||||
const { sendWorkbookDownload } = require('../services/exportWorkbook');
|
||||
@ -60,18 +59,6 @@ function resolveDays(raw) {
|
||||
return PICKING_BIN_WINDOW_OPTIONS.includes(n) ? n : PICKING_BIN_DEFAULT_DAYS;
|
||||
}
|
||||
|
||||
/** 解析仓位类型,只保留已知类型;空 → 默认(拣选位) */
|
||||
function resolveBinTypes(raw) {
|
||||
if (raw === undefined || raw === null || String(raw).trim() === '') {
|
||||
return [...PICKING_BIN_DEFAULT_TYPES];
|
||||
}
|
||||
const list = String(raw)
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => BIN_TYPES.includes(v));
|
||||
return list.length > 0 ? list : [...PICKING_BIN_DEFAULT_TYPES];
|
||||
}
|
||||
|
||||
/** 解析仓库过滤,只允许出现在仓库清单里的 wmsCoId,防止任意拼参 */
|
||||
function resolveWarehouses(raw) {
|
||||
const known = DAILY_INVENTORY_BASELINE.filter((b) => b.wmsCoId != null);
|
||||
@ -101,11 +88,10 @@ router.get('/', async (req, res) => {
|
||||
try {
|
||||
const force = String(req.query?.refresh || '') === '1';
|
||||
const days = resolveDays(req.query?.days);
|
||||
const binTypes = resolveBinTypes(req.query?.binType);
|
||||
const warehouses = resolveWarehouses(req.query?.wmsCoId);
|
||||
const wmsCoIds = warehouses.map((b) => String(b.wmsCoId));
|
||||
|
||||
const cacheKey = `${days}::${binTypes.join('|')}::${wmsCoIds.join('|')}`;
|
||||
const cacheKey = `${days}::${wmsCoIds.join('|')}`;
|
||||
const latestBizDate = await fetchLatestBizDate().catch(() => null);
|
||||
const cached = cacheByKey.get(cacheKey);
|
||||
if (!force && cached && cached.latestBizDate === latestBizDate
|
||||
@ -123,7 +109,7 @@ router.get('/', async (req, res) => {
|
||||
|
||||
const { shipFrom, shipTo } = buildShipWindow(asOf, days);
|
||||
const [rows, takenAt] = await Promise.all([
|
||||
fetchPickingBinSkuRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days, binTypes }),
|
||||
fetchPickingBinSkuRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days }),
|
||||
fetchBinTakenAt().catch(() => null),
|
||||
]);
|
||||
|
||||
@ -189,8 +175,8 @@ router.get('/', async (req, res) => {
|
||||
const payload = {
|
||||
days,
|
||||
windowOptions: PICKING_BIN_WINDOW_OPTIONS,
|
||||
binTypes,
|
||||
binTypeOptions: BIN_TYPES,
|
||||
binTypes: [...PICKING_BIN_BIN_TYPES],
|
||||
binTypeOptions: [...PICKING_BIN_BIN_TYPES],
|
||||
latestBizDate,
|
||||
asOf,
|
||||
shipWindow: { shipFrom, shipTo, days },
|
||||
@ -217,12 +203,11 @@ router.get('/', async (req, res) => {
|
||||
|
||||
/**
|
||||
* GET /api/picking-bin-sales-analysis/export
|
||||
* 导出库位明细。query: days、binType、wmsCoId(逗号分隔)、grade(逗号分隔)、format(xlsx|csv)
|
||||
* 导出库位明细(只含拣选位)。query: days、wmsCoId(逗号分隔)、grade(逗号分隔)、format(xlsx|csv)
|
||||
*/
|
||||
router.get('/export', async (req, res) => {
|
||||
try {
|
||||
const days = resolveDays(req.query?.days);
|
||||
const binTypes = resolveBinTypes(req.query?.binType);
|
||||
const warehouses = resolveWarehouses(req.query?.wmsCoId);
|
||||
const wmsCoIds = warehouses.map((b) => String(b.wmsCoId));
|
||||
const format = String(req.query?.format || 'xlsx').toLowerCase() === 'csv' ? 'csv' : 'xlsx';
|
||||
@ -243,7 +228,7 @@ router.get('/export', async (req, res) => {
|
||||
|
||||
const { shipFrom, shipTo } = buildShipWindow(asOf, days);
|
||||
// 导出按「仓位 × 商品编码」拆行:一个仓位有 N 个商品就输出 N 行
|
||||
const rows = await fetchPickingBinSkuRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days, binTypes });
|
||||
const rows = await fetchPickingBinSkuRows(cloudDb, wmsCoIds, { shipFrom, shipTo, days });
|
||||
|
||||
const whMap = new Map(warehouses.map((b) => [String(b.wmsCoId), b.warehouse]));
|
||||
const filtered = grades.length > 0
|
||||
@ -253,7 +238,6 @@ router.get('/export', async (req, res) => {
|
||||
const sheetRows = filtered.map((r) => [
|
||||
whMap.get(r.wmsCoId) || r.wmsCoId,
|
||||
r.binName,
|
||||
r.binType,
|
||||
r.skuId,
|
||||
r.stockQty,
|
||||
r.skuCount,
|
||||
@ -272,14 +256,14 @@ router.get('/export', async (req, res) => {
|
||||
sheets: [{
|
||||
name: '库位动销明细',
|
||||
header: [
|
||||
'仓库', '仓位', '类型', '商品编码', '库存数量', '仓位SKU数',
|
||||
'仓库', '仓位', '商品编码', '库存数量', '仓位SKU数',
|
||||
`商品销量(近${days}天)`, `分摊销量(近${days}天)`, '商品日销',
|
||||
'动销等级', '统计天数', '统计基准日',
|
||||
],
|
||||
rows: sheetRows,
|
||||
}],
|
||||
});
|
||||
console.log(`[picking-bin-sales-analysis] 导出: ${totalRows} 行, 仓库 ${wmsCoIds.length} 个, 类型 ${binTypes.join('/')}, 窗口 ${days} 天`);
|
||||
console.log(`[picking-bin-sales-analysis] 导出: ${totalRows} 行, 仓库 ${wmsCoIds.length} 个, 仅拣选位, 窗口 ${days} 天`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[picking-bin-sales-analysis] 导出失败:', error.message);
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
*
|
||||
* 口径:
|
||||
* 库存数量:rpt.jst_packInventoryInfo 按 wms_co_id + 仓位 + sku_id 汇总 qty
|
||||
* 仓位类型:bin_group 为空 → 拣选位;bin_group = '存货' → 存货位
|
||||
* 统计范围:只统计拣选位(bin_group 为空);存货位(bin_group=存货)不纳入
|
||||
* 销量: rpt.jst_Orders × rpt.jst_OrderItem,按 send_date,排除 Delete/Cancelled
|
||||
* 日销: 商品窗口销量 / 窗口天数(整档,不按仓位分摊)
|
||||
* 等级: 严格按商品日销判定,同一商品占多少个仓位都是同一等级
|
||||
@ -35,12 +35,14 @@ const PICKING_BIN_LEVEL2 = '拣选库位动销分析';
|
||||
const PICKING_BIN_WINDOW_OPTIONS = [7, 15, 30, 60];
|
||||
const PICKING_BIN_DEFAULT_DAYS = 30;
|
||||
|
||||
/** 仓位类型:业务库 bin_group 为空视为拣选位 */
|
||||
/**
|
||||
* 仓位类型:本功能只统计拣选位(业务库 bin_group 为空即拣选位)。
|
||||
* 存货位(bin_group=存货)不纳入动销等级统计,取数在仓位库存表上直接过滤,
|
||||
* 页面也不再提供类型切换。
|
||||
*/
|
||||
const BIN_TYPE_PICKING = '拣选';
|
||||
const BIN_TYPE_STORAGE = '存货';
|
||||
const BIN_TYPES = [BIN_TYPE_PICKING, BIN_TYPE_STORAGE];
|
||||
/** 默认只看拣选位(本功能的核心是拣货位动销) */
|
||||
const PICKING_BIN_DEFAULT_TYPES = [BIN_TYPE_PICKING];
|
||||
/** 固定统计范围:仅拣选位(保持数组形式,便于接口字段与后续扩展) */
|
||||
const PICKING_BIN_BIN_TYPES = [BIN_TYPE_PICKING];
|
||||
|
||||
/**
|
||||
* 动销等级(顺序即展示顺序)。判定依据为「商品日销」= 商品窗口整档销量 ÷ 天数,
|
||||
@ -65,9 +67,7 @@ module.exports = {
|
||||
PICKING_BIN_WINDOW_OPTIONS,
|
||||
PICKING_BIN_DEFAULT_DAYS,
|
||||
BIN_TYPE_PICKING,
|
||||
BIN_TYPE_STORAGE,
|
||||
BIN_TYPES,
|
||||
PICKING_BIN_DEFAULT_TYPES,
|
||||
PICKING_BIN_BIN_TYPES,
|
||||
PICKING_BIN_GRADE_RULES,
|
||||
PICKING_BIN_TOTAL_LABEL,
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* 拣选库位动销分析 —— 业务库取数
|
||||
*
|
||||
* 库存:rpt.jst_packInventoryInfo 按 wms_co_id + 仓位 + sku_id 汇总 qty(仓位为空的行不参与)
|
||||
* 类型:bin_group 为空 → 拣选位;否则取 bin_group(业务库目前只有「存货」)
|
||||
* 范围:只统计拣选位(bin_group 为空);存货位(bin_group=存货)不纳入
|
||||
* 销量:rpt.jst_Orders × rpt.jst_OrderItem,按 send_date,排除 Delete/Cancelled
|
||||
* 粒度:仓位 × 商品编码(一个仓位有 N 个商品就输出 N 行),列表与导出共用
|
||||
*
|
||||
@ -19,7 +19,7 @@
|
||||
* 没有仓位记录的 SKU 的销量不会体现在任何库位上。
|
||||
*/
|
||||
|
||||
const { PICKING_BIN_GRADE_RULES, BIN_TYPES } = require('../config/pickingBinSalesAnalysis');
|
||||
const { PICKING_BIN_GRADE_RULES, BIN_TYPE_PICKING } = require('../config/pickingBinSalesAnalysis');
|
||||
|
||||
/** mssql 不支持数组参数,按位展开成 @p0,@p1... */
|
||||
function buildInClause(values, prefix) {
|
||||
@ -68,7 +68,7 @@ function buildGradeCase(salesExpr = 'ISNULL(s.salesQty, 0)', daysParam = '@days'
|
||||
*
|
||||
* @param {object} cloudDb 业务库实例
|
||||
* @param {string[]} wmsCoIds 仓库编码
|
||||
* @param {{shipFrom:string, shipTo:string, days:number, binTypes?:string[]}} options
|
||||
* @param {{shipFrom:string, shipTo:string, days:number}} options
|
||||
* @returns {Promise<Array<{wmsCoId:string, binName:string, binType:string, skuId:string,
|
||||
* stockQty:number, skuCount:number, salesQty:number, allocatedSalesQty:number,
|
||||
* dailySales:number, gradeKey:string}>>}
|
||||
@ -77,39 +77,29 @@ async function fetchPickingBinSkuRows(cloudDb, wmsCoIds, options = {}) {
|
||||
if (!cloudDb || typeof cloudDb.query !== 'function') {
|
||||
throw new TypeError('fetchPickingBinSkuRows 的第一个参数必须是业务库实例');
|
||||
}
|
||||
const { shipFrom, shipTo, days, binTypes } = options;
|
||||
const { shipFrom, shipTo, days } = options;
|
||||
if (!Array.isArray(wmsCoIds) || wmsCoIds.length === 0) return [];
|
||||
if (!shipFrom || !shipTo || !days) return [];
|
||||
|
||||
const binIn = buildInClause(wmsCoIds, 'w');
|
||||
const shipIn = buildInClause(wmsCoIds, 'v');
|
||||
|
||||
// 仓位类型过滤(不传 = 全部类型;选满全部类型时也不过滤,避免参数嗅探把查询计划带偏)
|
||||
let typeFilter = '';
|
||||
const typeParams = {};
|
||||
const wantAllTypes = BIN_TYPES.every((t) => (binTypes || []).includes(t));
|
||||
if (Array.isArray(binTypes) && binTypes.length > 0 && !wantAllTypes) {
|
||||
const t = buildInClause(binTypes, 't');
|
||||
Object.assign(typeParams, t.params);
|
||||
typeFilter = `AND b.binType IN (${t.clause})`;
|
||||
}
|
||||
|
||||
// 统计范围固定为拣选位:直接在仓位库存表上过滤 bin_group 为空,
|
||||
// 存货位(bin_group=存货)不参与动销等级统计。
|
||||
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,
|
||||
N'${BIN_TYPE_PICKING}' 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
|
||||
AND ISNULL(p.bin_group, '') = ''
|
||||
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 (
|
||||
@ -154,8 +144,6 @@ async function fetchPickingBinSkuRows(cloudDb, wmsCoIds, options = {}) {
|
||||
ON a.wmsCoId = b.wmsCoId AND a.binName = b.binName AND a.binType = b.binType
|
||||
LEFT JOIN ship s
|
||||
ON s.wmsCoId = b.wmsCoId AND s.skuId = b.skuId
|
||||
WHERE 1 = 1
|
||||
${typeFilter}
|
||||
GROUP BY b.wmsCoId, b.binName, b.binType, b.skuId, a.skuCount, s.salesQty, sp.binCount
|
||||
HAVING ABS(SUM(CAST(b.binSkuQty AS DECIMAL(18, 2)))) > 0.0001
|
||||
ORDER BY b.wmsCoId, b.binType, b.binName, b.skuId`;
|
||||
@ -165,7 +153,6 @@ async function fetchPickingBinSkuRows(cloudDb, wmsCoIds, options = {}) {
|
||||
{
|
||||
...binIn.params,
|
||||
...shipIn.params,
|
||||
...typeParams,
|
||||
shipFrom,
|
||||
shipTo,
|
||||
days,
|
||||
|
||||
@ -12,8 +12,7 @@ const {
|
||||
PICKING_BIN_GRADE_RULES,
|
||||
PICKING_BIN_DEFAULT_DAYS,
|
||||
PICKING_BIN_WINDOW_OPTIONS,
|
||||
PICKING_BIN_DEFAULT_TYPES,
|
||||
BIN_TYPES,
|
||||
PICKING_BIN_BIN_TYPES,
|
||||
} = require('../src/config/pickingBinSalesAnalysis');
|
||||
|
||||
test('buildInClause 按位展开参数', () => {
|
||||
@ -37,11 +36,10 @@ test('等级按商品销量判定(默认表达式取 s.salesQty,不含仓位
|
||||
assert.ok(!expr.includes('x.salesQty'));
|
||||
});
|
||||
|
||||
test('默认窗口为 30 天,默认只看拣选位', () => {
|
||||
test('默认窗口为 30 天,统计范围固定为拣选位', () => {
|
||||
assert.equal(PICKING_BIN_DEFAULT_DAYS, 30);
|
||||
assert.ok(PICKING_BIN_WINDOW_OPTIONS.includes(30));
|
||||
assert.deepEqual(PICKING_BIN_DEFAULT_TYPES, ['拣选']);
|
||||
assert.deepEqual(BIN_TYPES, ['拣选', '存货']);
|
||||
assert.deepEqual(PICKING_BIN_BIN_TYPES, ['拣选']);
|
||||
});
|
||||
|
||||
test('fetchPickingBinSkuRows 首参不是数据库实例时抛错', async () => {
|
||||
@ -84,7 +82,6 @@ test('fetchPickingBinSkuRows 按仓位×商品输出,等级与日销来自商
|
||||
shipFrom: '2026-09-08 01:00:00',
|
||||
shipTo: '2026-10-08 01:00:00',
|
||||
days: 30,
|
||||
binTypes: ['拣选'],
|
||||
});
|
||||
|
||||
assert.equal(rows.length, 2);
|
||||
@ -108,34 +105,34 @@ test('fetchPickingBinSkuRows 按仓位×商品输出,等级与日销来自商
|
||||
gradeKey: 'hot',
|
||||
});
|
||||
|
||||
// 参数必须带上两个仓库、发货窗口与天数,以及仓位类型过滤
|
||||
// 参数必须带上两个仓库、发货窗口与天数
|
||||
assert.equal(captured.params.w0, 101);
|
||||
assert.equal(captured.params.w1, 102);
|
||||
assert.equal(captured.params.v0, 101);
|
||||
assert.equal(captured.params.shipFrom, '2026-09-08 01:00:00');
|
||||
assert.equal(captured.params.shipTo, '2026-10-08 01:00:00');
|
||||
assert.equal(captured.params.days, 30);
|
||||
assert.equal(captured.params.t0, '拣选');
|
||||
|
||||
// 商品销量按占用仓位数分摊(供求和),但等级依据的是未分摊的商品销量
|
||||
assert.ok(captured.sqlText.includes('/ NULLIF(sp.binCount, 0)'));
|
||||
assert.ok(captured.sqlText.includes('ISNULL(s.salesQty, 0) / @days AS DECIMAL(18, 4)'));
|
||||
// 仓位类型:bin_group 为空视为拣选位;只统计有库存的行
|
||||
assert.ok(captured.sqlText.includes("THEN N'拣选'"));
|
||||
// 统计范围固定为拣选位:bin_group 为空;只统计有库存的行
|
||||
assert.ok(captured.sqlText.includes("ISNULL(p.bin_group, '') = ''"));
|
||||
assert.ok(captured.sqlText.includes('ISNULL(p.qty, 0) <> 0'));
|
||||
assert.ok(captured.sqlText.includes('HAVING ABS(SUM(CAST(b.binSkuQty AS DECIMAL(18, 2)))) > 0.0001'));
|
||||
});
|
||||
|
||||
test('fetchPickingBinSkuRows 不传类型时不过滤 binType', async () => {
|
||||
test('fetchPickingBinSkuRows 只统计拣选位,不带类型参数', async () => {
|
||||
let captured = null;
|
||||
const db = {
|
||||
query: async (sqlText, params) => { captured = { sqlText, params }; return []; },
|
||||
};
|
||||
await fetchPickingBinSkuRows(db, [101], {
|
||||
shipFrom: 'a', shipTo: 'b', days: 15, binTypes: [],
|
||||
});
|
||||
await fetchPickingBinSkuRows(db, [101], { shipFrom: 'a', shipTo: 'b', days: 15 });
|
||||
// 存货位(bin_group=存货)被排除,且不再有 binType IN (...) 的可变过滤
|
||||
assert.ok(captured.sqlText.includes("ISNULL(p.bin_group, '') = ''"));
|
||||
assert.ok(!captured.sqlText.includes('b.binType IN'));
|
||||
assert.equal(captured.params.t0, undefined);
|
||||
assert.equal(captured.params.binTypes, undefined);
|
||||
});
|
||||
|
||||
test('gradeKey 缺失时兜底为 zero', async () => {
|
||||
|
||||
@ -11,11 +11,6 @@ export const PICKING_BIN_COLUMNS = [
|
||||
label: '仓位',
|
||||
tip: '系统仓位编码(jst_packInventoryInfo.bin);同一仓位有多个商品时会出现多行',
|
||||
},
|
||||
{
|
||||
key: 'binType',
|
||||
label: '类型',
|
||||
tip: '拣选 = 拣货位(bin_group 为空);存货 = 存货位(bin_group=存货)',
|
||||
},
|
||||
{
|
||||
key: 'skuId',
|
||||
label: '商品编码',
|
||||
@ -52,13 +47,6 @@ export const PICKING_BIN_WINDOW_OPTIONS = [
|
||||
|
||||
export const PICKING_BIN_DEFAULT_DAYS = 30;
|
||||
|
||||
export const PICKING_BIN_TYPE_OPTIONS = [
|
||||
{ label: '拣选位', value: '拣选' },
|
||||
{ label: '存货位', value: '存货' },
|
||||
];
|
||||
|
||||
export const PICKING_BIN_DEFAULT_TYPES = ['拣选'];
|
||||
|
||||
/** 动销等级展示色(与后端 key 对应) */
|
||||
export const PICKING_BIN_GRADE_COLORS = {
|
||||
hot: 'red',
|
||||
@ -70,6 +58,8 @@ export const PICKING_BIN_GRADE_COLORS = {
|
||||
};
|
||||
|
||||
export const PICKING_BIN_DESCRIPTION = [
|
||||
'【统计范围】只统计拣选位(bin_group 为空的仓位);存货位不纳入动销分析。',
|
||||
'',
|
||||
'【粒度】一行 = 一个「仓位 × 商品编码」组合。一个仓位放了 N 个商品就显示 N 行,列表与导出一致。',
|
||||
' 库存数量 = 该商品在这个仓位上的实际库存(同一仓位多行相加 = 仓位总库存)。',
|
||||
'',
|
||||
@ -91,5 +81,5 @@ export const PICKING_BIN_DESCRIPTION = [
|
||||
'',
|
||||
'数据来源:库存取 rpt.jst_packInventoryInfo(按仓库 + 仓位 + 商品汇总);',
|
||||
'销量取 rpt.jst_Orders × rpt.jst_OrderItem(按 send_date,排除 Delete/Cancelled),与「每日进销存」发货口径一致。',
|
||||
'仓位类型:bin_group 为空视为拣选位,bin_group=存货 为存货位。',
|
||||
'统计范围:只取 bin_group 为空的拣选位,存货位(bin_group=存货)不参与统计。',
|
||||
];
|
||||
|
||||
@ -13,8 +13,6 @@ import {
|
||||
PICKING_BIN_COLUMNS,
|
||||
PICKING_BIN_WINDOW_OPTIONS,
|
||||
PICKING_BIN_DEFAULT_DAYS,
|
||||
PICKING_BIN_TYPE_OPTIONS,
|
||||
PICKING_BIN_DEFAULT_TYPES,
|
||||
PICKING_BIN_GRADE_COLORS,
|
||||
PICKING_BIN_DESCRIPTION,
|
||||
} from '../../constants/pickingBinSalesAnalysis';
|
||||
@ -47,7 +45,6 @@ const PickingBinSalesAnalysis = () => {
|
||||
const [data, setData] = useState(null);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [days, setDays] = useState(PICKING_BIN_DEFAULT_DAYS);
|
||||
const [binTypes, setBinTypes] = useState(PICKING_BIN_DEFAULT_TYPES);
|
||||
const [warehouse, setWarehouse] = useState('');
|
||||
const [grades, setGrades] = useState([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
@ -55,9 +52,8 @@ const PickingBinSalesAnalysis = () => {
|
||||
|
||||
const params = useMemo(() => ({
|
||||
days,
|
||||
binType: binTypes.join(','),
|
||||
...(warehouse ? { wmsCoId: warehouse } : {}),
|
||||
}), [days, binTypes, warehouse]);
|
||||
}), [days, warehouse]);
|
||||
|
||||
const load = useCallback(async (options = {}) => {
|
||||
setLoading(true);
|
||||
@ -96,7 +92,7 @@ const PickingBinSalesAnalysis = () => {
|
||||
title: <TipTitle tip={col.tip}>{col.label}</TipTitle>,
|
||||
key: col.key,
|
||||
dataIndex: col.key,
|
||||
align: ['warehouse', 'binName', 'binType', 'skuId', 'gradeKey'].includes(col.key) ? 'left' : 'right',
|
||||
align: ['warehouse', 'binName', 'skuId', 'gradeKey'].includes(col.key) ? 'left' : 'right',
|
||||
width: col.key === 'warehouse' ? 240
|
||||
: (col.key === 'gradeKey' ? 170 : (col.key === 'skuId' ? 150 : 110)),
|
||||
sorter: ['stockQty', 'salesQty', 'dailySales'].includes(col.key)
|
||||
@ -107,7 +103,7 @@ const PickingBinSalesAnalysis = () => {
|
||||
const g = gradeMap.get(value);
|
||||
return <Tag color={PICKING_BIN_GRADE_COLORS[value] || 'default'}>{g?.label || value}</Tag>;
|
||||
}
|
||||
if (col.key === 'binType' || col.key === 'skuId') return <Text>{value || '—'}</Text>;
|
||||
if (col.key === 'skuId') return <Text>{value || '—'}</Text>;
|
||||
if (col.key === 'dailySales') return num(value, 2);
|
||||
if (col.key === 'salesQty') return num(value, 2);
|
||||
if (col.key === 'stockQty') return int(value);
|
||||
@ -174,16 +170,6 @@ const PickingBinSalesAnalysis = () => {
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
</Space>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
value={binTypes}
|
||||
options={PICKING_BIN_TYPE_OPTIONS}
|
||||
onChange={(v) => setBinTypes(v && v.length > 0 ? v : PICKING_BIN_DEFAULT_TYPES)}
|
||||
style={{ minWidth: 170 }}
|
||||
placeholder="仓位类型"
|
||||
maxTagCount={2}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
value={warehouse || undefined}
|
||||
@ -205,7 +191,7 @@ const PickingBinSalesAnalysis = () => {
|
||||
<Tooltip title="跳过 5 分钟缓存,重新扫描订单与仓位明细重算(约 5~20 秒)">
|
||||
<Button onClick={() => load({ force: true })} loading={loading}>强制重算</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="导出当前筛选下的明细(仓库 / 仓位 / 类型 / 商品编码 / 库存数量 / 仓位SKU数 / 商品销量 / 分摊销量 / 商品日销 / 动销等级)。按「仓位 × 商品编码」拆行;商品销量为整档销量(同一商品跨仓位重复,不可直接求和),需要求和请用「分摊销量」列;动销等级按商品日销判定">
|
||||
<Tooltip title="导出当前筛选下的明细(仓库 / 仓位 / 商品编码 / 库存数量 / 仓位SKU数 / 商品销量 / 分摊销量 / 商品日销 / 动销等级),仅含拣选位。按「仓位 × 商品编码」拆行;商品销量为整档销量(同一商品跨仓位重复,不可直接求和),需要求和请用「分摊销量」列;动销等级按商品日销判定">
|
||||
<Button icon={<DownloadOutlined />} onClick={handleExport} loading={exporting}>导出明细</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
@ -263,7 +249,7 @@ const PickingBinSalesAnalysis = () => {
|
||||
className="picking-bin-table"
|
||||
columns={columns}
|
||||
dataSource={filteredRows}
|
||||
rowKey={(r) => `${r.wmsCoId}::${r.binType}::${r.binName}::${r.skuId}`}
|
||||
rowKey={(r) => `${r.wmsCoId}::${r.binName}::${r.skuId}`}
|
||||
size="small"
|
||||
bordered
|
||||
pagination={{
|
||||
@ -279,7 +265,7 @@ const PickingBinSalesAnalysis = () => {
|
||||
<Text type="secondary" className="picking-bin-footer">
|
||||
当前筛选出 {filteredRows.length} 条明细
|
||||
{items.length !== filteredRows.length ? `(共 ${items.length} 条)` : ''};
|
||||
去重商品 {int(data?.total?.skuCount ?? 0)} 个;已选类型:{(data?.binTypes ?? []).join('、') || '—'}。
|
||||
去重商品 {int(data?.total?.skuCount ?? 0)} 个;统计范围:仅拣选位。
|
||||
</Text>
|
||||
|
||||
<Modal
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user