跳至正文

Monitor ERP 拒收数据深度解析:18张表、5个成本子查询与完整 Tableau 查询指南

大家好,我是喜乐君。在制造业数据分析领域深耕多年,今天和大家深度拆解 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 个信息区块:

  1. 制造订单与报工基本信息 — 订单号、报工时间
  2. 拒收原因信息 — 拒收代码、拒收原因描述
  3. 零件信息 — 零件编号、零件描述
  4. 工序与工作中心信息 — 工序号、工序描述、工作中心编号
  5. 拒收数量 — 拒收件数
  6. 成本分析 — 加工成本、外协加工成本、材料成本、拒收总成本
  7. 拒收评论 — 条件性输出(仅当有评论内容时显示ID)
  8. 产品追溯信息 — 产品序列号
  9. 零件标准单位 — 单位代码(通过 DynamicPhrase 多语言)
  10. 扩展字段 — 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 PartStandardUnitCode
from
monitor.ManufacturingOrderReporting as _ManufacturingOrderReporting
left outer join monitor.ManufacturingOrder as _ManufacturingOrder on _ManufacturingOrderReporting.OrderId = _ManufacturingOrder.Id
left outer join monitor.ManufacturingOrderOperationReporting as _ManufacturingOrderOperationReporting on _ManufacturingOrderReporting.Id = _ManufacturingOrderOperationReporting.ReportingId
left outer join monitor.ManufacturingOrderOperation as _ManufacturingOrderOperation on _ManufacturingOrderOperationReporting.OperationId = _ManufacturingOrderOperation.Id
left outer join monitor.DynamicPhrase as _ManufacturingOrderOperationDescriptionFallback on _ManufacturingOrderOperation.DescriptionId = _ManufacturingOrderOperationDescriptionFallback.Id
left outer join monitor.WorkCenter as _WorkCenter on _ManufacturingOrderOperationReporting.WorkCenterId = _WorkCenter.Id
left outer join monitor.ManufacturingOrderNode as _ManufacturingOrderNode on _ManufacturingOrderOperation.ManufacturingOrderNodeId = _ManufacturingOrderNode.Id
left outer join monitor.Warehouse as _ManufacturingOrderRejectionWarehouse on _ManufacturingOrderOperationReporting.WarehouseId = _ManufacturingOrderRejectionWarehouse.Id
left outer join monitor.Part as _Part on _ManufacturingOrderNode.PartId = _Part.Id
inner join monitor.ManufacturingOrderOperationRejectionRow as _ManufacturingOrderOperationRejectionRow on _ManufacturingOrderOperationReporting.Id = _ManufacturingOrderOperationRejectionRow.ManufacturingOrderOperationReportingId
left outer join monitor.CommentText as _ManufacturingOrderOperationRejectionRowRejectionComment on _ManufacturingOrderOperationRejectionRow.RejectionCommentId = _ManufacturingOrderOperationRejectionRowRejectionComment.Id
left outer join monitor.RejectionCodeItem as _RejectionCodeItem on _ManufacturingOrderOperationRejectionRow.RejectionCodeId = _RejectionCodeItem.Id
left outer join monitor.DynamicPhrase as _RejectionCodeItemDescriptionFallback on _RejectionCodeItem.DescriptionId = _RejectionCodeItemDescriptionFallback.Id
left outer join monitor.PartUnitUsage as _PartUnitUsage on _Part.StandardPartUnitUsageId = _PartUnitUsage.Id
left outer join monitor.Unit as _PartStandardUnit on _PartUnitUsage.UnitId = _PartStandardUnit.Id
left outer join monitor.DynamicPhrase as _PartStandardUnitCodeFallback on _PartStandardUnit.CodeId = _PartStandardUnitCodeFallback.Id
left outer join monitor.ProductRecord as _ProductRecord on _ManufacturingOrderOperationRejectionRow.ProductRecordId = _ProductRecord.Id
OUTER APPLY
(select sum
(
case when coalesce(m.PartType, 0) = 0
then (m.ExtraQuantityProcent / 100) * m.PlannedPrice + m.PlannedPrice * (1 + som.Markup / 100)
else 0
end
* m.QuantityPerUnit
) as Cost
from monitor.ManufacturingOrderOperation as op
inner join monitor.ManufacturingOrderMaterial m on m.ManufacturingOrderNodeId = op.ManufacturingOrderNodeId and m.ToOperationNo = op.OperationNumber
inner join monitor.Part as p on p.Id = m.PartId
left 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 0
end
* m.QuantityPerUnit
) as Cost
from monitor.ManufacturingOrderOperation as op
inner join monitor.ManufacturingOrderMaterial m on m.ManufacturingOrderNodeId = op.ManufacturingOrderNodeId and m.ToOperationNo = op.OperationNumber
inner join monitor.Part as p on p.Id = m.PartId
left 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 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
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 Cost
from monitor.ManufacturingOrderOperation as op
left 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) = 0
then 0
else 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) = 0
then 0
else 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) = 0
then 0
else 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 ProcessingManufacturedParts
from monitor.ManufacturingOrderOperation as op
inner join monitor.ManufacturingOrderMaterial m on m.ManufacturingOrderNodeId = op.ManufacturingOrderNodeId and m.ToOperationNo = op.OperationNumber
inner join monitor.Part as p on p.Id = m.PartId
left outer join monitor.PartPlanningInformation ppi on p.Id = ppi.PartId and ppi.WarehouseId = _ManufacturingOrderRejectionWarehouse.Id
left 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 manufacturingCostAtCalculation
from monitor.PartCalculation pc
inner join monitor.PreCalculationParameters cp on cp.Id = pc.ParametersId
where pc.PartId = p.Id and cp.CalculationDate <= _ManufacturingOrder.RegistrationDate
order 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 manufacturingCostAtCalculation
from monitor.PartCalculation pc
inner join monitor.PreCalculationParameters cp on cp.Id = pc.ParametersId
where pc.PartId = p.Id and cp.CalculationDate > _ManufacturingOrder.RegistrationDate
order 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)整理,所有表结构和字段含义均来自官方文档。