先说结论
如果你排查 Odoo 库存链路时总盯着单据状态发呆,那很容易越看越乱。
最实用的理解是:
stock.move的状态不是“系统心情”,而是在表达这张 move 当前卡在哪一层:前置依赖、可分配性、还是已完成预留。
你可以先把几个常见状态记成这样:
waiting:我前面还有依赖,先等confirmed:我已成立,但还没真正拿到货partially_available:我拿到了一部分,不是全部assigned:我需要的量已经完成预留
所以状态机的重点不是背单词,而是看:
- 它为什么会进入这个状态
- 从这个状态跳到下一个状态的条件是什么
这才是排查真正有用的部分。
为什么很多人会把库存状态看成“玄学”
因为前台看到的只是一个词:
- Waiting
- Ready
- Partially Available
但源码里不是按“显示词”来思考的,而是按一系列条件判断:
- 有没有前置 move
- procure_method 是什么
- 有没有 reserve 到足够 quantity
- reservation_date 到没到
- 当前 quant / move line 情况怎么样
所以你在 UI 里看到的是一个状态名,源码里真正表达的是:
这张 move 目前属于哪一类可执行性。
只要脑子里换成“可执行性分类”而不是“状态标签”,理解就会顺很多。
第一层:waiting 到底是在等什么
很多人一看到 waiting,第一反应就是:
- 没货了
其实不一定。
在 stock.move._action_confirm() 里,源码很清楚:
- 如果 move 有
move_orig_ids - 那它会先进入
waiting
翻成人话就是:
只要这张 move 有前置依赖,它就不是链路第一跳,所以先等前面的 move。
这很关键。
因为这说明 waiting 首先表达的不是“库存数量不足”,而是:
- 供应链顺序上,我还没轮到。
还有一种 waiting
源码里还有一类:
- 如果 move 是
make_to_order - 它也会进入
waiting
原因是:
- 当前这张 move 不是直接从现货完成
- 它还要继续向上游触发 procurement
所以 waiting 本质上通常来自两类原因:
- 有前置 move
- 需要继续等上游补货动作长出来
第二层:confirmed 到底在表达什么
confirmed 最容易被误读成:
- 已经差不多好了
其实更准确的理解是:
这张 move 在业务上已经成立,但当前还没拿到足够的预留结果。
也就是说:
- 不是 draft 了
- 也不是纯等待前置链了
- 但还没进入“我已经拿到货”的状态
在 _action_confirm() 里,普通 move 往往会被写成 confirmed。
后面如果满足条件,系统才会进一步 _action_assign()。
所以 confirmed 更像:
- 已进入执行队列,但资源还没真正锁到手
这跟很多人以为的“马上就 ready”其实差一层。
第三层:partially_available 为什么特别值钱
这个状态常常被忽略,但它在实战里很值钱。
它表示:
这张 move 不是完全没拿到货,而是已经 reserve 到了一部分。
这通常意味着:
- 数量边界不够整张吃满
- 一部分库存已经可用
- 剩下部分还在等进一步供给或后续操作
在 _action_assign() 以及后续状态整理逻辑里,源码都会单独把它和 assigned 分开。
这很重要,因为 partially_available 告诉你:
- 问题不是“完全没有库存”
- 而是“库存 / 可分配量 / 时机 / 上游链路只够一部分”
所以一张 move 如果长期停在 partially_available,排查思路就不该再是“为什么一点都没分到”,而应该转成:
- 差的那部分为什么一直补不上
第四层:assigned 真正表示什么
assigned 才是大家最想看到的状态。
它表示:
需要的 quantity 已经完成 reservation,可以进入后续作业。
但这也不等于“业务流程已经结束”,只是表示:
- 这张 move 在库存占用层已经准备好了
所以你可以把它理解成:
- 货已经真正锁住了
- 仓库可以开始按这张 move 干活
这也是为什么很多 picking 到了 assigned,用户体感上就觉得“终于 ready 了”。
源码里真正的状态切分点在哪里
最值钱的一段在 stock.move 里整理状态的逻辑,大意可以翻成这样:
进入 assigned
- 当前已 reserve 数量 >= 需求数量
进入 partially_available
- 当前 reserve 到了一部分
- 但还没达到全部需求
进入 waiting
- 是 MTO 且没有前置来源,或者
- 有
move_orig_ids且前置里仍有未完成的正向 move
否则进入 confirmed
- 已成立,但既不是明确等待依赖,也还没 reserve 到任何 / 足够数量
这段逻辑很值钱,因为它让你知道:
状态不是凭感觉切的,而是按“依赖 + reservation 结果”算出来的。
为什么 reservation_date 会改变状态体感
这点很多人会忽略。
move 不是一 confirmed 就一定立刻 assign。
源码里的 _should_assign_at_confirm() 条件大致是:
- bypass reservation,或者
- picking type 的
reservation_method == at_confirm,或者 reservation_date <= 今天
这意味着:
有些 move 之所以一直停在
confirmed,不是因为系统不行,而是因为它还没到该抢库存的时间。
所以当你看到:
- move 已 confirmed
- 也没有 waiting 的前置依赖
- 但迟迟没进 assigned / partially_available
一定要查:
- reservation method
- reservation_date
否则你会误判成库存问题。
一个非常实用的状态机理解法
你可以把 move 状态机先粗暴拆成三层:
第一类:顺序问题
waiting
说明:
- 我前面还有链路没走完
第二类:资源问题
confirmedpartially_availableassigned
说明:
- 链路上轮到我了,但资源锁定程度不同
第三类:完成 / 取消问题
donecancel
说明:
- 这张 move 已经离开当前执行竞争
这样一来,你排查时就不会把所有状态都当一个维度看。
最实用的排查顺序
如果你想快速看懂一张 move 为什么停在当前状态,我建议按这个顺序:
1. 先看它是不是 waiting
如果是,先查:
move_orig_ids- 上游 move 状态
- 是否是 MTO 链的一环
2. 如果不是 waiting,而是 confirmed
查:
reservation_methodreservation_date- 有没有真正触发
_action_assign()
3. 如果是 partially_available
查:
- 已 reserve 数量 vs 目标数量
- 缺的那部分为什么补不上
- 有没有 lot / owner / package / tracking 边界
4. 如果是 assigned
说明 reservation 层已经过了,后面要看的是:
- 仓库执行
- move line
- validate / done 过程
这套顺序会比“先看库存够不够”更稳。
最容易踩的 6 个坑
1. 把 waiting 简化成“没货”
很多时候它是在等前置 move,不是在等凭空长库存。
2. 把 confirmed 误以为“马上就 ready”
它只是进入执行队列,不代表已经 reserve 到货。
3. 把 partially_available 当成“几乎等于 assigned”
不是。它说明还有缺口,只是缺口不是 100%。
4. 忽略 reservation timing
有些 move 还没到 reservation_date,本来就不会立刻分配。
5. 只看当前 move,不看上游链
特别是 waiting 场景,单看当前 move 基本看不明白。
6. 只看状态,不看数量
assigned / partially_available 的分界,本质上就是数量比较。
一句话记忆法
把 move 状态机记成一句话:
waiting是顺序还没轮到,confirmed是业务已成立但资源未锁定,partially_available是锁到一部分,assigned是锁到全部。
理解这一句之后,你再看 Odoo 库存 move 的状态,就不会再把它们当成抽象标签,而会开始看到:
- 这张 move 是卡在依赖
- 还是卡在时机
- 还是卡在数量
- 还是已经真的 ready 了
DISCUSSION
评论区