大家好,我是喜乐君。在制造业数据分析领域深耕多年,今天和大家深度拆解 Monitor ERP 的拒收数据架构。
在制造业质量管理中,拒收(Rejection)是最直接影响成本和交付的关键指标之一。Monitor ERP 作为北欧领先的制造执行系统(MES),其拒收数据架构涉及 18 张核心数据表、5 个嵌套成本子查询和三维成本拆分逻辑。本文将带你深入拆解这套系统的数据结构,并提供一份可直接用于 Tableau 分析的完整 SQL 查询。
一、Monitor ERP 中的”拒收”到底是什么?
在深入技术细节之前,先厘清一个常见的概念混淆:Monitor ERP 中的”拒收” 可以是生产过程,也可以是采购到货,我们这里仅仅以生产拒收为例介绍。
| 对比维度 | 拒收列表(ManufacturingOrderReporting) |
|---|---|
| 数据来源 | ManufacturingOrderReporting(制造订单报工) |
| 业务场景 | 生产过程中的工序拒收 |
| 成本计算 | 基于工序成本核算(精确) |
| 关联实体 | 工作中心、制造订单节点 |
| 成本维度 | 加工、外协、材料三维拆分 |
制造订单维度的拒收列表,它直接反映生产工序的质量状况,并且具有精确的多维成本核算能力。
二、18 张数据表:拒收查询的完整数据模型
一条拒收记录看似简单——”某道工序拒收了多少件、什么原因”——但背后需要关联 18 张数据表才能还原完整的信息。我们可以将它们分为四个层级:
2.1 第一层:报工主链(4 张表)
这是数据的入口,从报工动作开始逐级下钻:
- ManufacturingOrderReporting(制造订单报工)— 主表,记录每次报工操作
- ManufacturingOrderOperationReporting(工序报工)— 报工的工序级别明细
- ManufacturingOrder(制造订单)— 订单主信息,包含注册日期(成本核算的时间锚点)
- ManufacturingOrderOperationRejectionRow(工序拒收行)— INNER JOIN 的核心表,仅当有拒收时才有记录
2.2 第二层:工序与物料定义(5 张表)
定义”在哪个工作中心、做哪道工序、用哪些物料”:
- ManufacturingOrderOperation(工序定义)— 工序号、成本因子、是否外协
- ManufacturingOrderNode(订单BOM节点)— 连接制造订单与零件
- ManufacturingOrderMaterial(订单物料)— 每道工序的物料清单(BOM展开)
- WorkCenter(工作中心)— 实际执行加工的场地
- Part(零件)— 物料主数据,含仓储加价规则ID
2.3 第三层:成本核算引擎(4 张表)
这是最复杂的部分,负责将拒收数量转化为可量化的成本:
- PartCalculation(零件成本计算)— 存储18个维度的成本数据(材料、外协、加工各3列×2种来源)
- PreCalculationParameters(预计算参数)— 计算日期,用于时间锚定
- StorageOverheadMarkup(仓储间接费加价)— 按 Type 分为材料/外协/通用三类加价规则
- PartPlanningInformation(零件计划信息)— 零件在不同仓库的计划信息
2.4 第四层:辅助信息(5 张表)
- RejectionCodeItem(拒收代码)— 拒收原因分类,含自动退料标志
- DynamicPhrase(动态短语)— Monitor 的多语言引擎,几乎所有文本描述都通过它实现
- CommentText(评论文本)— 拒收记录的详细说明
- ProductRecord(产品记录)— 产品序列号,用于质量追溯
- PartUnitUsage + Unit(零件单位)— 零件的标准计量单位
借助于E-R,我们可以把这些表的关系勾勒出来,如下所示:

