在很多一线用户眼里,制造单点 Mark as Done 之后跳出来的那些窗口很烦:不是提示多耗了料,就是问要不要 backorder,看起来像系统“没想清楚”。
但从源码看,Odoo 其实是故意把这些问题拆成两层:先校验“你现在消耗的料对不对”,再校验“你这次实际产出的数量够不够”。这不是界面细节,而是制造放行的责任分层。
完工前的总入口:不是 button_mark_done(),而是 pre_button_mark_done()
在 addons/mrp/models/mrp_production.py 里,真正决定是否弹窗的是 pre_button_mark_done()。
这个入口先做三件事:
- 基础 sanity check,例如公司一致性、序列号唯一性。
- 对自动生产或没有 lot 的情况做预处理。
- 然后才开始判断异常:先 consumption issues,再 quantity issues。
这意味着 Odoo 的顺序是固定的:
- 先看组件消耗是不是和 BoM / 当前产量匹配;
- 再看本次产量是否会产生 backorder;
- 两个问题都没有,才真正放行完成。
这套顺序很重要,因为“料不对”和“量不够”不是同一种业务问题。前者更像执行偏差,后者更像计划拆分。
_get_consumption_issues() 真正在比较什么
很多人以为 strict / warning consumption 只是“超领料就报错”。源码并不是这么粗糙。
_get_consumption_issues() 的核心逻辑是:
- 先根据当前 MO、当前
qty_producing和 BoM,用_get_moves_raw_values()重新推一遍理论应耗; - 再遍历
move_raw_ids,读取实际 picked 数量,汇总成实际已耗; - 如果有额外物料、数量不一致、或某个产品理论/实际不匹配,就把
(order, product, consumed_qty, expected_qty)塞进 issues 列表。
也就是说,这里不是拿 move 上静态字段机械比较,而是在 “当前这次准备完工的产量” 上重新计算基准。
这就解释了两个常见误区:
误区一:改了 qty_producing,为什么 warning 结果也会变
因为 expected quantity 不是死的,它会按 qty_producing / product_qty 比例换算。你这次只报工一半,理论消耗自然也跟着缩。
误区二:为什么多出一条额外领料也会被抓出来
源码里专门处理了“BoM 里没有,但 move 上 picked 了非零数量”的场景。只要额外物料真的被拣过,系统就会把它当成 issue,而不是默默吞掉。
这反映出 Odoo 的态度:允许现场偏差被记录,但不允许偏差无声发生。
consumption warning wizard 不是报错框,而是“二次决策层”
当 _get_consumption_issues() 返回内容后,_action_generate_consumption_wizard() 会把问题转成 transient lines,打开 mrp.consumption.warning。
这个 wizard 里有两个特别关键的动作。
action_confirm():承认偏差,但继续放行
这个动作会带着 skip_consumption=True 回到 button_mark_done()。意思很直接:
- 这次偏差我已经看过;
- 不再重复校验 consumption;
- 继续后续流程。
所以 warning 模式并不是“自动放过”,而是要求有人显式确认偏差。
action_set_qty():把 move 修回理论值再放行
这个动作更有意思。它会遍历 warning lines:
- 找到对应原料 move;
- 把 move 数量改到 expected qty;
- 把
picked=True补上; - 如果原 move 已不存在,还会新建额外 move;
- 但若涉及 tracked product 且缺 lot/serial,则直接拒绝自动修正。
这里能看出 Odoo 很克制:
- 对普通物料,系统可以帮你把数字纠正到理论值;
- 对受 lot / serial 管控的物料,不敢替你“瞎补”,必须由现场明确指定批次。
这就是制造系统里典型的自动化边界:数量能代填,追溯不能代签。
为什么 consumption 处理完之后,才轮到 backorder wizard
pre_button_mark_done() 在 consumption issue 解决后,才会去看 _get_quantity_produced_issues()。
后者只做一件事:检查 _get_quantity_to_backorder() 是否为零。如果不为零,说明这张 MO 本次没有全部做完,系统就要决定是否拆补单。
这一步又会根据 operation type 的 create_backorder 策略分流:
always:自动 backorder;ask:弹 wizard 让用户决定;never:直接完工,不再拆补单。
这和 consumption 的治理逻辑完全不同。
- consumption 处理的是执行内容是否偏离;
- backorder 处理的是执行数量是否拆批。
两者如果混成一个弹窗,现场很难理解到底是在承认超耗,还是在决定剩余产量的命运。源码把它们拆开,是对现场认知负担的优化。
这套放行顺序对实施和开发有什么启发
1. 不要把 warning 当 bug
很多项目里用户会说:“为什么我点完成还要多点一次确认?”
如果你的业务允许偏差但必须留痕,那这恰恰是正确行为。真正要看的是:当前产品是否该用 flexible / warning / strict 中的哪一种,而不是一味追求“一键完成”。
2. 自定义放行流程时,优先接在 pre_button_mark_done() 前后
如果你要增加额外校验,最好理解 Odoo 现有顺序。因为你插在不同位置,业务含义完全不同:
- 插在 consumption 之前,是更强的放行前置条件;
- 插在 backorder 之后,意味着你接受拆单逻辑先发生;
- 如果直接绕过
pre_button_mark_done(),很容易把 Odoo 原有的责任分层打散。
3. tracked 产品的自动修正要格外保守
action_set_qty() 对 lot / serial 的谨慎处理很值得借鉴。任何“自动补 move line”“自动补批次”的定制都要非常小心,否则你修的是数字,坏的是追溯。
一句话总结
Odoo 制造完工前的两个 wizard,不是重复弹窗,而是两道不同的治理关口:先确认“你到底耗了什么”,再确认“你这次到底做完了多少”。
理解这层顺序后,很多看似烦人的弹窗,就会从“系统不顺手”变成“系统在帮你把执行偏差和计划拆分分开记账”。
DISCUSSION
评论区