先说结论
Odoo 里的 Change Production Qty,本质上不是“把制造单数量字段改一下”。
它解决的是一个非常现实的问题:
当生产目标数量变化时,系统要尽量沿用当前这条 MO 的执行链,而不是粗暴删掉 move、工单和预留再重建。
所以官方实现的重点不是“重新生成一张新单”,而是:
- 按比例更新原料 move
- 按
unit_factor更新成品和副产品 move - 重新计算工单预计时长和待生产数量
- 对缺料 move 再触发 scheduler
这也是为什么很多人改完数量后会觉得“怎么和我想的 BOM 不一样”。
因为这个向导的目标是延续执行链,不是重新解释工艺定义。
这个机制到底在解决什么问题
真实现场里,制造数量经常在生产前后被改动:
- 客户临时加单,原本做 50 件改成 80 件
- 某批次试产后决定缩量,只做 20 件
- 已经开始领料,但计划层又重算了一次需求
如果系统每次都把 MO 删除重建,会立刻出现一堆问题:
- 已经挂上的 reservation 怎么办
- 已有 work order 的状态和已产数量怎么接
- 已创建的成品 / 副产品 move 怎么保留追溯
所以 Odoo 在 addons/mrp/wizard/change_production_qty.py 里采用了更稳妥的路线:
优先改已有对象,让 move 和 workorder 顺着新数量继续跑。
核心链路:向导到底做了什么
入口是 change.production.qty 的 change_prod_qty()。
1. 先拿旧数量和新数量
向导先读:
old_production_qtynew_production_qtyfactor = new / old
后面大部分更新,都是围绕这个比例展开。
2. 调 production._update_raw_moves(factor)
这是最关键的一步。
它会把原料 move 按比例放大或缩小,并返回哪些 move 被改了。
这一步解决的是“原料需求怎么跟着目标数量变”。
注意这里的语义是:
- 更新现有 raw moves 的需求量
- 不等于“重新按最新 BOM 全量 explode 一次”
所以如果你改数量之前,BOM 自身已经变了,Change Production Qty 并不会自动替你完成“换工艺版本”这件事。
3. 记录异常与活动日志
源码里会把 move 变化整理成 documents,再调用:
_log_manufacture_exception()
这说明官方并不把数量调整当成一个无痕操作,而是默认它可能影响上下游执行对象,应该留下可追踪的提示。
4. 更新成品与副产品 move
向导随后调用 _update_finished_moves(production, new_qty, old_qty)。
这里的逻辑非常值得注意:
- 它只处理 未 done / 未 cancel 的 finished moves
- 变更量按
(new_qty - old_qty) * move.unit_factor算 - 如果 move 还有
move_dest_ids,可能走 copy + confirm 的传播路线
这说明成品和副产品不是直接写死成“新数量”,而是沿着既有 move 结构按单位比例续算。
一句话记:
Change Production Qty 处理 finished move 时,依赖的是现有 move 的
unit_factor,不是重新回到 BOM 重新推一遍。
5. 调整工单时长和 qty_producing
对每条 workorder_ids,系统会:
- 重算
duration_expected - 重新计算
qty_production - qty_produced - 更新
qty_producing - 必要时把状态从
done拉回progress,或者从progress推回done
这一步解决的是执行层最容易乱掉的问题:
- 数量变了,工单预计时长要不要变
- 已做一半的工单,后面还要做多少
- 工单关联的 move_line、operation 绑定还对不对
6. 最后再给缺料 move 触发 scheduler
源码最后会对 confirmed/progress 状态的 raw moves 调 _trigger_scheduler()。
意思很明确:
- 如果这次改大数量,把需求放大了
- 系统会再去看哪些组件需要补货 / 补制
所以数量调整不是停留在 MRP 表单层,而会继续把影响传到补货逻辑。
为什么它不是“重新展开 BOM”
这是新手最容易误解的地方。
源码注释已经说得很直接:
更新 finished moves 时,不会考虑生产过程中 BoM 被修改的情况。
raw move 这边虽然会按比例更新,但整个向导的目标仍然是在原 MO 上续跑,不是“把 MO 变成按最新 BOM 全新生成的一张单”。
所以这两件事不能混:
- 改数量:用 Change Production Qty
- 换 BOM / 更新工艺定义:看是否需要
action_update_bom()或重新建 MO
很多现场问题,本质上都是把这两个动作混成一个动作了。
新手最常踩的 4 个坑
坑 1:以为直接 write({'product_qty': ...}) 就够了
不够。
你如果只改 product_qty 字段:
- raw moves 不会按官方链路重算
- finished moves 的传播逻辑不会跑
- work order 预计时长和状态续算也可能失真
坑 2:以为数量调整会顺便吃掉最新 BOM 变化
不会。
如果你同时改了 BOM 行、工序或副产品定义,单独跑数量调整,常常只会得到“按旧 move 结构放大/缩小”的结果。
坑 3:忽视序列号产品的特殊分支
源码里对 tracking == 'serial' 有单独处理。
序列号产品的 qty_producing 不会像普通批量产品那样直接按剩余量推进,而是更接近“一次一件”的执行语义。
坑 4:以为 scheduler 会自动补齐所有后果
scheduler 只是在最后对 raw moves 再看一遍补货。
它不是万能修复器,修不了你错误的 BOM、错误的 move 绑定、或者你绕开官方向导直接改字段造成的脏数据。
开发时最该注意什么
1. 不要把“改数量”和“改工艺”塞进同一个 override
这是最危险也最常见的自定义错误。
如果你在数量变更时顺手:
- 重建 raw moves
- 重建 finished moves
- 重建 workorders
很容易把 Odoo 原本保留的 reservation、move_dest 传播和状态续链全冲掉。
2. 自定义字段要想清楚挂在哪一层
因为 finished move 可能走 copy 传播,raw move 也可能重算数量。
如果你有自定义字段依赖:
- move copy
- workorder duration
- qty_producing
就要检查这些字段在变更数量后是否还能正确继承。
3. 调试时先看 4 组对象
最有效的排查顺序通常是:
mrp.production.product_qtymove_raw_ids.product_uom_qtymove_finished_ids.product_uom_qtyworkorder_ids.duration_expected / qty_producing / state
不要只盯 MO 头字段。
4. 真要按新 BOM 重算,就走另一条链路
如果业务语义已经不是“继续当前单”,而是“按新定义重算这张单”,那就应该明确走:
- 更新 BOM
- 重新 link BOM
- 或直接取消旧单、重建新单
而不是把 Change Production Qty 硬改成 BOM 重建器。
最后总结
Change Production Qty 的真正价值,不是方便改数字,而是:
在制造执行链已经长出来之后,让 Odoo 用尽量小的代价把数量变化传播到 raw move、finished move、workorder 和补货链。
所以你可以把它理解成一个“延续式重算器”。
它擅长的是:
- 数量变化
- 执行续链
- 预留与工单尽量保真
它不擅长的是:
- 用最新 BOM 重新定义整张 MO
- 替你修复绕开官方链路的脏更新
把这个边界看清,很多“为什么数量改了结果怪怪的”问题,基本就能解释通了。
DISCUSSION
评论区