需求链追踪

Odoo 需求链到底怎么串起来:procurement.group、move_orig_ids、move_dest_ids 深度解读

很多人能看懂单张拣货单、采购单、制造单,却一到“这条需求链到底是怎么从销售一路串到补货和履约”的问题就开始迷路。本文把 procurement.group、move_orig_ids、move_dest_ids 讲成人话,解释 Odoo 是怎么把一条需求变成一串彼此关联的 move 链的。

Odoo 开发 库存
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 26 阅读

先说结论

如果你总觉得 Odoo 的库存链“会自己长”,那背后其实不是黑箱,而是几套关系字段在一起工作。

最实用的理解是:

  • procurement.group:负责把“这些动作属于同一条需求来源”这件事组织起来
  • move_orig_ids:表示这张 move 依赖哪些前置 move
  • move_dest_ids:表示这张 move 会继续喂给哪些后继 move

所以它们分别解决的是三件不同的事:

  • 谁是一伙的
  • 谁在前
  • 谁在后

一句话记就是:

procurement.group 更像需求归组标签,move_orig_ids / move_dest_ids 更像 supply chain 里的前后指针。

理解这一句之后,很多“为什么销售单会串出调拨、采购、制造、发货这一长串对象”的问题就不再神秘。


为什么很多人一看链路就迷路

因为业务界面更容易让人盯着“单据”。

你看到的通常是:

  • 销售单
  • 拣货单
  • 采购单
  • 制造单
  • 调拨单

这些对象都很显眼。

但 Odoo 真正把它们串起来的,不是“单据名字像不像一条线”,而是底层关系:

  • reference
  • procurement grouping
  • move chain

所以如果你只看单据编号,很容易卡在表层:

  • 为什么会一起出现?
  • 为什么这张 move 在等另一张?
  • 为什么一张销售行能长出多跳库存动作?

这些问题的答案,很多时候都在 move_orig_ids / move_dest_ids 这种链路字段里。


先把三个核心对象讲成人话

procurement.group 像什么

它最像:

同一来源需求的一张“归组标签”。

也就是说,它主要在告诉系统:

  • 这些履约动作是不是同一批来源需求的延伸
  • 它们后续能不能继续按同一来源上下文组织
  • 某些 picking / move 是否应该归到同一来源链下看

它不是 move 之间的“前后因果箭头”,更像是:

  • 这些对象属于同一条业务来源上下文

move_orig_ids 像什么

它最像:

当前 move 的前置依赖。

也就是:

  • 这张 move 不是凭空就能做
  • 它要等前面的 move 先完成,或者至少推进到某种状态

move_dest_ids 像什么

它最像:

当前 move 的后继去向。

也就是:

  • 我这张 move 做出来的货,接下来会继续供应给谁

所以这两个字段放在一起,本质上就是一条链:

  • 前一跳 → 当前跳 → 后一跳

从源码上看,Odoo 自己就是把 move 当“链”来处理的

stock.move 模型里,字段定义已经写得很直白:

  • move_dest_idsDestination Moves
  • move_orig_idsOriginal Move

帮助文案也很清楚:

  • move_dest_idsOptional: next stock move when chaining them
  • move_orig_idsOptional: previous stock move when chaining them

也就是说:

Odoo 官方就把 move 设计成可以彼此串链,而不是一张张孤立存在。

这不是某个模块的额外玩法,而是 stock.move 的底层设计之一。


move_orig_ids 为什么这么关键

因为它直接决定一张 move 会不会进入“等前置动作”的状态。

stock.move._action_confirm() 里,源码逻辑很明确:

  • 如果 move 有 move_orig_ids
  • 那它会先进 waiting

翻成人话就是:

只要当前 move 有前置 move,它就会被视为“要等前面的动作”。

这非常重要。

因为很多人看到库存 move 是 waiting,会以为只是“库存不够”。

其实不一定。

更准确地说,waiting 常常意味着:

  • 这张 move 不是链路第一跳,它要等它的上游 move。

所以当你排查“为什么这张拣货 / 调拨 / 原料 move 一直不动”时,第一反应不该只是查库存,还要查:

  • 它是不是有 move_orig_ids
  • 这些 orig move 当前状态是什么

move_dest_ids 为什么能把需求往后串下去

这点在 MTO 场景里特别明显。

stock.move._prepare_procurement_values() 里,源码有一段关键逻辑:

  • 如果当前 move 的 procure_method == "make_to_order"
  • move_dest_ids = self

什么意思?

翻成人话就是:

当这张下游 move 因为 MTO 要继续往上游补货时,系统会把“我自己”作为后继 move,塞进新 procurement 的 values 里。

后面当 stock.rule 根据这个 procurement 去创建新的上游 move 时,又会把 values['move_dest_ids'] 写进新 move 的 move_dest_ids

于是关系就形成了:

  • 新的上游 move → 当前这张下游 move

也就是说:

MTO 并不只是“触发补货”,它还会把补出来的上游动作,精确地链回原本那张下游需求 move。

这就是需求链可追踪的关键原因之一。


为什么 move_orig_idsmove_dest_ids 是一对,而不是两个重复字段

很多人会下意识想:

  • 反正都在描述关系,留一个不就够了?

不够。

因为供应链关系本来就有方向。

从“我依赖谁”的角度看

你需要 move_orig_ids

它回答:

  • 我这张 move 前面是谁
  • 我要等谁

