先说结论
Odoo 制造里的 backorder,和仓库发货里的 backorder 长得像,但内部复杂度明显更高。
因为制造 backorder 不只是把“剩余数量”另挂一张单,它还要同时处理:
- 原料 move 怎么拆
- 成品 / 副产品 move 怎么拆
- 已预留的 move line 怎么分给新旧单
- 工单的已产数量和待接续数量怎么继承
一句话记:
制造 backorder 本质上不是复制 MO,而是把一条正在执行的生产链,按已完工与未完工数量重新切开。
为什么制造 backorder 比库存 backorder 更难
普通库存 backorder 的核心语义比较单纯:
- 这次做了多少
- 剩余多少以后再做
但制造单背后挂着一整串执行对象:
move_raw_idsmove_finished_idsworkorder_ids- reservation / move line
- byproduct 语义
所以 MO 不能只靠“新建一张剩余单据”解决。
如果只是新建一张空白 MO,系统马上会失真:
- 预留丢了
- 已消耗和未消耗混了
- 工序进度断了
这也是为什么官方专门做了 _split_productions()。
触发 backorder 的边界到底是什么
在 button_mark_done() 前,Odoo 会先跑 _get_quantity_produced_issues()。
逻辑很直接:
- 如果
_get_quantity_to_backorder()不为 0 - 就说明这次没把应完工数量全部做完
- 然后进入
_action_generate_backorder_wizard()或直接 split
而 _get_quantity_to_backorder() 返回的是:
max(product_qty - qty_producing, 0)
也就是说,制造 backorder 的核心边界是:
你这次准备正式收口的产出量
qty_producing,是否小于制造单原本要做的product_qty。
不是简单看 move done 没 done。
_split_productions() 到底做了什么
这段源码是制造 backorder 的核心。
1. 先确定拆分数量
如果没显式传 amounts,系统默认会按:
- 原单保留本次
qty_producing - 剩余量变成 backorder
也就是说,当前单不是整单结束,而是被改写成“这次实际完成的那部分”。
2. 给原单和补单重命名、编号
源码里会处理:
backorder_sequence_get_name_backorder()
所以你看到名称像 WH/MO/001-001、-002 这类变化,不是界面技巧,而是 backorder 链正式激活了。
3. 拆原料和成品 move
系统会遍历:
move_raw_idsmove_finished_ids
按原单数量比例算 unit_factor,然后:
- 把原 move 缩成当前完工量那部分
- 再给每个 backorder 复制新 move
所以新旧 MO 不是共享同一组 move,而是按数量重切后的 move 族谱。
最难的地方:move line 和 reservation 怎么分
这也是制造 backorder 真正“深”的地方。
源码没有偷懒去全部 unreserve 再重新 assign,而是尽量手工拆 reservation。
注释里明确说了这么做的原因:
- 直接 do_unreserve → action_assign 虽然简单
- 但会更慢
- 还可能在 FIFO 场景下,因为中途来了新批次,导致 reservation 结果和原现场事实不一致
所以 Odoo 会:
- 把原 move line 上的可用数量按顺序分给新旧 move
- 标记 assigned / partially_available
- 最后只对必要部分再
_action_assign()
这背后的设计思想很实际:
制造补单不是重新算一遍理想预留,而是尽量保住当下已经发生过的预留事实。
工单为什么也要跟着续链
如果只拆 move 不拆 workorder,现场执行就断了。
所以 _split_productions() 后面还专门处理了工单:
- 给 backorder 上的新工单重算
duration_expected - 对原工单,把
qty_produced收缩到本次该保留的量 - 给补单工单写
qty_reported_from_previous_wo - 对已经没有剩余数量可接的工单,直接取消
这个字段很关键:qty_reported_from_previous_wo
它的帮助文本写的是:
已从前序 backorder 链里带过来、等待分配的数量。
所以制造 backorder 不是“新单从零开始”,而是:
前面已经做过的那部分产出,会沿着工单链继续往后传。
这也是很多人第一次看 Odoo MRP 源码会觉得惊讶的地方。
为什么 backorder 后有时又自动 reserve 了
在 button_mark_done() 末尾,官方还会对符合条件的 backorder 跑 action_assign()。
特别是 reservation_method == 'at_confirm' 的场景,系统会谨慎地再尝试预留一次。
原因也很合理:
- 原单完工后,可能释放或腾挪出新的可用库存
- 这些库存应该尽快喂给刚生成的补单
所以你看到 backorder 生成后状态马上变好,不一定是人操作了什么,而是源码本来就会顺手推进。
实战里最容易误解的 5 件事
1. 以为制造 backorder 只是多建一张剩余 MO
实际上它会重切 move、move line 和工单。
2. 以为原单永远保持原始数量
不对。原单会被改写成“本次完成的那部分数量”。
3. 以为 reservation 会全部推倒重来
源码恰恰在尽量避免这么做。
4. 以为新工单是全新开始
qty_reported_from_previous_wo 说明它会继承前链数量语义。
5. 只看 MO 头,不看 move 和工单链
你会完全看不懂 backorder 为什么变成现在这样。
一句话记忆法
制造 backorder 不是给剩余数量补一张单,而是把在制生产链按数量重新切开,并尽量保留原本的预留、消耗和工序接力关系。
DISCUSSION
评论区