feat(配件库存监控/MKH-BOM): 新增「采购在途」数量列与在途明细弹窗,补货预警改按结余库存判定
配件库存监控: - 列表「实时库存」后新增「采购在途数量」列,口径 SUM(MAX(0, jst_purchaseItem.qty - inQty)) - 顶部「实时库存合计」后新增可点击「采购在途」卡片,弹窗展示在途明细 (商品编码/商品名称/采购数量/已入库/在途数量/计划到货日期/采购单号),支持关键词、排序、分页 - 新增后端接口 GET /api/modular-stock-monitor/purchase-in-transit - 筛选器品类改直查 jst_sku 主表,避免新增 OUTER APPLY 拖慢筛选 - 采购在途列仅取数展示,不参与 ORDER BY / 聚合(规避 MSSQL 对 OUTER APPLY 列的灾难性执行计划) MKH-BOM 补货预警: - 日销驱动与已排产分支改用「结余库存 = 现有库存 - 待发货件数」判定可用天数与预警等级 - 建议补货量按 ceil(3 天日销 + 待发货件数 - 现有库存) 计算 - 新增 scope=work-order 范围筛选;列表新增「结余库存」列 - 打印模板剥离公司名前缀,打印字号调大 测试:mkhBomService 单测新增结余库存缓冲/紧张用例与回归用例
This commit is contained in:
parent
be52bdd0b5
commit
b7aca2343b
@ -4,6 +4,7 @@ const express = require('express');
|
||||
const {
|
||||
getModularStockList,
|
||||
getModularStockFilters,
|
||||
getPurchaseInTransitList,
|
||||
syncModularStock,
|
||||
} = require('../services/modularStockMonitorService');
|
||||
|
||||
@ -37,6 +38,15 @@ router.get('/filters', async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 采购在途明细列表:目标仓全部「采购数量 > 已入库数量」的采购单明细
|
||||
router.get('/purchase-in-transit', async (req, res) => {
|
||||
try {
|
||||
res.json({ success: true, data: await getPurchaseInTransitList(req.query || {}) });
|
||||
} catch (error) {
|
||||
sendError(res, error, '加载采购在途明细失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 手动同步:重新取最新监控日,前端「同步」按钮调用后再拉一次列表即拿到最新快照
|
||||
router.post('/sync', async (_req, res) => {
|
||||
try {
|
||||
|
||||
@ -70,8 +70,11 @@
|
||||
// · 现有库存 rpt.jst_inventoryInfo(wms_co_id = 15870495 品牌仓)实时库存;
|
||||
// 该仓对 MKH 成品只覆盖约 256 / 4408 条(2026-09-16 快照;成品总数口径见 ①),
|
||||
// 缺失时回退监控快照 jsStock
|
||||
// · 可用天数 = 现有库存 / 近15天日销
|
||||
// · 建议补货量 = ceil(3 × 近15天日销 − 现有库存),不足 0 取 0
|
||||
// · 结余库存 = max(0, 现有库存 − 待发货订单)
|
||||
// ⚠️ 2026-09-18 起:已下单未发货的部分已被占用,**所有口径一律先扣待发订单**
|
||||
// (见 ⑫)。待发货订单 = 0 时结余库存 ≡ 现有库存,与旧口径完全等价。
|
||||
// · 可用天数 = 结余库存 / 近15天日销
|
||||
// · 建议补货量 = ceil(3 × 近15天日销 + 待发货订单 − 现有库存),不足 0 取 0
|
||||
// ⚠️ 刻意**不用**源系统的 jsUseDays 列:实测它与 jsStock / fifteenAvgSale 不同源
|
||||
// (同一 SKU 可差 20 天以上,如 MKHDNZ0409260700488 库存 44 / 日销 0.67,
|
||||
// 除法 65.7 天而 jsUseDays 为 44),本页口径以业务给定的「近15天日销」为准。
|
||||
@ -190,6 +193,27 @@
|
||||
// ⚠️ 配件库存查询失败时**不做任何判定**(宁可少标也不误标),
|
||||
// 接口透出 componentStockDegraded=true。
|
||||
//
|
||||
// ── ⑫ 结余库存(2026-09-18 业务口径修正)──────────────────────────
|
||||
// 背景:`MKHDNZ0409260601376` 现有库存 3、待发货订单 3,业务认为结余库存应为 0、
|
||||
// 可用天数 0、应判缺货;但旧代码里「待发货订单」**只在日销 = 0 时参与判定**
|
||||
// (`orderDriven = dailySale <= 0 && pendingOrderQty > 0`),该品日销 0.27 > 0,
|
||||
// 于是走日销分支 3 ≥ 0.81(3 天日销)→ 判「正常」,3 件待发订单被完全忽略。
|
||||
// 新口径:**待发货订单对所有商品都参与计算,不再区分日销是否为 0**
|
||||
// · 结余库存 availableStock = max(0, 现有库存 − 待发货订单)
|
||||
// · 可用天数 = 结余库存 ÷ 近15天日销(日销 > 0 时)
|
||||
// · 缺货:结余库存 ≤ 0(订单都发不出去);预警:0 < 可用天数 < 3;否则正常
|
||||
// · 建议补货量 = ceil(3 × 近15天日销 + 待发货订单 − 现有库存)
|
||||
// (先补到能把订单发掉,再补 3 天缓冲)
|
||||
// ⚠️ ⑩ 的订单驱动分支(日销 = 0)**保持不变**:那里本来就是以代发订单件数为分母,
|
||||
// 语义是「库存能覆盖几倍订单」,与日销无关,改动会误伤已有的订单驱动预警。
|
||||
// 实测(2026-09-18,4,223 个成品):既有日销又有待发订单的商品 38 个,
|
||||
// 其中 **4 个等级变化、全部升级为缺货**:
|
||||
// MKHDNZ0409260700503(日销1.67 库存1 待发2) 预警0.60天 → 缺货0天
|
||||
// MKHDNZ0409260601547(日销0.42 库存1 待发1) 预警2.38天 → 缺货0天
|
||||
// MKHDNZ0409260601527(日销0.13 库存1 待发1) 正常7.69天 → 缺货0天
|
||||
// MKHDNZ0409260601376(日销0.27 库存3 待发3) 正常11.11天 → 缺货0天
|
||||
// 全量分布:缺货 10→14、预警 6→4、正常 165→163(已排产 / 无销量不变)。
|
||||
//
|
||||
const cloudDatabase = require('../config/cloudDatabase');
|
||||
const productDb = require('../config/productInformationDatabase');
|
||||
|
||||
@ -333,18 +357,23 @@ const TRUTHY_FLAGS = new Set(['1', 'true', 'yes', 'on']);
|
||||
const parseFlag = (value) => TRUTHY_FLAGS.has(String(value ?? '').trim().toLowerCase());
|
||||
|
||||
// 列表展示范围(内存过滤)
|
||||
const SCOPES = Object.freeze({ WARNING: 'warning', SOLD: 'sold', ALL: 'all' });
|
||||
const SCOPES = Object.freeze({
|
||||
WARNING: 'warning', SOLD: 'sold', WORK_ORDER: 'work-order', ALL: 'all',
|
||||
});
|
||||
|
||||
/**
|
||||
* 解析展示范围:
|
||||
* warning —— 仅需补货(近15天日销 > 0 且 可用天数 < 3 天)
|
||||
* sold —— 有近15天日销的商品
|
||||
* all —— 全部 MKH 成品
|
||||
* warning —— 仅需补货(缺货 + 3 天内预警)
|
||||
* sold —— 有近15天日销的商品
|
||||
* work-order —— 有未完成加工单(在途加工量 > 0)的商品
|
||||
* all —— 全部 MKH 成品
|
||||
* 兼容旧参数 warningOnly=1(等价 scope=warning)。
|
||||
*/
|
||||
function resolveScope(query = {}) {
|
||||
const raw = String(query.scope ?? '').trim().toLowerCase();
|
||||
if (raw === SCOPES.WARNING || raw === SCOPES.SOLD || raw === SCOPES.ALL) return raw;
|
||||
if (raw === SCOPES.WARNING || raw === SCOPES.SOLD || raw === SCOPES.WORK_ORDER || raw === SCOPES.ALL) {
|
||||
return raw;
|
||||
}
|
||||
if (parseFlag(query.warningOnly)) return SCOPES.WARNING;
|
||||
return SCOPES.ALL;
|
||||
}
|
||||
@ -360,6 +389,7 @@ function filterRows(rows, { keyword, scope }) {
|
||||
return rows.filter((row) => {
|
||||
if (scope === SCOPES.WARNING && !row.hasWarning) return false;
|
||||
if (scope === SCOPES.SOLD && !(toNumber(row.dailySale) > 0)) return false;
|
||||
if (scope === SCOPES.WORK_ORDER && !row.hasPendingWorkOrder) return false;
|
||||
return matchesKeyword(row, keyword);
|
||||
});
|
||||
}
|
||||
@ -813,8 +843,10 @@ const sumQty = (components) => components.reduce((sum, item) => sum + (toNumber(
|
||||
/**
|
||||
* 计算单个 MKH 成品的补货预警指标(纯函数,便于回归测试)。
|
||||
*
|
||||
* 口径(见文件头 ⑦ + ⑩):
|
||||
* ① 近15天日销 > 0 且 现有库存 < 3 天日销 → 预警
|
||||
* 口径(见文件头 ⑦ + ⑩ + ⑫):
|
||||
* ① 近15天日销 > 0 且 结余库存 < 3 天日销 → 预警
|
||||
* 结余库存 = max(0, 现有库存 − 待发货订单),待发订单为 0 时即现有库存(见 ⑫)
|
||||
* 结余库存 = 0 → 缺货(订单都发不出去)
|
||||
* ② 近15天日销 = 0 且有代发(待发货)订单 → 订单驱动预警(见文件头 ⑩):
|
||||
* 可用天数 = 现有库存 ÷ 代发订单件数,< 3 天预警、= 0(无库存)缺货、≥ 3 天不预警
|
||||
* 两者都不满足 → 无销量,不参与预警。
|
||||
@ -845,12 +877,16 @@ function buildReplenishMetrics(row = {}) {
|
||||
// 待发货订单(见文件头 ⑩):当前未发货订单里的件数
|
||||
const pendingOrderQty = Math.max(0, toNumber(row.pendingOrderQty) ?? 0);
|
||||
const pendingOrderCount = Math.max(0, toNumber(row.pendingOrderCount) ?? 0);
|
||||
// 快照日销还是 0、但已经有订单 → 由订单驱动预警
|
||||
// 快照日销还是 0、但已经有订单 → 由订单驱动预警(⑩ 专属分支,分母用代发订单件数)
|
||||
const orderDriven = dailySale <= 0 && pendingOrderQty > 0;
|
||||
// 结余库存(见文件头 ⑫):已下单未发货的部分已被占用,先扣掉才是可自由支配的库存。
|
||||
// ⚠️ 不分日销是否为 0,所有口径统一按结余库存计算(2026-09-18 口径修正)。
|
||||
const availableStock = Math.max(0, stock - pendingOrderQty);
|
||||
|
||||
const base = {
|
||||
dailySale,
|
||||
stock,
|
||||
availableStock,
|
||||
pendingWorkOrderQty,
|
||||
pendingWorkOrderCount,
|
||||
hasPendingWorkOrder,
|
||||
@ -884,7 +920,7 @@ function buildReplenishMetrics(row = {}) {
|
||||
if (hasPendingWorkOrder) {
|
||||
return {
|
||||
...base,
|
||||
stockDays: dailySale > 0 ? Number((stock / dailySale).toFixed(2)) : null,
|
||||
stockDays: dailySale > 0 ? Number((availableStock / dailySale).toFixed(2)) : null,
|
||||
replenishQty: 0,
|
||||
warningLevel: WARNING_LEVEL.SCHEDULED,
|
||||
warningOrder: 4,
|
||||
@ -926,9 +962,11 @@ function buildReplenishMetrics(row = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const stockDays = stock / dailySale;
|
||||
const replenishQty = Math.max(0, Math.ceil(base.thresholdQty - stock));
|
||||
const level = stockDays <= 0
|
||||
// 日销驱动(见文件头 ⑦ + ⑫):一律用**结余库存**衡量,待发货订单先从库存里扣掉
|
||||
const stockDays = availableStock / dailySale;
|
||||
// 建议补货量 = 3 天日销缓冲 + 待发货订单缺口(先补到能把订单发掉,再补 3 天缓冲)
|
||||
const replenishQty = Math.max(0, Math.ceil(base.thresholdQty + pendingOrderQty - stock));
|
||||
const level = availableStock <= 0
|
||||
? WARNING_LEVEL.OUT_OF_STOCK
|
||||
: (stockDays < REPLENISH_DAYS_THRESHOLD ? WARNING_LEVEL.WARNING : WARNING_LEVEL.NORMAL);
|
||||
|
||||
|
||||
@ -28,8 +28,11 @@
|
||||
// 当日入库:业务库无出入库流水表,用采购到货 rpt.jst_purchaseItem 按「监控日」汇总 inQty
|
||||
// (全库 207 张视图均无库存流水表;dws_sn_sales_full_info_di 是成品销售明细,只命中 97 / 794)
|
||||
//
|
||||
// ⚠️ 业务库确实无数据源的字段:
|
||||
// - 仓位 location:rpt.jst_sku.bin / other_bin 全库 557,475 行填充数为 0
|
||||
// ✅ 仓位 location(2026-09-18 改源,旧的「无数据源」结论作废):
|
||||
// - 旧源 rpt.jst_sku.bin / other_bin 在目标仓 794 行**恒为空**,所以页面长期显示「未维护」
|
||||
// - 新源 rpt.jst_packInventoryInfo.bin(真货位):该表 2026-09-18 10:48 被上游刷新后
|
||||
// 目标仓 794 个 SKU **命中 727 个**(如 ZT-8 / BCP01-01-1)
|
||||
// - 详见 buildLocationSql() 注释
|
||||
|
||||
const cloudDatabase = require('../config/cloudDatabase');
|
||||
const productDb = require('../config/productInformationDatabase');
|
||||
@ -110,6 +113,24 @@ const buildWarningOrderSql = () => (
|
||||
END`
|
||||
);
|
||||
|
||||
/**
|
||||
* 统一的「仓位」表达式(2026-09-18 改源)。
|
||||
* 旧源 `jst_sku.bin / other_bin` 在目标仓 794 行**恒为空**(页面因此长期显示「未维护」);
|
||||
* 真货位在 `rpt.jst_packInventoryInfo.bin` —— 该表 2026-09-18 10:48 被上游刷新后,
|
||||
* 目标仓 794 个 SKU **命中 727 个**(实测),如 ZT-8 / BCP01-01-1。
|
||||
* · 只取 `qty > 0` 的箱,同 SKU 多箱时取库存最大的那个(列表页只展示主货位)
|
||||
* · 取不到才回退商品档案的 bin / other_bin(目前仍全空,保留兜底不影响结果)
|
||||
* 与 MKH-BOM 页 `mkhBomService.loadComponentLocations()` 同源,区别只在本页取单个主货位。
|
||||
* ⚠️ 列表映射、仓位筛选两处共用此表达式,改口径时只改这里。
|
||||
*/
|
||||
const buildLocationSql = () => (
|
||||
`COALESCE(
|
||||
ploc.bin,
|
||||
NULLIF(LTRIM(RTRIM(s.bin)), N''),
|
||||
NULLIF(LTRIM(RTRIM(s.other_bin)), N'')
|
||||
)`
|
||||
);
|
||||
|
||||
const buildBaseSql = () => `
|
||||
FROM dbo.jst_sku s
|
||||
OUTER APPLY (
|
||||
@ -132,12 +153,31 @@ OUTER APPLY (
|
||||
AND p.delivery_date >= @monitorDate
|
||||
AND p.delivery_date < DATEADD(DAY, 1, @monitorDate)
|
||||
) pin
|
||||
-- 采购在途:该 SKU 全部采购单中「已下单 − 已入库」的未到货部分合计
|
||||
-- jst_purchaseItem.qty = 采购数量,inQty = 已入库数量,plan_arrive_qty 全库恒为 0(不可用)、
|
||||
-- is_delivery 全库恒为 0(不可信),故在途口径 = SUM(MAX(0, qty − inQty))。
|
||||
-- ⚠️ 此列只参与「取数/展示」,绝不参与 ORDER BY / 聚合(参考本文件头 MSSQL 坑说明,
|
||||
-- 对 OUTER APPLY 列做排序/聚合会触发灾难性执行计划)。
|
||||
OUTER APPLY (
|
||||
SELECT CAST(SUM(CASE WHEN p.qty > ISNULL(p.inQty, 0) THEN p.qty - ISNULL(p.inQty, 0) ELSE 0 END) AS DECIMAL(18, 2)) AS transit
|
||||
FROM dbo.jst_purchaseItem p
|
||||
WHERE p.sku_id = s.sku_id
|
||||
) ptr
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 NULLIF(LTRIM(RTRIM(c.pictureSkucode)), N'') AS pictureSkucode
|
||||
FROM dbo.fys_skuCostPriceUpdate c
|
||||
WHERE UPPER(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), c.sku)))) = UPPER(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), s.sku_id))))
|
||||
ORDER BY c.createTime DESC
|
||||
) cost
|
||||
-- 仓位:主货位(真货位),见 buildLocationSql() 说明
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 NULLIF(LTRIM(RTRIM(pk.bin)), N'') AS bin
|
||||
FROM dbo.jst_packInventoryInfo pk WITH (NOLOCK)
|
||||
WHERE pk.sku_id = s.sku_id
|
||||
AND NULLIF(LTRIM(RTRIM(pk.bin)), N'') IS NOT NULL
|
||||
AND ISNULL(pk.qty, 0) > 0
|
||||
ORDER BY pk.qty DESC
|
||||
) ploc
|
||||
-- 模块化配置状态:见文件头部「已配置MKH」口径说明
|
||||
OUTER APPLY (
|
||||
SELECT CAST(CASE WHEN ${buildConfiguredExistsSql()} THEN 1 ELSE 0 END AS BIT) AS configured
|
||||
@ -204,7 +244,7 @@ const buildFilters = (query = {}, monitorDate = null) => {
|
||||
const location = toText(query.location);
|
||||
if (location) {
|
||||
params.location = location;
|
||||
conditions.push(`COALESCE(NULLIF(LTRIM(RTRIM(s.bin)), N''), NULLIF(LTRIM(RTRIM(s.other_bin)), N'')) = @location`);
|
||||
conditions.push(`${buildLocationSql()} = @location`);
|
||||
}
|
||||
|
||||
const mkhStatus = toText(query.mkhStatus);
|
||||
@ -306,6 +346,7 @@ const mapRow = (row = {}) => {
|
||||
inQtyToday: toNumber(row.inQtyToday) ?? 0,
|
||||
outQtyToday: toNumber(row.outQtyToday) ?? 0,
|
||||
realtimeStock,
|
||||
purchaseTransit: toNumber(row.purchaseTransit) ?? 0,
|
||||
location: toText(row.location),
|
||||
avgShip15d: toNumber(row.avgShip15d),
|
||||
stockDays,
|
||||
@ -320,9 +361,10 @@ const buildProjection = () => `
|
||||
s.name AS productName,
|
||||
s.category AS category,
|
||||
COALESCE(inv.qty, m.jsStock, 0) AS realtimeStock,
|
||||
ISNULL(ptr.transit, 0) AS purchaseTransit,
|
||||
ISNULL(pin.inQty, 0) AS inQtyToday,
|
||||
ISNULL(m.nearSecondSale, 0) AS outQtyToday,
|
||||
COALESCE(NULLIF(LTRIM(RTRIM(s.bin)), N''), NULLIF(LTRIM(RTRIM(s.other_bin)), N'')) AS location,
|
||||
${buildLocationSql()} AS location,
|
||||
m.fifteenAvgSale AS avgShip15d,
|
||||
m.jsUseDays AS stockDays,
|
||||
m.safetyDays AS safetyDays,
|
||||
@ -391,6 +433,7 @@ async function getModularStockList(query = {}) {
|
||||
configuredCount: summaryRow?.configuredCount || 0,
|
||||
unconfiguredCount: (summaryRow?.skuCount || 0) - (summaryRow?.configuredCount || 0),
|
||||
totalStock: summaryRow?.totalStock || 0,
|
||||
totalPurchaseTransit: summaryRow?.totalPurchaseTransit || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -401,6 +444,7 @@ async function buildSummary(whereSql, params) {
|
||||
COUNT_BIG(*) AS skuCount,
|
||||
SUM(CASE WHEN ${buildWarningOrderSql()} = 0 THEN 1 ELSE 0 END) AS outOfStockCount,
|
||||
CAST(ISNULL(SUM(COALESCE(inv.qty, m.jsStock, 0)), 0) AS BIGINT) AS totalStock,
|
||||
CAST(ISNULL(SUM(ISNULL(ptr.transit, 0)), 0) AS BIGINT) AS totalPurchaseTransit,
|
||||
SUM(CASE WHEN ${buildWarningOrderSql()} = 1 THEN 1 ELSE 0 END) AS lowStockDaysCount,
|
||||
SUM(CASE WHEN cfg.configured = 1 THEN 1 ELSE 0 END) AS configuredCount,
|
||||
MAX(m.date) AS monitorDate
|
||||
@ -408,13 +452,13 @@ async function buildSummary(whereSql, params) {
|
||||
params,
|
||||
{ requestTimeout: 180000 }
|
||||
);
|
||||
|
||||
const row = rows[0] || {};
|
||||
|
||||
return [{
|
||||
skuCount: Number(row.skuCount || 0),
|
||||
outOfStockCount: Number(row.outOfStockCount || 0),
|
||||
totalStock: Number(row.totalStock || 0),
|
||||
totalPurchaseTransit: Number(row.totalPurchaseTransit || 0),
|
||||
lowStockDaysCount: Number(row.lowStockDaysCount || 0),
|
||||
configuredCount: Number(row.configuredCount || 0),
|
||||
monitorDate: row.monitorDate ? new Date(row.monitorDate).toISOString().slice(0, 10) : null,
|
||||
@ -422,19 +466,17 @@ async function buildSummary(whereSql, params) {
|
||||
}
|
||||
|
||||
async function getModularStockFilters() {
|
||||
const monitorDate = await resolveMonitorDate();
|
||||
const params = {
|
||||
wmsCoId: WAREHOUSE_WMS_CO_ID,
|
||||
warehouseName: WAREHOUSE_NAME,
|
||||
monitorDate,
|
||||
};
|
||||
|
||||
// 2026-09-18 修正:品类下拉不需要 JOIN 库存/采购/监控等重表,只用 SKU 主表即可,
|
||||
// 避免 buildBaseSql 里新增采购在途 OUTER APPLY 后把 filters 接口拖慢。
|
||||
const rows = await cloudDatabase.query(
|
||||
`SELECT DISTINCT s.category
|
||||
${buildBaseSql()} AND s.category IS NOT NULL AND LTRIM(RTRIM(s.category)) <> N''
|
||||
FROM dbo.jst_sku s
|
||||
WHERE s.other_3 = @warehouseName
|
||||
AND s.category IS NOT NULL
|
||||
AND LTRIM(RTRIM(s.category)) <> N''
|
||||
ORDER BY s.category`,
|
||||
params,
|
||||
{ requestTimeout: 180000 }
|
||||
{ warehouseName: WAREHOUSE_NAME },
|
||||
{ requestTimeout: 60000 }
|
||||
);
|
||||
|
||||
return {
|
||||
@ -445,6 +487,86 @@ async function getModularStockFilters() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 采购在途明细列表:目标仓全部「采购数量 > 已入库数量」的采购单明细。
|
||||
* 口径:在途数量 = qty − inQty(已下单但尚未入库的部分)。
|
||||
* 支持 keyword 搜索商品编码 / 名称,支持按 transitQty(默认)/ skuId / purchaseQty / deliveryDate 排序。
|
||||
*/
|
||||
async function getPurchaseInTransitList(query = {}) {
|
||||
const page = Math.max(1, Number.parseInt(query.page, 10) || 1);
|
||||
const requestedSize = Number.parseInt(query.pageSize, 10) || DEFAULT_PAGE_SIZE;
|
||||
const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, requestedSize));
|
||||
|
||||
const keyword = toText(query.keyword);
|
||||
const params = { warehouseName: WAREHOUSE_NAME };
|
||||
let whereSql = `s.other_3 = @warehouseName AND p.qty > ISNULL(p.inQty, 0)`;
|
||||
if (keyword) {
|
||||
params.keyword = `%${keyword}%`;
|
||||
whereSql += ` AND (p.sku_id LIKE @keyword OR p.name LIKE @keyword)`;
|
||||
}
|
||||
|
||||
const sortField = String(query.sortField || '').trim();
|
||||
const sortOrder = String(query.sortOrder || 'DESC').toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
|
||||
const orderMap = {
|
||||
skuId: 'p.sku_id',
|
||||
deliveryDate: 'p.delivery_date',
|
||||
transitQty: 'transitQty',
|
||||
purchaseQty: 'p.qty',
|
||||
};
|
||||
const orderCol = orderMap[sortField] || 'transitQty';
|
||||
const orderClause = sortField === 'skuId'
|
||||
? `${orderCol} ${sortOrder}`
|
||||
: `${orderCol} ${sortOrder}, p.sku_id ASC`;
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const [rows, totalRow] = await Promise.all([
|
||||
cloudDatabase.query(
|
||||
`SELECT
|
||||
p.sku_id AS skuId,
|
||||
p.name AS productName,
|
||||
CAST(p.qty AS BIGINT) AS purchaseQty,
|
||||
CAST(ISNULL(p.inQty, 0) AS BIGINT) AS inQty,
|
||||
CAST(p.qty - ISNULL(p.inQty, 0) AS BIGINT) AS transitQty,
|
||||
p.delivery_date AS deliveryDate,
|
||||
CAST(p.po_id AS NVARCHAR(50)) AS poId,
|
||||
CAST(p.poi_id AS NVARCHAR(50)) AS poiId
|
||||
FROM rpt.jst_purchaseItem p
|
||||
INNER JOIN dbo.jst_sku s ON s.sku_id = p.sku_id
|
||||
WHERE ${whereSql}
|
||||
ORDER BY ${orderClause}
|
||||
OFFSET ${offset} ROWS FETCH NEXT ${pageSize} ROWS ONLY`,
|
||||
params,
|
||||
{ requestTimeout: 180000 }
|
||||
),
|
||||
cloudDatabase.query(
|
||||
`SELECT COUNT_BIG(*) AS total
|
||||
FROM rpt.jst_purchaseItem p
|
||||
INNER JOIN dbo.jst_sku s ON s.sku_id = p.sku_id
|
||||
WHERE ${whereSql}`,
|
||||
params,
|
||||
{ requestTimeout: 180000 }
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
rows: rows.map((row) => ({
|
||||
skuId: toText(row.skuId),
|
||||
productName: toText(row.productName),
|
||||
purchaseQty: Number(row.purchaseQty || 0),
|
||||
inQty: Number(row.inQty || 0),
|
||||
transitQty: Number(row.transitQty || 0),
|
||||
deliveryDate: row.deliveryDate ? new Date(row.deliveryDate).toISOString().slice(0, 10) : null,
|
||||
poId: toText(row.poId),
|
||||
poiId: toText(row.poiId),
|
||||
})),
|
||||
total: Number(totalRow?.[0]?.total || 0),
|
||||
page,
|
||||
pageSize,
|
||||
warehouse: WAREHOUSE_NAME,
|
||||
};
|
||||
}
|
||||
|
||||
// 手动同步:本页无进程内缓存(每次请求直连副本库),同步动作 = 重新解析监控日并返回,
|
||||
// 前端据此刷新列表到最新快照。
|
||||
// 注意:副本库由聚水潭侧同步,应用层无法触发上游拉取,这里只保证取到最新监控日。
|
||||
@ -463,5 +585,6 @@ module.exports = {
|
||||
DEFAULT_SAFETY_DAYS,
|
||||
getModularStockList,
|
||||
getModularStockFilters,
|
||||
getPurchaseInTransitList,
|
||||
syncModularStock,
|
||||
};
|
||||
|
||||
@ -471,12 +471,42 @@ test('replenish: pending order fields default to empty without any unshipped ord
|
||||
assert.equal(noSale.warningLevel, '无销量');
|
||||
assert.equal(noSale.orderDriven, false);
|
||||
|
||||
// 有日销的商品照旧走原口径,但待发货量也要带上(页面新增列)
|
||||
// 有日销的商品:待发货订单先从库存里扣掉(结余库存口径,见服务文件头 ⑫)
|
||||
const withSale = metrics({ avgSale: 2, realtimeStock: 0, pendingOrderQty: 3 });
|
||||
assert.equal(withSale.warningLevel, '缺货');
|
||||
assert.equal(withSale.orderDriven, false);
|
||||
assert.equal(withSale.pendingOrderQty, 3);
|
||||
assert.equal(withSale.replenishQty, 6); // ceil(3 × 2 − 0),不受订单量影响
|
||||
assert.equal(withSale.availableStock, 0); // max(0, 0 − 3)
|
||||
assert.equal(withSale.stockDays, 0);
|
||||
assert.equal(withSale.replenishQty, 9); // ceil(3 × 2 + 3 − 0):先补订单缺口 3,再补 3 天缓冲 6
|
||||
|
||||
// 结余库存 > 0 时按结余算可用天数:库存 10 − 待发 3 = 7,7 ÷ 2 = 3.5 天 → 正常
|
||||
const buffered = metrics({ avgSale: 2, realtimeStock: 10, pendingOrderQty: 3 });
|
||||
assert.equal(buffered.availableStock, 7);
|
||||
assert.equal(buffered.stockDays, 3.5);
|
||||
assert.equal(buffered.warningLevel, '正常');
|
||||
assert.equal(buffered.replenishQty, 0); // ceil(6 + 3 − 10) < 0 → 0
|
||||
|
||||
// 结余库存不足以支撑 3 天日销 → 预警(MKHDNZ0409260601376 同型:库存3 待发3 日销0.27)
|
||||
const tight = metrics({ avgSale: 1, realtimeStock: 5, pendingOrderQty: 3 });
|
||||
assert.equal(tight.availableStock, 2);
|
||||
assert.equal(tight.stockDays, 2);
|
||||
assert.equal(tight.warningLevel, '预警');
|
||||
assert.equal(tight.replenishQty, 1); // ceil(3 + 3 − 5)
|
||||
});
|
||||
|
||||
// 回归:MKHDNZ0409260601376(2026-09-18 业务反馈)
|
||||
// 现有库存 3、待发货订单 3 → 结余库存 0、可用天数 0,应判「缺货」。
|
||||
// 旧代码只在 dailySale <= 0 时才让待发货订单参与判定,本例日销 0.27 > 0 导致漏报。
|
||||
test('replenish: pending orders are deducted even when daily sale is positive (⑫)', () => {
|
||||
const row = metrics({ avgSale: 0.27, realtimeStock: 3, pendingOrderQty: 3, pendingOrderCount: 3 });
|
||||
assert.equal(row.orderDriven, false);
|
||||
assert.equal(row.availableStock, 0);
|
||||
assert.equal(row.stockDays, 0);
|
||||
assert.equal(row.warningLevel, '缺货');
|
||||
assert.equal(row.hasWarning, true);
|
||||
assert.equal(row.warningOrder, 1);
|
||||
assert.equal(row.replenishQty, 1); // ceil(3 × 0.27 + 3 − 3) = ceil(0.81)
|
||||
});
|
||||
|
||||
// ── 导出(见服务文件头 ⑨)────────────────────────────────────────
|
||||
|
||||
@ -15,6 +15,12 @@ export const getModularStockFilters = () => request.get(
|
||||
{ timeout: META_TIMEOUT_MS }
|
||||
);
|
||||
|
||||
// 采购在途明细列表(目标仓全部「采购数量 > 已入库数量」的采购单明细)
|
||||
export const getPurchaseInTransitList = (params = {}) => request.get(
|
||||
'/api/modular-stock-monitor/purchase-in-transit',
|
||||
{ params, timeout: LIST_TIMEOUT_MS }
|
||||
);
|
||||
|
||||
// 手动同步:后端重新取最新监控日,前端随后再拉一次列表即拿到最新快照
|
||||
export const syncModularStock = () => request.post(
|
||||
'/api/modular-stock-monitor/sync',
|
||||
|
||||
@ -63,6 +63,7 @@ const shortageTooltip = (row) => {
|
||||
const SCOPE_OPTIONS = [
|
||||
{ label: '仅预警', value: 'warning' },
|
||||
{ label: '有日销', value: 'sold' },
|
||||
{ label: '加工在途', value: 'work-order' },
|
||||
{ label: '全部', value: 'all' },
|
||||
];
|
||||
|
||||
@ -76,6 +77,7 @@ const PRODUCT_EXPORT_COLUMNS = [
|
||||
{ title: '现有库存', dataIndex: 'stock' },
|
||||
{ title: '在途加工量', dataIndex: 'pendingWorkOrderQty' },
|
||||
{ title: '待发货订单', dataIndex: 'pendingOrderQty' },
|
||||
{ title: '结余库存', dataIndex: 'availableStock' },
|
||||
{
|
||||
title: '可用天数',
|
||||
dataIndex: 'stockDays',
|
||||
@ -119,7 +121,9 @@ const EXPORT_MENU_ITEMS = [
|
||||
{ key: 'components-csv', label: '配件明细 · CSV' },
|
||||
];
|
||||
|
||||
const EXPORT_SCOPE_LABEL = { warning: '仅预警', sold: '有日销', all: '全部' };
|
||||
const EXPORT_SCOPE_LABEL = {
|
||||
warning: '仅预警', sold: '有日销', 'work-order': '加工在途', all: '全部',
|
||||
};
|
||||
|
||||
// 补货预警推送:范围选项与跳过原因文案
|
||||
const ALERT_SCOPE_OPTIONS = [
|
||||
@ -144,6 +148,17 @@ const ALERT_RECIPIENT_SOURCE_TEXT = {
|
||||
};
|
||||
|
||||
// A4 打印预览:表头 MKH商品编码 | 配件明细 | 数量 | 配件仓位
|
||||
// 仓位在库里是「仓库名 · 货位」的形式(如「常州简尚家居有限公司 · BCP01-01-1」),
|
||||
// 打印给车间/仓库用时只留货位,去掉公司名前缀(业务要求 2026-09-18)。
|
||||
const stripWarehousePrefix = (value) => String(value || '')
|
||||
.split('、')
|
||||
.map((part) => {
|
||||
const idx = part.lastIndexOf('·');
|
||||
return (idx >= 0 ? part.slice(idx + 1) : part).trim();
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
|
||||
const PrintPreview = ({ data, onClose }) => {
|
||||
const now = useMemo(() => new Date().toLocaleString('zh-CN'), []);
|
||||
const items = data.items || [];
|
||||
@ -178,7 +193,7 @@ const PrintPreview = ({ data, onClose }) => {
|
||||
<table className="mkh-print-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '170px' }}>MKH商品编码</th>
|
||||
<th style={{ width: '190px' }}>MKH商品编码</th>
|
||||
<th>配件明细</th>
|
||||
<th style={{ width: '64px' }}>数量</th>
|
||||
<th style={{ width: '110px' }}>配件仓位</th>
|
||||
@ -192,7 +207,7 @@ const PrintPreview = ({ data, onClose }) => {
|
||||
) : null}
|
||||
<td>{text(item.skuName)}{item.skuId ? <span className="mkh-print-code">{item.skuId}</span> : null}</td>
|
||||
<td className="mkh-center">{number(item.qty)}</td>
|
||||
<td className="mkh-center">{item.location ? text(item.location) : <span className="mkh-muted">未维护</span>}</td>
|
||||
<td className="mkh-center">{item.location ? text(stripWarehousePrefix(item.location)) : <span className="mkh-muted">未维护</span>}</td>
|
||||
</tr>
|
||||
))}
|
||||
{items.length === 0 ? (
|
||||
@ -305,6 +320,12 @@ const MkhBomData = () => {
|
||||
const changeFilter = (key) => (value) => {
|
||||
setFilters((old) => ({ ...old, [key]: value || '' }));
|
||||
setPage(1);
|
||||
// 点击「仅预警 / 有日销 / 加工在途 / 全部」后自动滚动到下方表格明细
|
||||
if (key === 'scope') {
|
||||
setTimeout(() => {
|
||||
tableWrapRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
const resetFilters = () => { setFilters(EMPTY_FILTERS); setPage(1); };
|
||||
|
||||
@ -571,6 +592,23 @@ const MkhBomData = () => {
|
||||
</Tooltip>
|
||||
) : <span className="mkh-muted">—</span>),
|
||||
},
|
||||
{
|
||||
// 结余库存(2026-09-18 新增口径):现有库存先扣掉待发货订单,才是可自由支配的库存,
|
||||
// 补货预警 / 可用天数一律按它算(待发订单为 0 时 ≡ 现有库存)
|
||||
title: '结余库存',
|
||||
dataIndex: 'availableStock',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
render: (value, row) => (
|
||||
<Tooltip
|
||||
title={Number(row.pendingOrderQty) > 0
|
||||
? `结余库存 = 现有库存 ${number(row.stock)} − 待发货订单 ${number(row.pendingOrderQty)}`
|
||||
: '无待发货订单,结余库存 = 现有库存'}
|
||||
>
|
||||
<span className={Number(value) <= 0 ? 'mkh-days--warn' : undefined}>{number(value)}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '可用天数',
|
||||
dataIndex: 'stockDays',
|
||||
@ -591,7 +629,15 @@ const MkhBomData = () => {
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return text;
|
||||
return (
|
||||
<Tooltip title={
|
||||
Number(row.pendingOrderQty) > 0
|
||||
? `可用天数 = 结余库存 ${number(row.availableStock)} ÷ 近15天日销 ${number(row.dailySale, 2)}(结余库存已扣待发货订单)`
|
||||
: `可用天数 = 现有库存 ${number(row.stock)} ÷ 近15天日销 ${number(row.dailySale, 2)}`
|
||||
}>
|
||||
{text}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -751,8 +797,10 @@ const MkhBomData = () => {
|
||||
优先显示具体仓位,没有具体仓位时显示箱号,表中没有有效记录时显示「未维护」。
|
||||
<br />
|
||||
补货预警:近15天日销取聚水潭库存监控快照(监控日 {data.monitorDate || '—'}),现有库存优先取品牌仓实时库存、
|
||||
该仓无记录时回退监控快照库存;可用天数 = 现有库存 ÷ 近15天日销,低于 3 天即预警,缺货单独标识;
|
||||
建议补货量 = 3 × 近15天日销 − 现有库存(向上取整)。无日销且无订单的商品不参与预警。
|
||||
该仓无记录时回退监控快照库存;<b>结余库存 = 现有库存 − 待发货订单</b>(已下单未发货的部分已被占用,不足 0 取 0),
|
||||
可用天数 = 结余库存 ÷ 近15天日销,低于 3 天即预警,结余库存为 0 判缺货;
|
||||
建议补货量 = 3 × 近15天日销 + 待发货订单 − 现有库存(向上取整,先补到能把订单发掉再补 3 天缓冲)。
|
||||
无日销且无订单的商品不参与预警。
|
||||
<br />
|
||||
待发货订单(代发):近15天暂无日销、但聚水潭存在「未发货订单」的 MKH 成品同样列入补货预警
|
||||
(新品刚接到订单时监控快照的日销还是 0,避免漏备货)。未发货 = 订单无发货时间且状态非「取消 / 已合并」;
|
||||
@ -785,6 +833,7 @@ const MkhBomData = () => {
|
||||
options={[
|
||||
{ label: `仅预警 (${data.warningCount ?? 0})`, value: 'warning' },
|
||||
{ label: `有日销 (${data.soldCount ?? 0})`, value: 'sold' },
|
||||
{ label: `加工在途 (${data.pendingWorkOrderSkuCount ?? 0})`, value: 'work-order' },
|
||||
{ label: `全部 (${data.mkhTotalCount ?? 0})`, value: 'all' },
|
||||
]}
|
||||
/>
|
||||
@ -847,7 +896,7 @@ const MkhBomData = () => {
|
||||
按近 {data.saleWindowDays || 15} 天日销核算(监控日 {data.monitorDate || '—'}):
|
||||
其中 <b>{data.outOfStockCount || 0}</b> 个已缺货,
|
||||
<b>{Math.max(0, (data.warningCount || 0) - (data.outOfStockCount || 0))}</b> 个可用天数不足 {data.replenishDaysThreshold || 3} 天。
|
||||
建议补货量 = {data.replenishDaysThreshold || 3} × 近15天日销 − 现有库存。点「只看预警」即为补货清单。
|
||||
建议补货量 = {data.replenishDaysThreshold || 3} × 近15天日销 + 待发货订单 − 现有库存(库存已先扣待发货订单后再核算)。点「只看预警」即为补货清单。
|
||||
{(data.orderDrivenCount || 0) > 0 ? (
|
||||
<>
|
||||
<br />
|
||||
@ -937,7 +986,7 @@ const MkhBomData = () => {
|
||||
pageSizeOptions: [50, 100, 200],
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
scroll={{ x: 1766, y: bodyHeight }}
|
||||
scroll={{ x: 1866, y: bodyHeight }}
|
||||
/>
|
||||
</div>
|
||||
</Spin>
|
||||
|
||||
@ -235,7 +235,10 @@
|
||||
|
||||
.mkh-print-table .mkh-print-sku {
|
||||
font-family: 'SFMono-Regular', Consolas, Menlo, monospace;
|
||||
font-size: 11px;
|
||||
/* 2026-09-18 业务要求调大:原来是 11px,车间核对编码时太小 */
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@ -243,8 +246,10 @@
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-family: 'SFMono-Regular', Consolas, Menlo, monospace;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
/* 2026-09-18 业务要求调大:原来是 10px / #666,扫码与人工核对都偏小偏淡 */
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.mkh-print-foot {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert, Button, Card, Col, Dropdown, Image, Input, Row, Select,
|
||||
Alert, Button, Card, Col, Dropdown, Image, Input, Modal, Row, Select,
|
||||
Space, Spin, Statistic, Switch, Table, Tabs, Tag, Tooltip, Typography, message,
|
||||
} from 'antd';
|
||||
import { DownloadOutlined, ReloadOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { SycmPage } from '../../components/Common';
|
||||
import { getModularStockList, getModularStockFilters, syncModularStock } from '../../api/modularStockMonitor';
|
||||
import { getModularStockList, getModularStockFilters, getPurchaseInTransitList, syncModularStock } from '../../api/modularStockMonitor';
|
||||
import { exportTableFile } from '../../utils/tableExport';
|
||||
import './style.css';
|
||||
|
||||
@ -45,6 +45,15 @@ const AccessoryStockPanel = () => {
|
||||
const tableWrapRef = useRef(null);
|
||||
const [bodyHeight, setBodyHeight] = useState(420);
|
||||
|
||||
// 采购在途明细弹窗状态
|
||||
const [transitVisible, setTransitVisible] = useState(false);
|
||||
const [transitData, setTransitData] = useState({ rows: [], total: 0 });
|
||||
const [transitLoading, setTransitLoading] = useState(false);
|
||||
const [transitPage, setTransitPage] = useState(1);
|
||||
const [transitPageSize, setTransitPageSize] = useState(50);
|
||||
const [transitKeyword, setTransitKeyword] = useState('');
|
||||
const [transitSorter, setTransitSorter] = useState({ field: 'transitQty', order: 'DESC' });
|
||||
|
||||
// 表体高度按「表格顶部 → 内容区底部」实测计算,替代纯 CSS calc:
|
||||
// 不同显示器缩放(125%/150%)下 100vh 偏小,写死的 calc 会把表格压到只剩一两行
|
||||
useEffect(() => {
|
||||
@ -135,6 +144,56 @@ const AccessoryStockPanel = () => {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
// 点击顶部统计卡片 → 自动设置对应筛选条件并滚动到表格明细
|
||||
const applySummaryFilter = (type) => () => {
|
||||
const next = { ...EMPTY_FILTERS };
|
||||
let warning = false;
|
||||
if (type === 'outOfStock') {
|
||||
next.warningLevel = '缺货';
|
||||
warning = true;
|
||||
} else if (type === 'lowStockDays') {
|
||||
next.warningLevel = '预警';
|
||||
warning = true;
|
||||
} else if (type === 'configured') {
|
||||
next.mkhStatus = '已配置MKH';
|
||||
} else if (type === 'unconfigured') {
|
||||
next.mkhStatus = '未配置MKH';
|
||||
}
|
||||
setFilters(next);
|
||||
setOnlyWarning(warning);
|
||||
setPage(1);
|
||||
setTimeout(() => {
|
||||
tableWrapRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// 打开采购在途明细弹窗
|
||||
const openPurchaseTransit = () => {
|
||||
setTransitVisible(true);
|
||||
setTransitPage(1);
|
||||
setTransitKeyword('');
|
||||
setTransitSorter({ field: 'transitQty', order: 'DESC' });
|
||||
loadPurchaseTransit(1, '', { field: 'transitQty', order: 'DESC' });
|
||||
};
|
||||
|
||||
const loadPurchaseTransit = useCallback(async (p = transitPage, kw = transitKeyword, sort = transitSorter) => {
|
||||
setTransitLoading(true);
|
||||
try {
|
||||
const result = await getPurchaseInTransitList({
|
||||
page: p,
|
||||
pageSize: transitPageSize,
|
||||
keyword: kw,
|
||||
sortField: sort.field,
|
||||
sortOrder: sort.order,
|
||||
});
|
||||
setTransitData(result?.data || { rows: [], total: 0 });
|
||||
} catch (err) {
|
||||
message.error(err?.response?.data?.error || err?.message || '加载采购在途明细失败');
|
||||
} finally {
|
||||
setTransitLoading(false);
|
||||
}
|
||||
}, [transitPage, transitPageSize, transitKeyword, transitSorter]);
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
title: '图片',
|
||||
@ -219,10 +278,22 @@ const AccessoryStockPanel = () => {
|
||||
sorter: true,
|
||||
render: int,
|
||||
},
|
||||
{
|
||||
title: <TipTitle
|
||||
title="采购在途数量"
|
||||
tip="该商品(配件)全部采购单中『已下单 − 已入库』的未到货数量合计,取自 jst_purchaseItem.qty − inQty"
|
||||
/>,
|
||||
dataIndex: 'purchaseTransit',
|
||||
width: 120,
|
||||
align: 'right',
|
||||
render: (value) => (value === null || value === undefined || Number(value) === 0
|
||||
? <span className="msm-muted">--</span>
|
||||
: int(value)),
|
||||
},
|
||||
{
|
||||
title: <TipTitle
|
||||
title="仓位"
|
||||
tip="业务库 jst_sku.bin / other_bin 全库 55.7 万行均为空,上游未回传库位数据,因此该列暂无值"
|
||||
tip="取自聚水潭「箱及仓位库存」jst_packInventoryInfo 的真货位(bin),同商品多箱时显示库存最大的主货位;该表查不到才显示「未维护」"
|
||||
/>,
|
||||
dataIndex: 'location',
|
||||
width: 88,
|
||||
@ -353,11 +424,32 @@ const AccessoryStockPanel = () => {
|
||||
|
||||
<Row gutter={[12, 12]} className="msm-summary-row">
|
||||
<Col xs={12} md={4}><Card bordered={false} className="msm-summary-card"><Statistic title="SKU 总数" value={summary.skuCount || 0} /></Card></Col>
|
||||
<Col xs={12} md={4}><Card bordered={false} className="msm-summary-card is-red"><Statistic title="缺货 SKU" value={summary.outOfStockCount || 0} /></Card></Col>
|
||||
<Col xs={12} md={4}><Card bordered={false} className="msm-summary-card is-orange"><Statistic title="可用天数不足" value={summary.lowStockDaysCount || 0} suffix="个 SKU" /></Card></Col>
|
||||
<Col xs={12} md={4}>
|
||||
<Card bordered={false} className="msm-summary-card is-red is-clickable" onClick={applySummaryFilter('outOfStock')}>
|
||||
<Statistic title="缺货 SKU" value={summary.outOfStockCount || 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={4}>
|
||||
<Card bordered={false} className="msm-summary-card is-orange is-clickable" onClick={applySummaryFilter('lowStockDays')}>
|
||||
<Statistic title="可用天数不足" value={summary.lowStockDaysCount || 0} suffix="个 SKU" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={4}><Card bordered={false} className="msm-summary-card is-green"><Statistic title="实时库存合计" value={summary.totalStock || 0} /></Card></Col>
|
||||
<Col xs={12} md={4}><Card bordered={false} className="msm-summary-card"><Statistic title="已配置MKH" value={summary.configuredCount || 0} /></Card></Col>
|
||||
<Col xs={12} md={4}><Card bordered={false} className="msm-summary-card"><Statistic title="未配置MKH" value={summary.unconfiguredCount || 0} /></Card></Col>
|
||||
<Col xs={12} md={4}>
|
||||
<Card bordered={false} className="msm-summary-card is-blue is-clickable" onClick={openPurchaseTransit}>
|
||||
<Statistic title="采购在途" value={summary.totalPurchaseTransit || 0} suffix="件" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={4}>
|
||||
<Card bordered={false} className="msm-summary-card is-clickable" onClick={applySummaryFilter('configured')}>
|
||||
<Statistic title="已配置MKH" value={summary.configuredCount || 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={4}>
|
||||
<Card bordered={false} className="msm-summary-card is-clickable" onClick={applySummaryFilter('unconfigured')}>
|
||||
<Statistic title="未配置MKH" value={summary.unconfiguredCount || 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card
|
||||
@ -411,6 +503,75 @@ const AccessoryStockPanel = () => {
|
||||
</div>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={(
|
||||
<Space>
|
||||
<span>采购在途明细</span>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
共 {transitData.total || 0} 条采购单 / 在途 {summary.totalPurchaseTransit || 0} 件
|
||||
</Text>
|
||||
</Space>
|
||||
)}
|
||||
open={transitVisible}
|
||||
onCancel={() => setTransitVisible(false)}
|
||||
width={960}
|
||||
footer={null}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder="商品编码 / 名称"
|
||||
defaultValue={transitKeyword}
|
||||
onSearch={(value) => { setTransitKeyword(value); setTransitPage(1); loadPurchaseTransit(1, value, transitSorter); }}
|
||||
style={{ width: 260 }}
|
||||
/>
|
||||
<Table
|
||||
rowKey={(row) => `${row.skuId}-${row.poiId || row.poId || Math.random()}`}
|
||||
size="small"
|
||||
loading={transitLoading}
|
||||
columns={[
|
||||
{
|
||||
title: '商品编码', dataIndex: 'skuId', width: 176, render: (v) => <span className="msm-sku">{text(v)}</span>,
|
||||
},
|
||||
{
|
||||
title: '商品名称', dataIndex: 'productName', width: 240, ellipsis: true, render: (v) => <Tooltip title={v}>{text(v)}</Tooltip>,
|
||||
},
|
||||
{ title: '采购数量', dataIndex: 'purchaseQty', width: 96, align: 'right', render: int, sorter: true },
|
||||
{ title: '已入库', dataIndex: 'inQty', width: 84, align: 'right', render: int, sorter: true },
|
||||
{
|
||||
title: '在途数量', dataIndex: 'transitQty', width: 96, align: 'right', sorter: true,
|
||||
render: (value) => <strong style={{ color: '#1677ff' }}>{int(value)}</strong>,
|
||||
},
|
||||
{
|
||||
title: '计划到货日期', dataIndex: 'deliveryDate', width: 120, align: 'center', sorter: true,
|
||||
render: (value) => text(value),
|
||||
},
|
||||
{ title: '采购单号', dataIndex: 'poId', width: 116, render: text },
|
||||
]}
|
||||
dataSource={transitData.rows || []}
|
||||
pagination={{
|
||||
current: transitPage,
|
||||
pageSize: transitPageSize,
|
||||
total: transitData.total || 0,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [50, 100, 200],
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
onChange={(pagination, _filters, nextSorter) => {
|
||||
setTransitPage(pagination.current || 1);
|
||||
setTransitPageSize(pagination.pageSize || 50);
|
||||
const nextSort = nextSorter?.field
|
||||
? { field: nextSorter.field, order: nextSorter.order === 'ascend' ? 'ASC' : 'DESC' }
|
||||
: { field: 'transitQty', order: 'DESC' };
|
||||
setTransitSorter(nextSort);
|
||||
loadPurchaseTransit(pagination.current || 1, transitKeyword, nextSort);
|
||||
}}
|
||||
scroll={{ x: 860, y: 360 }}
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -426,7 +587,8 @@ const ModularStockMonitor = () => (
|
||||
<Text type="secondary" className="msm-note">
|
||||
数据口径:实时库存取 jst_inventoryInfo(wms_co_id 15718196);近 15 天日均发货量、
|
||||
库存可用天数、安全天数取 pm_stockMonitor 最新快照(按完整 SKU 编码关联,794 个 SKU 全覆盖);
|
||||
当日入库取采购到货、当日出库取近一日发货量,均以监控日为基准;仓位上游未回传,暂显示「未维护」;
|
||||
当日入库取采购到货、当日出库取近一日发货量,均以监控日为基准;
|
||||
仓位取聚水潭「箱及仓位库存」(jst_packInventoryInfo)的真货位,同商品多箱时显示库存最大的主货位,查不到才显示「未维护」;
|
||||
图片取自「产品资料系统数据库」商品档案(业务库图片字段均为空)。
|
||||
注:日均发货量 2026-09-17 起由近 7 天改为近 15 天(业务原要求 30 天,但业务库无 30 天日销,最长只到 15 天);
|
||||
库存可用天数仍沿用监控表 jsUseDays(源系统口径),故两者并非同一时间窗。
|
||||
|
||||
@ -87,6 +87,7 @@
|
||||
.msm-summary-card.is-red::before { background: var(--sycm-color-danger); }
|
||||
.msm-summary-card.is-orange::before { background: var(--sycm-color-orange); }
|
||||
.msm-summary-card.is-green::before { background: var(--sycm-color-success); }
|
||||
.msm-summary-card.is-blue::before { background: var(--sycm-color-primary); }
|
||||
.msm-summary-card .ant-card-body {
|
||||
display: flex;
|
||||
min-height: 98px;
|
||||
@ -107,6 +108,15 @@
|
||||
}
|
||||
.msm-summary-card .ant-statistic-content-suffix { font-size: var(--sycm-font-size-caption); }
|
||||
|
||||
.msm-summary-card.is-clickable {
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.msm-summary-card.is-clickable:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.msm-panel { margin-bottom: 0; overflow: hidden; }
|
||||
.msm-panel .ant-card-head {
|
||||
min-height: 48px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user