从“我供给谁”的角度看

你需要 move_dest_ids

它回答:

  • 我这张 move 后面喂给谁
  • 我完成后谁会继续推进

所以一个是:

  • backward dependency

另一个是:

  • forward propagation

这就是为什么两者都得有。


procurement.group 和 move 链到底是什么关系

这是最容易混淆的点。

很多人听说 procurement.group 很重要,就会以为:

  • 有了 group,move 之间的前后关系就自动都清楚了

其实不对。

procurement.group 解决的不是“箭头”问题

它更像:

  • 这些 move / picking / procurement 是否属于同一来源需求上下文

也就是“是不是一伙的”。

move_orig / move_dest 解决的才是“箭头”问题

它们描述的是:

  • 哪张在前
  • 哪张在后
  • 谁依赖谁
  • 谁供应谁

所以这两层关系最好分开记:

procurement.group

同源归组

move_orig_ids / move_dest_ids

具体前后链路

这也是为什么光看 procurement.group,不一定能完全看清每一跳供应方向;但如果没有 procurement.group,整条来源上下文又会更散。


为什么销售单看起来能“一路串下去”

因为 sale_stock 也在主动利用这些链路。

sale_stock/models/stock.py 里,你能看到:

  • move 会带 sale_line_id
  • 并且 _get_sale_order_lines() 会把当前 move 加上它整条 origin / dest 链一起 rollup,再去找相关 sale line

这说明一件事:

销售相关追踪并不是只看当前这张 move,而是会沿整条 move chain 回溯和前探。

也就是说,Odoo 自己就知道:

  • 要理解这张 move 和哪张销售行有关
  • 不能只看它自己
  • 要看整条链

这也是为什么销售、库存、补货之间的来源关联很多时候是能“串回去”的。


Odoo 甚至内置了“整条链 rollup”的方法

stock.move 里,源码直接提供了:

  • _rollup_move_origs()
  • _rollup_move_dests()

以及对应的递归实现 _rollup_moves()

这非常值钱,因为它说明:

Odoo 不只是允许 move 成链,它还默认提供了遍历整条上下游链的能力。

也就是说,链路追踪不是你自己想象出来的分析方式,而是系统层面就内建的思路。

所以实战里,如果你想知道:

  • 这张 move 前面还有哪些来源动作
  • 后面还串了哪些下游动作

本质上就是沿着这两条链去看。


用一个销售 → 补货 → 发货的例子讲透

假设客户下了一个销售单,需要 10 件产品,而当前仓库没有足够现货。

第一步:销售行发起需求

销售行会通过 _action_launch_stock_rule() 创建 procurement。

第二步:先生成下游履约 move

系统先生成面向客户方向的下游 move。

第三步:如果是 MTO / 上游补货场景

这张下游 move 在 confirm 时会继续触发 procurement。

此时系统会把这张下游 move 放进 move_dest_ids

第四步:创建上游补货 move

不管最终命中的是:

  • 内部调拨
  • 采购补货
  • 制造补货

新生成的上游动作都会带着:

  • move_dest_ids = 下游那张 move

于是关系就成了:

  • 上游补货 move → 下游履约 move

第五步:下游 move 会等上游 move

于是下游 move 往往会在 move_orig_ids / 依赖逻辑下进入 waiting,直到上游推进。

这就是“需求链自己长出来”的真相:

  • 不是乱长
  • 是每一跳都通过 move_dest_ids / move_orig_ids 精确串起来的

实战排查时最该看的 7 个点

1. 当前 move 有没有 move_orig_ids

如果有,它大概率不是第一跳。

2. 这些 move_orig_ids 当前状态是什么

决定它为什么 waiting。

3. 当前 move 有没有 move_dest_ids

可以快速看出它后面还喂给谁。

4. 当前 move 是不是 MTO 触发出来的链中一环

这会直接影响 move_dest_ids 的生成方式。

5. procurement.group 是否一致

看这批动作是不是同一来源上下文。

6. sale_line / picking / reference 是不是沿链能 rollup 回去

可以帮助你从库存对象回到业务来源。

7. 不要只盯单据号,要盯“依赖关系”

单据号是表象,链路关系才是根因。


最容易踩的 6 个坑

1. 把 procurement.group 当成“前后链路本身”

它更像归组,不是箭头。

2. 看到 waiting 就只怀疑库存不足

其实很多时候是前置 move 还没完成。

3. 只看当前 move,不看它的 orig / dest

这样永远只能看到局部。

4. 把 MTO 理解成“只是自动采购 / 制造”

它更重要的意义之一,是把补货动作链回原始需求 move。

5. 只用前台文档追链,不看 move 关系字段

会很容易迷路。

6. 以为 Odoo 的链路追踪是“模块之间偶然对上了”

其实 move chain 是底层设计,不是偶然。


一句话记忆法

把这套机制记成一句话:

procurement.group 负责把同源需求归成一组,move_orig_ids 负责说明“我在等谁”,move_dest_ids 负责说明“我在供给谁”,三者一起把 Odoo 的销售、补货、调拨、采购、制造串成一条可追踪的需求链。

理解这一句之后,你再看 Odoo 的库存链,就不会只看到一堆散落单据,而会开始看到:

  • 哪些动作属于同一来源
  • 哪些动作在前
  • 哪些动作在后
  • 整条需求到底是怎么一路传下去的

DISCUSSION

评论区

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