2.5 SQL查询
下面是基于以上数据模型的完整 SQL 查询,专为 Tableau 分析设计。查询包含:
- 16 个主表 LEFT/INNER JOIN 关联
- 5 个 OUTER APPLY 成本子查询(含 2 个嵌套子查询)
- 所有字段均带中文字段别名
- 三维成本拆分:加工成本、外协加工成本、材料成本、拒收总成本
核心SELECT结构:查询的 SELECT 部分输出以下 10 个信息区块:
- 制造订单与报工基本信息 — 订单号、报工时间
- 拒收原因信息 — 拒收代码、拒收原因描述
- 零件信息 — 零件编号、零件描述
- 工序与工作中心信息 — 工序号、工序描述、工作中心编号
- 拒收数量 — 拒收件数
- 成本分析 — 加工成本、外协加工成本、材料成本、拒收总成本
- 拒收评论 — 条件性输出(仅当有评论内容时显示ID)
- 产品追溯信息 — 产品序列号
- 零件标准单位 — 单位代码(通过 DynamicPhrase 多语言)
- 扩展字段 — 12 个 ExtraField 开关字段
以下是简化的查询骨架(完整查询代码较长,建议从本文附带的 SQL 文件获取):
SELECT _ManufacturingOrder.OrderNumber AS 制造订单号, _ManufacturingOrderReporting.ReportedWhen AS 报工时间, _RejectionCodeItem.Code AS 拒收代码, _RejectionCodeItemDescriptionFallback.Text AS 拒收原因描述, _Part.PartNumber AS 零件编号, _ManufacturingOrderOperation.OperationNumber AS 工序号, _ManufacturingOrderOperationRejectionRow.RejectedQuantity AS 拒收数量, -- 加工成本、外协成本、材料成本(通过5个OUTER APPLY计算) ... * _ManufacturingOrderOperationRejectionRow.RejectedQuantity AS 加工成本, ... * _ManufacturingOrderOperationRejectionRow.RejectedQuantity AS 外协加工成本, ... * _ManufacturingOrderOperationRejectionRow.RejectedQuantity AS 材料成本, ... AS 拒收总成本, _ProductRecord.SerialNo AS 产品序列号FROM monitor.ManufacturingOrderReporting AS _ManufacturingOrderReporting-- 16个表关联(LEFT/INNER JOIN)-- 5个成本子查询(OUTER APPLY)
基于上述数据表,Monitor ERP提供了一个查询SQL,如下所示:
注:子查询5因包含大量嵌套逻辑,此处为简化展示。完整代码请参考附带的 SQL 文件。
Select _ManufacturingOrder.OrderNumber as ManufacturingOrderOrderNumber,_ManufacturingOrderReporting.ReportedWhen as ReportedWhen,_RejectionCodeItem.Code as RejectionCodeItemCode,_RejectionCodeItemDescriptionFallback.Text as RejectionCodeItemDescription,_Part.PartNumber as PartPartNumber,_Part.Description as PartDescription,_ManufacturingOrderOperation.OperationNumber as ManufacturingOrderOperationOperationNumber,_ManufacturingOrderOperationDescriptionFallback.Text as ManufacturingOrderOperationDescription,_ManufacturingOrderOperationRejectionRow.RejectedQuantity as RejectedQuantity,_WorkCenter.Number as WorkCenterNumber,(coalesce(OperationProcessingCostJoin.Cost, 0) + coalesce(MaterialCostManufacturedPartsJoin.ProcessingManufacturedParts, 0)) * _ManufacturingOrderOperationRejectionRow.RejectedQuantity as ManufacturingCost,(coalesce(OperationSubContractCostJoin.Cost, 0) + coalesce(MaterialCostManufacturedPartsJoin.SubcontractManufacturedParts, 0)) * _ManufacturingOrderOperationRejectionRow.RejectedQuantity as SubContractManufacturingCost,(coalesce(MaterialCostPurchasedPartsJoin.Cost, 0) + coalesce(MaterialCostManufacturedPartsJoin.DirectMaterialManufacturedParts, 0) + (CASE WHEN _RejectionCodeItem.AutomaticWithdrawalOfMaterial = 1 THEN coalesce(DirectMaterialCostPurchasedPartsJoin.Cost, 0) ELSE 0 END)) * _ManufacturingOrderOperationRejectionRow.RejectedQuantity as MaterialCost,coalesce((coalesce(MaterialCostPurchasedPartsJoin.Cost, 0) + coalesce(MaterialCostManufacturedPartsJoin.DirectMaterialManufacturedParts, 0) + (CASE WHEN _RejectionCodeItem.AutomaticWithdrawalOfMaterial = 1 THEN coalesce(DirectMaterialCostPurchasedPartsJoin.Cost, 0) ELSE 0 END)) * _ManufacturingOrderOperationRejectionRow.RejectedQuantity, 0) + coalesce((coalesce(OperationSubContractCostJoin.Cost, 0) + coalesce(MaterialCostManufacturedPartsJoin.SubcontractManufacturedParts, 0)) * _ManufacturingOrderOperationRejectionRow.RejectedQuantity, 0) + coalesce((coalesce(OperationProcessingCostJoin.Cost, 0) + coalesce(MaterialCostManufacturedPartsJoin.ProcessingManufacturedParts, 0)) * _ManufacturingOrderOperationRejectionRow.RejectedQuantity, 0) as TotalCost,case when _ManufacturingOrderOperationRejectionRowRejectionComment.Text > '' then _ManufacturingOrderOperationRejectionRow.RejectionCommentId else null end as ManufacturingOrderOperationRejectionRowRejectionComment,_ProductRecord.SerialNo as ProductRecordSerialNo,_PartStandardUnitCodeFallback.Text as PartStandardUnitCodefrom monitor.ManufacturingOrderReporting as _ManufacturingOrderReportingleft outer join monitor.ManufacturingOrder as _ManufacturingOrder on _ManufacturingOrderReporting.OrderId = _ManufacturingOrder.Idleft outer join monitor.ManufacturingOrderOperationReporting as _ManufacturingOrderOperationReporting on _ManufacturingOrderReporting.Id = _ManufacturingOrderOperationReporting.ReportingIdleft outer join monitor.ManufacturingOrderOperation as _ManufacturingOrderOperation on _ManufacturingOrderOperationReporting.OperationId = _ManufacturingOrderOperation.Idleft outer join monitor.DynamicPhrase as _ManufacturingOrderOperationDescriptionFallback on _ManufacturingOrderOperation.DescriptionId = _ManufacturingOrderOperationDescriptionFallback.Idleft outer join monitor.WorkCenter as _WorkCenter on _ManufacturingOrderOperationReporting.WorkCenterId = _WorkCenter.Idleft outer join monitor.ManufacturingOrderNode as _ManufacturingOrderNode on _ManufacturingOrderOperation.ManufacturingOrderNodeId = _ManufacturingOrderNode.Idleft outer join monitor.Warehouse as _ManufacturingOrderRejectionWarehouse on _ManufacturingOrderOperationReporting.WarehouseId = _ManufacturingOrderRejectionWarehouse.Idleft outer join monitor.Part as _Part on _ManufacturingOrderNode.PartId = _Part.Idinner join monitor.ManufacturingOrderOperationRejectionRow as _ManufacturingOrderOperationRejectionRow on _ManufacturingOrderOperationReporting.Id = _ManufacturingOrderOperationRejectionRow.ManufacturingOrderOperationReportingIdleft outer join monitor.CommentText as _ManufacturingOrderOperationRejectionRowRejectionComment on _ManufacturingOrderOperationRejectionRow.RejectionCommentId = _ManufacturingOrderOperationRejectionRowRejectionComment.Idleft outer join monitor.RejectionCodeItem as _RejectionCodeItem on _ManufacturingOrderOperationRejectionRow.RejectionCodeId = _RejectionCodeItem.Idleft outer join monitor.DynamicPhrase as _RejectionCodeItemDescriptionFallback on _RejectionCodeItem.DescriptionId = _RejectionCodeItemDescriptionFallback.Idleft outer join monitor.PartUnitUsage as _PartUnitUsage on _Part.StandardPartUnitUsageId = _PartUnitUsage.Idleft outer join monitor.Unit as _PartStandardUnit on _PartUnitUsage.UnitId = _PartStandardUnit.Idleft outer join monitor.DynamicPhrase as _PartStandardUnitCodeFallback on _PartStandardUnit.CodeId = _PartStandardUnitCodeFallback.Idleft outer join monitor.ProductRecord as _ProductRecord on _ManufacturingOrderOperationRejectionRow.ProductRecordId = _ProductRecord.IdOUTER APPLY (select sum(case when coalesce(m.PartType, 0) = 0 then (m.ExtraQuantityProcent / 100) * m.PlannedPrice + m.PlannedPrice * (1 + som.Markup / 100) else 0end* m.QuantityPerUnit) as Costfrom monitor.ManufacturingOrderOperation as opinner join monitor.ManufacturingOrderMaterial m on m.ManufacturingOrderNodeId = op.ManufacturingOrderNodeId and m.ToOperationNo = op.OperationNumberinner join monitor.Part as p on p.Id = m.PartIdleft outer join monitor.StorageOverheadMarkup as som on som.Id = p.StorageOverheadMarkupId and (som.Type = 0 or som.Type = 2)where op.OperationNumber = _ManufacturingOrderOperation.OperationNumber and op.ManufacturingOrderNodeId = _ManufacturingOrderNode.Id) as DirectMaterialCostPurchasedPartsJoin OUTER APPLY (select sum(case when coalesce(m.PartType, 0) = 0 then (m.ExtraQuantityProcent / 100) * m.PlannedPrice + m.PlannedPrice * (1 + som.Markup / 100) else 0end* m.QuantityPerUnit) as Costfrom monitor.ManufacturingOrderOperation as opinner join monitor.ManufacturingOrderMaterial m on m.ManufacturingOrderNodeId = op.ManufacturingOrderNodeId and m.ToOperationNo = op.OperationNumberinner join monitor.Part as p on p.Id = m.PartIdleft outer join monitor.StorageOverheadMarkup as som on som.Id = p.StorageOverheadMarkupId and (som.Type = 0 or som.Type = 2)where op.OperationNumber < _ManufacturingOrderOperation.OperationNumber and op.ManufacturingOrderNodeId = _ManufacturingOrderNode.Id) as MaterialCostPurchasedPartsJoin OUTER APPLY (select sum(coalesce((0 + (op.PlannedSetupCostFactor1 * 1) + (op.PlannedSetupCostFactor2 * 1) + (op.PlannedSetupCostFactor3 * 1)) * case when op.PlannedQuantity = 0 then 0 else ((op.PlannedSetupTime/ op.PlannedQuantity) / 36000000000) end, 0) + (0 + (op.PlannedUnitCostFactor1 * 1) + (op.PlannedUnitCostFactor2 * 1) + (op.PlannedUnitCostFactor3 * 1)) * case when op.PlannedQuantity = 0 then 0 else ((op.PlannedUnitTime/ op.PlannedQuantity) / 36000000000) end) as Costfrom monitor.ManufacturingOrderOperation as opinner join monitor.WorkCenter as wc on wc.Id = op.WorkCenterIdwhere op.OperationNumber <= _ManufacturingOrderOperation.OperationNumber and op.ManufacturingOrderNodeId = _ManufacturingOrderNode.Id) as OperationProcessingCostJoin OUTER APPLY (select sum(((case when op.PlannedQuantity = 0 then 0 else (op.PlannedSetupCost / op.PlannedQuantity) + (op.PlannedUnitCost / op.PlannedQuantity) end)) * (1 + som.Markup / 100)) as Costfrom monitor.ManufacturingOrderOperation as opleft outer join monitor.StorageOverheadMarkup as som on som.Id = _Part.StorageOverheadMarkupId and (som.Type = 1 or som.Type = 2)where op.OperationNumber <= _ManufacturingOrderOperation.OperationNumber and op.ManufacturingOrderNodeId = _ManufacturingOrderNode.Id and op.IsSubcontractor = 1) as OperationSubContractCostJoin OUTER APPLY (select sum(case when coalesce(m.PartType, 0) in (0,2) or coalesce(calculationBeforeRegistrationDate.materialCostWithMarkup, calculationAfterRegistrationDate.materialCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.subContractCostWithMarkup, calculationAfterRegistrationDate.subContractCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.manufacturingCostAtCalculation, calculationAfterRegistrationDate.manufacturingCostAtCalculation, 0) = 0then 0else m.PlannedPrice * coalesce(calculationBeforeRegistrationDate.materialCost, calculationAfterRegistrationDate.materialCost, 0) / (coalesce(calculationBeforeRegistrationDate.materialCostWithMarkup, calculationAfterRegistrationDate.materialCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.subContractCostWithMarkup, calculationAfterRegistrationDate.subContractCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.manufacturingCostAtCalculation, calculationAfterRegistrationDate.manufacturingCostAtCalculation, 0))end * m.QuantityPerUnit) as DirectMaterialManufacturedParts,sum(case when coalesce(m.PartType, 0) in (0,2) or coalesce(calculationBeforeRegistrationDate.materialCostWithMarkup, calculationAfterRegistrationDate.materialCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.subContractCostWithMarkup, calculationAfterRegistrationDate.subContractCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.manufacturingCostAtCalculation, calculationAfterRegistrationDate.manufacturingCostAtCalculation, 0) = 0then 0else m.PlannedPrice * coalesce(calculationBeforeRegistrationDate.subContractCost, calculationAfterRegistrationDate.subContractCost, 0) / (coalesce(calculationBeforeRegistrationDate.materialCostWithMarkup, calculationAfterRegistrationDate.materialCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.subContractCostWithMarkup, calculationAfterRegistrationDate.subContractCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.manufacturingCostAtCalculation, calculationAfterRegistrationDate.manufacturingCostAtCalculation, 0))end* m.QuantityPerUnit) as SubcontractManufacturedParts,sum(case when coalesce(m.PartType, 0) in (0,2) or coalesce(calculationBeforeRegistrationDate.materialCostWithMarkup, calculationAfterRegistrationDate.materialCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.subContractCostWithMarkup, calculationAfterRegistrationDate.subContractCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.manufacturingCostAtCalculation, calculationAfterRegistrationDate.manufacturingCostAtCalculation, 0) = 0then 0else m.PlannedPrice * coalesce(calculationBeforeRegistrationDate.manufacturingCost, calculationAfterRegistrationDate.manufacturingCost, 0) / (coalesce(calculationBeforeRegistrationDate.materialCostWithMarkup, calculationAfterRegistrationDate.materialCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.subContractCostWithMarkup, calculationAfterRegistrationDate.subContractCostWithMarkup, 0) + coalesce(calculationBeforeRegistrationDate.manufacturingCostAtCalculation, calculationAfterRegistrationDate.manufacturingCostAtCalculation, 0))end* m.QuantityPerUnit) as ProcessingManufacturedPartsfrom monitor.ManufacturingOrderOperation as opinner join monitor.ManufacturingOrderMaterial m on m.ManufacturingOrderNodeId = op.ManufacturingOrderNodeId and m.ToOperationNo = op.OperationNumberinner join monitor.Part as p on p.Id = m.PartIdleft outer join monitor.PartPlanningInformation ppi on p.Id = ppi.PartId and ppi.WarehouseId = _ManufacturingOrderRejectionWarehouse.Idleft outer join monitor.StorageOverheadMarkup as som on som.Id = p.StorageOverheadMarkupId and(som.Type = 0 or som.Type = 2)OUTER APPLY (select TOP 1 pc.MaterialCost + pc.MaterialCostFromStockParts + pc.StorageOverheadMaterial + (m.ExtraQuantityProcent / 100) * (pc.MaterialCost + pc.MaterialCostFromStockParts + pc.StorageOverheadMaterial) as materialCost,pc.MaterialCost + pc.MaterialCostFromStockParts + pc.StorageOverheadMaterial + (m.ExtraQuantityProcent / 100) * (pc.MaterialCost + pc.MaterialCostFromStockParts + pc.StorageOverheadMaterial) as materialCostWithMarkup,((pc.SubcontractSetupCost + pc.SubcontractUnitCost + pc.SubcontractUnitCostFromStockParts) + (pc.SubcontractSetupStorageOverhead + pc.SubcontractUnitStorageOverhead + pc.SubcontractUnitStorageOverheadFromStockParts)) as subContractCost,((pc.SubcontractSetupCost + pc.SubcontractUnitCost + pc.SubcontractUnitCostFromStockParts) + (pc.SubcontractSetupStorageOverhead + pc.SubcontractUnitStorageOverhead + pc.SubcontractUnitStorageOverheadFromStockParts)) as subContractCostWithMarkup,(0 + pc.SetupCostColum1 + pc.SetupCostColum2 + pc.SetupCostColum3 + 0 + pc.UnitCostColum1 + pc.UnitCostColum2 + pc.UnitCostColum3 + 0 + pc.SetupCostFromStockPartsCostColumn1 + pc.SetupCostFromStockPartsCostColumn2 + pc.SetupCostFromStockPartsCostColumn3 + 0 + pc.UnitCostFromStockPartsCostColumn1 + pc.UnitCostFromStockPartsCostColumn2 + pc.UnitCostFromStockPartsCostColumn3) as manufacturingCost,(0 + pc.SetupCostColum1 + pc.SetupCostColum2 + pc.SetupCostColum3 + 0 + pc.UnitCostColum1 + pc.UnitCostColum2 + pc.UnitCostColum3 + 0 + pc.SetupCostFromStockPartsCostColumn1 + pc.SetupCostFromStockPartsCostColumn2 + pc.SetupCostFromStockPartsCostColumn3 + 0 + pc.UnitCostFromStockPartsCostColumn1 + pc.UnitCostFromStockPartsCostColumn2 + pc.UnitCostFromStockPartsCostColumn3) as manufacturingCostAtCalculationfrom monitor.PartCalculation pc inner join monitor.PreCalculationParameters cp on cp.Id = pc.ParametersId where pc.PartId = p.Id and cp.CalculationDate <= _ManufacturingOrder.RegistrationDateorder by cp.CalculationDate desc) as calculationBeforeRegistrationDate OUTER APPLY (select TOP 1 pc.MaterialCost + pc.MaterialCostFromStockParts + pc.StorageOverheadMaterial + (m.ExtraQuantityProcent / 100) * (pc.MaterialCost + pc.MaterialCostFromStockParts + pc.StorageOverheadMaterial) as materialCost,pc.MaterialCost + pc.MaterialCostFromStockParts + pc.StorageOverheadMaterial + (m.ExtraQuantityProcent / 100) * (pc.MaterialCost + pc.MaterialCostFromStockParts + pc.StorageOverheadMaterial) as materialCostWithMarkup,((pc.SubcontractSetupCost + pc.SubcontractUnitCost + pc.SubcontractUnitCostFromStockParts) + (pc.SubcontractSetupStorageOverhead + pc.SubcontractUnitStorageOverhead + pc.SubcontractUnitStorageOverheadFromStockParts)) as subContractCost,((pc.SubcontractSetupCost + pc.SubcontractUnitCost + pc.SubcontractUnitCostFromStockParts) + (pc.SubcontractSetupStorageOverhead + pc.SubcontractUnitStorageOverhead + pc.SubcontractUnitStorageOverheadFromStockParts)) as subContractCostWithMarkup,(0 + pc.SetupCostColum1 + pc.SetupCostColum2 + pc.SetupCostColum3 + 0 + pc.UnitCostColum1 + pc.UnitCostColum2 + pc.UnitCostColum3 + 0 + pc.SetupCostFromStockPartsCostColumn1 + pc.SetupCostFromStockPartsCostColumn2 + pc.SetupCostFromStockPartsCostColumn3 + 0 + pc.UnitCostFromStockPartsCostColumn1 + pc.UnitCostFromStockPartsCostColumn2 + pc.UnitCostFromStockPartsCostColumn3) as manufacturingCost,(0 + pc.SetupCostColum1 + pc.SetupCostColum2 + pc.SetupCostColum3 + 0 + pc.UnitCostColum1 + pc.UnitCostColum2 + pc.UnitCostColum3 + 0 + pc.SetupCostFromStockPartsCostColumn1 + pc.SetupCostFromStockPartsCostColumn2 + pc.SetupCostFromStockPartsCostColumn3 + 0 + pc.UnitCostFromStockPartsCostColumn1 + pc.UnitCostFromStockPartsCostColumn2 + pc.UnitCostFromStockPartsCostColumn3) as manufacturingCostAtCalculationfrom monitor.PartCalculation pc inner join monitor.PreCalculationParameters cp on cp.Id = pc.ParametersId where pc.PartId = p.Id and cp.CalculationDate > _ManufacturingOrder.RegistrationDateorder by cp.CalculationDate asc) as calculationAfterRegistrationDate where op.OperationNumber <= _ManufacturingOrderOperation.OperationNumber and op.ManufacturingOrderNodeId = _ManufacturingOrderNode.Id) as MaterialCostManufacturedPartsJoin
2.6、在 Tableau 中的应用建议
这份查询直接连接 Monitor ERP 的 SQL Server 数据库后,可以作为 Tableau 的数据源。以下是一些实用的分析维度:
- 按工作中心分析:哪个工作中心的拒收率最高?加工成本损失最大?
- 按零件分析:哪些零件是”拒收大户”?成本集中在哪里?
- 按工序分析:早期工序的拒收(沉没成本较低)vs 晚期工序的拒收(沉没成本高)
- 按拒收代码分析:最常见的拒收原因是什么?是否集中在特定缺陷类型?
- 时间趋势分析:通过报工时间维度,观察拒收率的变化趋势
- 成本归因分析:加工成本 vs 材料成本 vs 外协成本的占比分布
三维成本拆分是这套查询最大的亮点——它让你不仅能看到”拒了多少件”,还能量化”每件拒收损失了多少钱”,并且知道这笔损失具体来自材料、加工还是外协。
基于这样的查询,喜乐君使用 #Tableau 快速完成了客户分析。

