先说结论
Odoo 里的 Manual Consumption 不是一个孤立开关。
它真正控制的是:
这条原料消耗,到底由系统按产量自动带着走,还是要由现场或用户明确登记、明确确认、明确算“已经拣/已记账”。
所以你在制造单里改了一次组件实耗,或者某条 move 被系统识别为必须手工登记时,后面会连锁影响:
manual_consumptionpickedqty_producing变化时 move done quantity 的重算pre_button_mark_done()完工前的兜底逻辑
很多实施问题,本质都不是“为什么这个勾选自己变了”,而是系统在保护“自动倒冲”和“人工确认”这两种责任边界不要混掉。
源码里这条链路从哪里开始
在 addons/mrp/models/stock_move.py 里,原料 move 上有个关键字段:
manual_consumption
字段帮助就已经说得很直白:
- 如果启用,就表示该组件的消耗必须手工记录
- 如果未启用,但用户在制造单里手工改过组件消耗,Odoo 也会把它视为手工消耗
也就是说,Manual Consumption 不是只来自表单勾选,还可能来自系统推断。
源码里 _compute_manual_consumption() 和 _is_manual_consumption() 的组合,就是在做这件事。
这一步很重要,因为它告诉我们:
Odoo 在判断的不是“你想不想手工领料”,而是“这条 move 还能不能再被系统安全地自动改写”。
为什么一旦手工改数,后面就不想让系统继续自动覆盖
在制造现场,最怕的不是自动化少,而是:
- 系统刚自动算完
- 现场人员又手工改了
- 下一次改产量或重新带 BOM 时
- 系统把人工确认过的数据又覆盖回去
所以源码在多个入口都体现出同一个原则:
1)改 move 数量时可能把 move 转成手工消耗
在 stock_move.py 里,某些带 force_manual_consumption 的写入场景,会把:
manual_consumption = True- 必要时连
picked = True
一起设上。
这说明 Odoo 把“手工改过”理解为一种责任切换:
- 之前是系统负责按产量推数量
- 之后是用户负责确认这条料到底怎么耗
2)_set_qty_producing() 不会无脑覆盖这类 move
在 addons/mrp/models/mrp_production.py 的 _set_qty_producing() 里,Odoo 会随着 qty_producing 变化去重算原料和成品 move 的 done quantity。
但它先判断:
- 如果 move 已经
picked - 并且它是副产品,或
manual_consumption
那就直接 continue
这句特别关键。
意思是:
一条已经被人工确认过的手工消耗 move,后续改报工数量时,系统宁可不再自动碰它。
这正是很多人看到“我改了 qty_producing,为什么某些料没跟着改”的根源。
不是 Odoo 漏算,而是它在尊重人工登记。
picked 在这里到底是什么意思
很多用户把 picked 理解成“仓库已经拣货完成”。
在制造链路里,这个理解只对一半。
在 _set_qty_producing() 中,Odoo 会在满足条件时自动把 move 标成 picked = True。这更多是在表达:
- 这条原料/成品的本次数量已经被确认进当前生产节拍
- 后续不要再把它当作“完全未处理”的 move 去反复重算
所以 picked 在制造里经常更接近:
本次产量下,这条 move 的处理结果已经被确认。
它既有执行含义,也有“不要再自动覆盖”的技术含义。
为什么完工前还会再兜底一次
在 pre_button_mark_done() 里,Odoo 在真正完工前会再做一轮处理。
其中有一段很值得注意:
- 如果当前
qty_producing不是 0 - 那些
manual_consumption and not picked的原料 move - 会被直接设成
picked = True
很多人第一次看这里会疑惑:
“不是说手工消耗要人工确认吗?为什么完工前系统又给它补 picked?”
答案是:
Manual 不等于永远不能被系统收口
Odoo 的意思不是“手工料永远完全不碰”,而是:
- 数量怎么来,优先由人工负责
- 但在制造单准备完工时,系统要把仍处于未确认尾巴状态的 move 收口
- 否则关单会留下半悬空的执行状态
换句话说,pre_button_mark_done() 做的是流程闭环,不是重新替用户决定消耗数量。
业务上最常见的场景
场景一:本来按 BOM 自动倒冲,后来现场临时多领/少领
一旦你在 move 明细里手工改了数量,系统就会更倾向把这条 move 当作人工负责对象。
后面你再改 qty_producing,这条料不一定继续被自动同步。
场景二:部分原料要扫码或逐笔登记
这类场景天生就适合 manual_consumption = True。
因为业务上你要保留:
- 谁领的
- 领了哪一批
- 实际领了多少
如果还让系统每次按产量自动回写,现场登记价值就被冲淡了。
场景三:用户以为 picked 只是仓库状态
结果看到系统自动勾 picked,就误以为“是不是已经真实出库了”。
其实很多时候,它是在表示这条 move 的本轮处理量已经被制造流程确认,而不只是仓库层面的拣货动作。
新手最容易误解的几点
1)Manual Consumption 不是纯手工勾选字段
它可能来自 BOM 逻辑,也可能来自用户手工改数后的系统推断。
2)改产量不一定会重算所有组件
已经 picked 或被认定为手工消耗的 move,系统会刻意少动。
3)picked 不只是“仓库已拣”
在制造场景里,它还有“该 move 已被本轮生产确认”的含义。
4)完工前补 picked 不是推翻手工逻辑
它是在把流程收口,避免制造单带着未确认尾巴直接关单。
实施和开发时最该注意什么
一,不要把“自动倒冲”和“人工登记”混着讲
如果业务上需要扫码、批号逐笔登记、现场确认,那就应该承认这是一条人工责任链。
不要一边要求手工精确记录,一边又期待系统在每次改产量时全自动覆盖。
二,定制时别轻易强制回写手工 move
很多二开喜欢在 onchange 或 server action 里,无条件把原料数量按 qty_producing 全量重算。
这样非常容易把已经人工确认的 manual_consumption move 覆盖掉。
三,排错时同时看三件事
遇到“为什么这条料没跟着变”时,优先看:
manual_consumption是否为真picked是否已为真- 当前动作是不是走了
_set_qty_producing()或完工前兜底逻辑
只盯界面数量,很容易误判成 bug。
最后总结
Odoo 对制造消耗的设计并不粗暴。
它不是只有“系统自动算”和“用户手工改”两个动作,而是把中间责任边界也编码进来了:
- 是否属于手工消耗
- 是否已被本轮生产确认
- 改产量时还能不能继续自动覆盖
- 完工前是否需要做闭环收口
一句话记住:
Manual Consumption 管的不是一个勾,而是“这条原料消耗现在归谁负责”。
DISCUSSION
评论区