forked from lunanting/cangku
按「库存监控」表格在侧边栏增加一级/二级菜单与共用页面 (路由 /planning/large-stock-monitor/:warehouseSlug)。 后端: - 新增 largeStockMonitorConfig.js:26 仓配置,含 wms_co_id / 快照仓名 / routeKey / slug 映射 - 新增 largeStockMonitor.js:config / overview / sku-list 三个接口 - 新增 largeStockMonitorFlow.js:采购进仓、采购退货、销售出仓、销售退货四类单据流水取数 - 新增 verify-large-stock-monitor.js:26 仓端到端校验脚本 - index.js:挂载 /api/large-stock-monitor 路由 口径说明: - 库存周转天数 = 月度发货总件数 / 月度日均库存件数 × 100%,下沉到 SQL CASE WHEN 计算 - 四级分级(滞销预警/慢流/呆滞/僵尸)按库龄 + 周转率双维度判定 - 期末数量取聚水潭实际库存 jst_inventoryInfo(按 wms_co_id 单仓), 不用 pm_stockMonitor.jsStock —— 后者 warehouse 是「归属仓」口径, 会把已调拨到外仓的货仍计在本仓名下(崔桥工厂仓因此虚高约 2.2 万) - 截止时刻统一为快照日次日 01:00(跑批时点),库龄/分级均以此为基准,不再用 GETDATE() 前端: - 新增 LargeStockMonitor 页面、18 列定义、API 封装与样式 - 路由与侧边栏:26 条二级菜单 + 权限点 冲突处理: - 主仓库已下线 /api/dashboard(backend/src/api/dashboard.js 被移除), 相应删除 dashboardRoutes 的 require 与挂载,避免后端启动时 require 失败崩溃 - 保留主仓库新增的「模块化库存监控」菜单组及其子项
133 lines
5.4 KiB
JavaScript
133 lines
5.4 KiB
JavaScript
// 大货库存监控 - 出入库流水取数(本月 1 日 ~ 快照日)
|
||
//
|
||
// 业务库 JSReportReplica 里与「库存流水」相关的单据表:
|
||
// 采购进仓 rpt.jst_purchase (wms_co_id, po_date, status) + rpt.jst_purchaseItem (po_id, inQty)
|
||
// 采购退货 rpt.jst_purchaseout (wms_co_id, io_date, status) + rpt.jst_purchaseoutItem (io_id, qty)
|
||
// 销售出仓 rpt.jst_salesOutboundInfo(wms_co_id, io_date, status) + rpt.jst_OrderItem (o_id, qty)
|
||
// 销售退货 rpt.jst_returnAndRefundInfo(warehouse 名称) + rpt.jst_returnAndRefundItem(as_id, r_qty)
|
||
//
|
||
// 有效单据状态:只统计 Confirmed / Finished,排除 Cancelled / Delete / WaitConfirm。
|
||
// 业务库中不存在加工、盘点、调拨、其它出库任何单据表,这 6 列无法取数。
|
||
const cloudDb = require('../config/cloudDatabase');
|
||
|
||
// 单据有效状态(已确认/已完成),避免把取消单算进流水
|
||
const PURCHASE_STATUS = "'Confirmed','Finished'";
|
||
const PURCHASEOUT_STATUS = "'Confirmed'";
|
||
const SALESOUT_STATUS = "'Confirmed'";
|
||
const REFUND_STATUS = "'Confirmed'";
|
||
|
||
function toNumber(value) {
|
||
if (value == null) return null;
|
||
const n = Number(value);
|
||
return Number.isFinite(n) ? n : null;
|
||
}
|
||
|
||
/**
|
||
* 采购进仓:采购单已入库数量(inQty)
|
||
*/
|
||
async function fetchPurchaseIn(wmsCoId, periodStart, periodEnd) {
|
||
const rows = await cloudDb.query(
|
||
`SELECT ISNULL(SUM(CAST(pi.inQty AS DECIMAL(18, 2))), 0) AS v
|
||
FROM rpt.jst_purchase p
|
||
JOIN rpt.jst_purchaseItem pi ON pi.po_id = p.po_id
|
||
WHERE p.wms_co_id = @wmsCoId
|
||
AND p.po_date >= @periodStart AND p.po_date < @periodEnd
|
||
AND p.status IN (${PURCHASE_STATUS})`,
|
||
{ wmsCoId, periodStart, periodEnd }
|
||
);
|
||
return toNumber(rows[0]?.v);
|
||
}
|
||
|
||
/**
|
||
* 采购退货:采购退货出库单数量
|
||
*/
|
||
async function fetchPurchaseReturn(wmsCoId, periodStart, periodEnd) {
|
||
const rows = await cloudDb.query(
|
||
`SELECT ISNULL(SUM(CAST(oi.qty AS DECIMAL(18, 2))), 0) AS v
|
||
FROM rpt.jst_purchaseout o
|
||
JOIN rpt.jst_purchaseoutItem oi ON oi.io_id = o.io_id
|
||
WHERE o.wms_co_id = @wmsCoId
|
||
AND o.io_date >= @periodStart AND o.io_date < @periodEnd
|
||
AND o.status IN (${PURCHASEOUT_STATUS})`,
|
||
{ wmsCoId, periodStart, periodEnd }
|
||
);
|
||
return toNumber(rows[0]?.v);
|
||
}
|
||
|
||
/**
|
||
* 销售出仓:销售出库单关联的订单明细数量
|
||
* 注意:jst_salesOutboundInfo.qty 是表头字段且恒为 0,真实件数在 jst_OrderItem.qty
|
||
*/
|
||
async function fetchSalesOut(wmsCoId, periodStart, periodEnd) {
|
||
const rows = await cloudDb.query(
|
||
`SELECT ISNULL(SUM(CAST(oi.qty AS DECIMAL(18, 2))), 0) AS v
|
||
FROM rpt.jst_salesOutboundInfo s
|
||
JOIN rpt.jst_OrderItem oi ON oi.o_id = s.o_id
|
||
WHERE s.wms_co_id = @wmsCoId
|
||
AND s.io_date >= @periodStart AND s.io_date < @periodEnd
|
||
AND s.status IN (${SALESOUT_STATUS})`,
|
||
{ wmsCoId, periodStart, periodEnd }
|
||
);
|
||
return toNumber(rows[0]?.v);
|
||
}
|
||
|
||
/**
|
||
* 销售退货:退货退款单明细的实退数量(r_qty)
|
||
* 该表 wms_co_id 大量为空(9 月 92821 单中 75094 单为空),改用 warehouse 名称匹配:
|
||
* 名称形如「简尚-崔桥工厂仓销退仓」/「简尚-分销部睢宁专用仓主仓」,统一去掉「销退仓」「主仓」后比对。
|
||
*/
|
||
async function fetchSalesReturn(dbWarehouse, periodStart, periodEnd) {
|
||
if (!dbWarehouse) return null;
|
||
const rows = await cloudDb.query(
|
||
`SELECT ISNULL(SUM(CAST(ri.r_qty AS DECIMAL(18, 2))), 0) AS v
|
||
FROM rpt.jst_returnAndRefundInfo r
|
||
JOIN rpt.jst_returnAndRefundItem ri ON ri.as_id = r.as_id
|
||
WHERE REPLACE(REPLACE(ISNULL(r.warehouse, ''), N'销退仓', N''), N'主仓', N'') = @whName
|
||
AND r.created >= @periodStart AND r.created < @periodEnd
|
||
AND r.status IN (${REFUND_STATUS})`,
|
||
{ whName: dbWarehouse, periodStart, periodEnd }
|
||
);
|
||
return toNumber(rows[0]?.v);
|
||
}
|
||
|
||
/**
|
||
* 仓库实物库存:聚水潭实时库存表 rpt.jst_inventoryInfo(按 wms_co_id 单仓统计)
|
||
* 这是报表「期末数量」的取数口径 —— pm_stockMonitor 的 warehouse 仅是归属仓,
|
||
* 其 jsStock 含已调拨到外仓的货,不能代表本仓实物库存。
|
||
*/
|
||
async function fetchInventoryQty(wmsCoId) {
|
||
if (!wmsCoId) return null;
|
||
const rows = await cloudDb.query(
|
||
`SELECT ISNULL(SUM(CAST(qty AS DECIMAL(18, 2))), 0) AS v,
|
||
COUNT(CASE WHEN ISNULL(qty, 0) <> 0 THEN 1 END) AS skuCount,
|
||
MAX(modified) AS takenAt
|
||
FROM rpt.jst_inventoryInfo
|
||
WHERE wms_co_id = @wmsCoId`,
|
||
{ wmsCoId }
|
||
);
|
||
return {
|
||
qty: toNumber(rows[0]?.v),
|
||
skuCount: toNumber(rows[0]?.skuCount) || 0,
|
||
takenAt: rows[0]?.takenAt ? new Date(rows[0].takenAt).toISOString() : null,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 一次性取全部流水;任一子查询失败不影响其它字段(返回 null 由调用方降级为「—」)
|
||
*/
|
||
async function fetchFlowSummary({ wmsCoId, dbWarehouse, periodStart, periodEnd }) {
|
||
const [purchaseIn, purchaseReturn, salesOut, salesReturn] = await Promise.all([
|
||
wmsCoId ? fetchPurchaseIn(wmsCoId, periodStart, periodEnd).catch(() => null) : null,
|
||
wmsCoId ? fetchPurchaseReturn(wmsCoId, periodStart, periodEnd).catch(() => null) : null,
|
||
wmsCoId ? fetchSalesOut(wmsCoId, periodStart, periodEnd).catch(() => null) : null,
|
||
fetchSalesReturn(dbWarehouse, periodStart, periodEnd).catch(() => null),
|
||
]);
|
||
|
||
return { purchaseIn, purchaseReturn, salesOut, salesReturn };
|
||
}
|
||
|
||
module.exports = {
|
||
fetchFlowSummary,
|
||
fetchInventoryQty,
|
||
};
|