轻制造 SaaS 的生产闭环建模:BOM、工单、领料、报工、质检与入库
本文是轻制造 MRP Lite 系列的第二篇,重点讨论领域建模。文中模型已做抽象处理,不对应任何具体公司或项目实现。
摘要
生产模块最容易被做成一堆名词的集合:BOM、计划单、工单、领料单、报工单、质检单、入库单、返工单、报废单。
如果只按名词建表,很容易得到一个"看起来很 ERP,实际上跑不通"的系统。
更稳的方式是先明确三句话:
text
BOM 是公式。
工单是闭环。
库存流水是事实账。
计划、领料、报工、质检、入库、返工、报废都要围绕这三句话展开。
本文讨论一个轻制造 SaaS 生产闭环的领域模型:如何从 BOM 推导需求,如何用工单承接生产责任,为什么报工和领料不强行多对多绑定,为什么前端可以融合,后端单据必须独立。
1. 先给核心对象定性
生产系统里的对象很多,但第一版必须先分清主次。
生产计划:需求来源和决策层
生产计划回答:
text
为什么要生产?
准备生产什么?
准备生产多少?
当前缺哪些物料?
是否要合并多个来源需求?
计划不是生产现场的执行账,它更像生产意图和需求归集。
所以计划可以存在,也可以没有。小工厂里老板经常直接安排生产,不一定先走正式计划。
生产工单:生产闭环主单据
生产工单回答:
text
这次到底由谁负责生产?
生产哪个 SKU?
目标数量是多少?
使用哪份 BOM 快照?
领了多少料?
交了多少货?
合格多少?
报废多少?
最终能不能关闭?
工单必须存在。因为领料、退料、报工、质检、入库、返工、报废都要围绕工单归集。
一个务实原则是:
text
计划可选,工单必有。
BOM:理论用料公式
BOM 回答:
text
生产一个成品,理论上需要哪些物料?
每种物料需要多少?
是否有损耗率?
单位如何换算?
如果有半成品,是否继续展开?
BOM 不是实际领料单。实际领料可以偏离 BOM,但必须留痕。
库存流水:库存事实账
库存流水回答:
text
哪张来源单据导致库存变化?
哪个商品、哪个仓库、哪个批次变化了?
数量增加还是减少?
变化前后如何追溯?
库存模块不应该理解复杂生产语义。生产语义由生产单据解释,库存流水只证明库存事实怎么变。
2. 端到端单据闭环
一个轻制造 MVP 的主链路可以这样建模:
#mermaid-svg-sDelkzKMHBw53eCt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sDelkzKMHBw53eCt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sDelkzKMHBw53eCt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sDelkzKMHBw53eCt .error-icon{fill:#552222;}#mermaid-svg-sDelkzKMHBw53eCt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sDelkzKMHBw53eCt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sDelkzKMHBw53eCt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sDelkzKMHBw53eCt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sDelkzKMHBw53eCt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sDelkzKMHBw53eCt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sDelkzKMHBw53eCt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sDelkzKMHBw53eCt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sDelkzKMHBw53eCt .marker.cross{stroke:#333333;}#mermaid-svg-sDelkzKMHBw53eCt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sDelkzKMHBw53eCt p{margin:0;}#mermaid-svg-sDelkzKMHBw53eCt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sDelkzKMHBw53eCt .cluster-label text{fill:#333;}#mermaid-svg-sDelkzKMHBw53eCt .cluster-label span{color:#333;}#mermaid-svg-sDelkzKMHBw53eCt .cluster-label span p{background-color:transparent;}#mermaid-svg-sDelkzKMHBw53eCt .label text,#mermaid-svg-sDelkzKMHBw53eCt span{fill:#333;color:#333;}#mermaid-svg-sDelkzKMHBw53eCt .node rect,#mermaid-svg-sDelkzKMHBw53eCt .node circle,#mermaid-svg-sDelkzKMHBw53eCt .node ellipse,#mermaid-svg-sDelkzKMHBw53eCt .node polygon,#mermaid-svg-sDelkzKMHBw53eCt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sDelkzKMHBw53eCt .rough-node .label text,#mermaid-svg-sDelkzKMHBw53eCt .node .label text,#mermaid-svg-sDelkzKMHBw53eCt .image-shape .label,#mermaid-svg-sDelkzKMHBw53eCt .icon-shape .label{text-anchor:middle;}#mermaid-svg-sDelkzKMHBw53eCt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sDelkzKMHBw53eCt .rough-node .label,#mermaid-svg-sDelkzKMHBw53eCt .node .label,#mermaid-svg-sDelkzKMHBw53eCt .image-shape .label,#mermaid-svg-sDelkzKMHBw53eCt .icon-shape .label{text-align:center;}#mermaid-svg-sDelkzKMHBw53eCt .node.clickable{cursor:pointer;}#mermaid-svg-sDelkzKMHBw53eCt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sDelkzKMHBw53eCt .arrowheadPath{fill:#333333;}#mermaid-svg-sDelkzKMHBw53eCt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sDelkzKMHBw53eCt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sDelkzKMHBw53eCt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sDelkzKMHBw53eCt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sDelkzKMHBw53eCt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sDelkzKMHBw53eCt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sDelkzKMHBw53eCt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sDelkzKMHBw53eCt .cluster text{fill:#333;}#mermaid-svg-sDelkzKMHBw53eCt .cluster span{color:#333;}#mermaid-svg-sDelkzKMHBw53eCt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sDelkzKMHBw53eCt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sDelkzKMHBw53eCt rect.text{fill:none;stroke-width:0;}#mermaid-svg-sDelkzKMHBw53eCt .icon-shape,#mermaid-svg-sDelkzKMHBw53eCt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sDelkzKMHBw53eCt .icon-shape p,#mermaid-svg-sDelkzKMHBw53eCt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sDelkzKMHBw53eCt .icon-shape .label rect,#mermaid-svg-sDelkzKMHBw53eCt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sDelkzKMHBw53eCt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sDelkzKMHBw53eCt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sDelkzKMHBw53eCt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 也可直接创建
生产需求
库存预警 / 销售缺货 / 手工
生产计划
可选,做需求归集
计划明细
一个计划可包含多个成品
生产工单
生产闭环主单据
领退料单
分批领料 / 退料
报工单
本次交付多少
质检验收单
合格 / 返工 / 报废 / 待处理
生产入库单
合格品正式入库
返工记录
重新处理后再次报工
报废 / 损耗记录
在制台账
投入物料追溯
库存流水
这条链路里有几个关键边界。
计划和工单不是一回事
计划偏"我要不要做、做多少、缺什么料"。
工单偏"这次生产责任闭环怎么跑完"。
计划可以一对多生成工单。比如一个计划里有多个成品,或者一个成品拆成多批生产。
但工单也可以手工直接创建。直接创建工单时,不应该偷偷在后台生成一个用户不可见的计划单,否则会制造概念上的脏数据。
更合理的方式是:工单带来源类型,比如手工、销售缺货、库存预警、计划下达。
领料和报工都挂工单,不强行互绑
一个常见疑问是:报工单是否要关联具体哪几张领料单?
在轻制造场景里,不建议第一版强绑。
原因是生产现场经常分批领料、分批报工,领料和产出并不总是严格一一对应。如果强行要求"这次报工消耗哪几张领料单",用户操作会变复杂,数据也未必更真实。
更主流、更稳的归集口径是:
text
领料挂工单。
报工挂工单。
工单关闭时统一计算理论用料、实际净领料、合格入库、报废和差异。
这能保持操作简单,同时保留差异分析能力。
报工和质检可以前端一页,后端两张单
对客户来说,"今天交了 100 个,其中 95 个合格、3 个返工、2 个报废"可能就是一个页面动作。
但后端语义上,报工和质检不是同一件事。
报工记录的是:
text
生产现场交付了多少产出。
质检记录的是:
text
这些产出如何被判定和处置。
所以可以设计成:
text
前端:一个报工质检确认页面。
后端:报工单 1:1 质检验收单。
这样后续要拆独立质检页、质检标准库、返工流程、质检附件,都有稳定基础。
生产入库单不能被质检单替代
质检合格不等于库存已经入库。
第一版为了简化交互,可以在质检确认后后台自动生成生产入库单并立即过账。前端只显示"已入库"。
但后端仍要有正式入库单。
原因是后续很容易出现这些需求:
- 质检合格,但仓库稍后确认入库。
- 合格数量分批入库。
- 入库单打印。
- 入库冲销。
- 仓库员独立操作生产入库。
如果第一版把入库动作完全塞进质检单,后面这些能力都会反向冲击质检模型。
因此更稳的规则是:
text
前端可以融合。
后端单据必须独立。
融合接口只是 Facade,不是单据模型合并。
3. BOM 建模:版本、快照和多层展开
BOM 是生产系统里最容易低估的部分。
如果只是单层公式:
text
可生产数量 = min(各物料库存 / 单位成品需求量)
那么树形 BOM 的价值会大打折扣。
真正的多层 BOM 要处理半成品库存:
text
成品 A 需要半成品 B。
B 自己也有 BOM。
如果 B 有库存,先用 B 的库存。
如果 B 不够,不足部分再展开 B 的 BOM 或生成 B 的生产需求。
示例:
text
生产成品 A 50 个。
每个 A 需要半成品 B 1 个。
B 当前库存 20 个。
B 毛需求 = 50
B 可用库存 = 20
B 净需求 = 30
系统应该给出:
text
A 工单先领用 B 现货 20 个。
B 缺口 30 个形成半成品生产需求或子工单。
B 子工单完工入库后,A 工单继续领用 B。
这就是多层净需求展开,而不是无脑把整棵 BOM 炸到底层原料。
BOM 版本和工单快照
BOM 会变。
可能今天换了包装材料,明天调整了损耗率,后天增加了替代料。
如果历史工单一直引用当前最新 BOM,那么后续差异分析会失真:
text
当时生产用的是旧配方。
现在系统却拿新配方算理论用量。
最后差异看起来异常,其实是系统自己改了公式。
所以工单创建或下达时必须锁定 BOM 快照:
text
BOM 可以有多个版本。
默认版本用于省操作。
工单一旦创建 / 下达,锁定当时使用的 BOM 版本和组件快照。
BOM 后续修改不能影响历史工单。
计划单到工单的转换点也要注意:
text
计划创建时算过一次缺料。
计划审核或下达时 BOM 可能已经变了。
生成工单前应重新计算,并提示差异,由用户确认。
工单创建后再锁定快照。
这比静默沿用旧快照或静默切换最新 BOM 都更稳。
4. SPU 和 SKU 的双层口径
很多系统里有 SPU 和 SKU 两层商品模型。
生产模块不能只看 SPU,也不能只看 SKU。
更合理的口径是:
text
商品能力、默认 BOM、需求聚合,可以先看 SPU。
实际 BOM 明细、工单快照、库存预占、领料、退料、入库、库存流水,必须落到 SKU。
原因很简单:库存账通常认的是 SKU。
如果生产模块只按 SPU 设计,到了库存扣减、批次追踪、仓库库存、销售出库时一定断层。
但如果所有配置都强制按 SKU 维护,客户操作成本又会很高。
所以推荐:
text
SPU 维护默认规则。
SKU 可以覆盖规则。
解析时按 SKU 专用 > SPU 通用 > 系统默认。
比如一个成品默认使用通用 BOM,但某个颜色或包装规格的 SKU 有专门 BOM,就用 SKU 专用 BOM。
这和很多成熟系统里的 Product Template / Product Variant 思路是一致的。
5. 供给策略:让算法给建议,让人做决策
多层 BOM 还有一个关键概念:BOM 行上的供给策略。
它回答的是:
text
这个物料在当前 BOM 里,缺了以后应该怎么处理?
常见策略可以抽象为:
text
原料:缺了形成采购建议。
库存型半成品:先看库存,不足再生产。
外采件:缺了采购,不生成工单。
虚拟件:不看自身库存,直接展开下级 BOM。
订单绑定供给:为某个父工单专门生产,不共享库存池。
MVP 不需要全部开放。普通客户只需要:
text
原料
库存型半成品
外采件
但架构上要预留。
尤其是 Make-or-Buy 场景:
text
某个半成品既能自己生产,也能外购。
这时候系统不要替老板自动决定。
更好的方式是:
text
系统计算净需求。
缺口进入待处理建议。
用户选择:转生产子工单、转采购、暂不处理。
算法提供建议,人做关键业务决策。
6. 关单时才真正看差异
生产过程允许现实偏差。
比如:
- 超领。
- 少领。
- 超报。
- 少报。
- 超产。
- 报废。
- 返工。
- 退料。
如果系统完全阻断,会不好用;如果完全不管,就退化成 Excel。
更合理的方式是:
text
允许偏差。
超过提醒阈值时标红。
超过确认阈值时要求主管确认。
超过阻断阈值时后端拒绝。
所有偏差必须留痕。
工单关闭时做最终守恒校验:
text
理论用料
实际领料
退料
净耗料
合格入库
返工
报废
在制余额
如果仍有差异,进入关单复核:
text
主管确认差异原因。
生成退料、损耗或待处理记录。
在制台账归零或进入允许关闭状态。
工单关闭。
这一步是老板真正要看的地方:不是每一分钟谁做了什么,而是一张工单最终有没有把料、货、损耗解释清楚。
7. 一句话总结
轻制造生产闭环建模,不是把 ERP 名词都建成表。
更重要的是分清:
text
计划是来源。
BOM 是公式。
工单是责任闭环。
库存流水是事实账。
在制台账解释原料离仓后去哪了。
质检解释产出如何处置。
入库单解释合格品如何进入库存。
前端可以把操作做简单,但后端不能把账做简单。
这就是轻制造 SaaS 第一版能不能"耐造"的关键。
本文为作者原创,首发于掘金,CSDN 为同步发布版本。