制造补单

Odoo 制造补单为什么不是库存 Backorder 的翻版:MO 拆分、数量继承与工单续链讲透

制造里的 backorder 不只是“剩余数量另起一张单”,它还会拆 move、搬 reservation、续接 work order。本文把 MO backorder 讲清楚。

Odoo 开发 制造 库存
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说结论

Odoo 制造里的 backorder,和仓库发货里的 backorder 长得像,但内部复杂度明显更高。

因为制造 backorder 不只是把“剩余数量”另挂一张单,它还要同时处理:

  • 原料 move 怎么拆
  • 成品 / 副产品 move 怎么拆
  • 已预留的 move line 怎么分给新旧单
  • 工单的已产数量和待接续数量怎么继承

一句话记:

制造 backorder 本质上不是复制 MO,而是把一条正在执行的生产链,按已完工与未完工数量重新切开。


为什么制造 backorder 比库存 backorder 更难

普通库存 backorder 的核心语义比较单纯:

  • 这次做了多少
  • 剩余多少以后再做

但制造单背后挂着一整串执行对象:

  • move_raw_ids
  • move_finished_ids
  • workorder_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_ids
  • move_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

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。