接下来,我尝试分解一下这个sql逻辑。
三、5 个成本子查询:拒收成本的精妙计算
拒收列表查询的核心价值在于 成本核算。
Monitor ERP 使用 5 个 OUTER APPLY 子查询(SQL Server 的横向连接),将拒收数量拆分为三个成本维度:
3.1 成本维度总览
| 成本维度 | 来源子查询 | 计算逻辑 |
|---|---|---|
| 加工成本 | 子查询3 + 子查询5(加工部分) | 本工序及之前工序的加工工时成本 + 自制件的加工材料成本 |
| 外协加工成本 | 子查询4 + 子查询5(外协部分) | 标记为外协的工序成本 + 自制件的外协材料成本 |
| 材料成本 | 子查询1 + 子查询2 + 子查询5(材料部分) | 累计采购件材料 + 自制件直接材料 + 条件性自动领料 |
在Monitor ER P 背后的数据库中,我们可以看到有多个成本字段,它们没有明确的名称,取决于系统的设置:

基于此,就可以用一个查询完成:
-- ─────────────────────────────────────────────-- 成本子查询3:累计加工成本-- 逻辑:取本工序及之前工序(<=)的加工成本-- 加工成本 = 准备成本(分摊到单位)+ 单件加工成本-- 准备成本 = (准备成本因子1+2+3) × (准备时间/计划数量)/36000000000-- 单件成本 = (单件成本因子1+2+3) × (单件时间/计划数量)/36000000000-- 注意:时间单位为100纳秒(ticks),36000000000 ticks = 1小时-- ─────────────────────────────────────────────OUTER APPLY ( SELECT SUM( COALESCE( (0 + (op.PlannedSetupCostFactor1 * 1) + (op.PlannedSetupCostFactor2 * 1) + (op.PlannedSetupCostFactor3 * 1)) * CASE WHEN op.PlannedQuantity = 0 THEN 0 ELSE ((op.PlannedSetupTime / op.PlannedQuantity) / 36000000000) END, 0) + (0 + (op.PlannedUnitCostFactor1 * 1) + (op.PlannedUnitCostFactor2 * 1) + (op.PlannedUnitCostFactor3 * 1)) * CASE WHEN op.PlannedQuantity = 0 THEN 0 ELSE ((op.PlannedUnitTime / op.PlannedQuantity) / 36000000000) END ) AS Cost FROM monitor.ManufacturingOrderOperation AS op INNER JOIN monitor.WorkCenter AS wc ON wc.Id = op.WorkCenterId WHERE op.OperationNumber <= _ManufacturingOrderOperation.OperationNumber AND op.ManufacturingOrderNodeId = _ManufacturingOrderNode.Id) AS OperationProcessingCostJoin
由于极端情形下,op.PlannedQuantity可能是0,所以上面的查询增加了 op.PlannedQuantity=0的特殊逻辑处理。 如下是一家公司多年生产数据中仅存的特殊值(几行即可让sql逻辑陷入错误)

