先说结论
如果你总觉得 Odoo 的库存链“会自己长”,那背后其实不是黑箱,而是几套关系字段在一起工作。
最实用的理解是:
procurement.group:负责把“这些动作属于同一条需求来源”这件事组织起来move_orig_ids:表示这张 move 依赖哪些前置 movemove_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_ids:Destination Movesmove_orig_ids:Original Move
帮助文案也很清楚:
move_dest_ids:Optional: next stock move when chaining themmove_orig_ids:Optional: 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_ids 和 move_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
评论区