3.2 五个子查询的分工
子查询1 — 本工序直接材料成本(采购件)
取当前工序号(=)的采购件物料。计算公式:
(额外损耗% × 计划价格 + 计划价格 × (1 + 仓储加价/100))× 单位用量
关键判断:PartType = 0 表示采购件。通过 StorageOverheadMarkup(Type=0 或 2)获取仓储加价比例。
子查询2 — 累计材料成本(采购件,不含本工序)
与子查询1逻辑相同,但取工序号 < 之前的所有工序。这代表了本工序拒收时,已经投入的材料成本——这些沉没成本需要计入拒收损失。
子查询3 — 累计加工成本
取本工序及之前工序(<=)的加工工时成本。这里需要特别注意 Monitor 的时间单位:
加工成本 = 准备成本 + 单件加工成本
其中准备成本和单件成本都通过三列成本因子(Factor1/2/3)计算,时间需要除以 36,000,000,000 转换为小时——因为 Monitor 使用 100 纳秒(ticks)作为时间单位。
子查询4 — 累计外协加工成本
只筛选 IsSubcontractor = 1 的外协工序,计算外协成本并加上仓储加价(Type=1 或 2)。
子查询5 — 自制件材料成本(最复杂的一个)
这个子查询处理自制件(PartType = 1)的场景,通过 PartCalculation 表中的成本比例进行分摊。它还包含两个嵌套的子查询:
- 嵌套子查询A:取订单注册日期之前的最近一次成本计算(
ORDER BY CalculationDate DESC TOP 1) - 嵌套子查询B:取订单注册日期之后的最早一次成本计算(
ORDER BY CalculationDate ASC TOP 1)
优先使用注册日期之前的计算,若无则用注册日期之后的。这种”时间锚定”机制确保了成本数据的时间一致性。
3.3 一个容易被忽略的细节:自动退料
材料成本的计算中有一个条件分支:只有当拒收代码设置了 AutomaticWithdrawalOfMaterial = 1 时,才会将子查询1的本工序直接材料成本计入总材料成本。
逻辑是:如果拒收代码标记了”自动退料”,说明材料已经被领出并投入生产,拒收意味着这部分材料报废,需要计入损失。如果未标记自动退料,则材料可以退回仓库,不构成沉没成本。
四、Monitor ERP 的几个技术细节
4.1 时间单位:100 纳秒(Ticks)
Monitor 内部所有时间字段都使用 100 纳秒为单位,这在 SQL 查询中需要特别注意:
| 时间单位 | 换算 |
|---|---|
| 1 小时 | 36,000,000,000 ticks |
| 1 分钟 | 600,000,000 ticks |
| 1 秒 | 10,000,000 ticks |
在成本计算中,PlannedSetupTime / 36000000000 就是将准备时间转换为小时数。
4.2 多语言引擎:DynamicPhrase
Monitor 使用 DynamicPhrase 表作为统一的多语言文本存储。几乎所有需要显示为人类可读文本的字段(工序描述、拒收原因、单位代码等),都不直接存储文本,而是存储一个 DescriptionId,通过关联 DynamicPhrase 获取对应语言的描述。
这意味着一条拒收记录可能关联 4-5 个 DynamicPhrase 实例,分别提供工序描述、拒收原因描述、单位代码等不同维度的多语言文本。
4.3 扩展字段(ExtraField)机制
Monitor 支持用户自定义扩展字段,字段名格式为 ExtraField_{实体类型}_{字段ID}_switch,值为 SMALLINT(0 或 6),对应开关类型。零件和工作中心各有独立的扩展字段集。
五、总结
Monitor ERP 的拒收数据架构体现了制造执行系统在质量管理上的深度思考:
- 数据完整性:通过 18 张表的关联,确保每条拒收记录都有完整的业务上下文
- 成本精确性:5 个嵌套子查询实现三维成本拆分,区分采购件/自制件/外协件的不同计算路径
- 时间一致性:通过注册日期锚定机制,确保成本数据与订单创建时点的匹配
- 可追溯性:ProductRecord 序列号支持从拒收记录追溯到具体产品
- 多语言支持:DynamicPhrase 引擎确保同一数据在不同语言环境下都能正确展示
希望本文能帮助使用 Monitor ERP 的数据分析师和 BI 开发者更好地理解拒收数据的底层结构。
如果你也在使用 Monitor ERP 进行数据分析,欢迎交流!
本文基于 Monitor ERP 官方 API 文档(v2.22 – v24.5)整理,所有表结构和字段含义均来自官方文档。