很多团队一提“制造追溯”,第一反应就是:系统里把 lot / serial 号记录下来,不就行了吗?
但 Odoo 在 test_traceability.py 里验证的远不止“有没有记录到批次号”。它在守三条更难的边界:
- 最终交付时,能不能把上游制造链一路追到最后一跳 delivery;
- 已经被消费过的序列号,能不能再次被消费;
- 如果通过 unbuild 把它合法释放回来,它又该不该重新变成可用。
这三条边界才是制造追溯真正值钱的地方。
第一层:最后一跳交付不是只记录成品 lot
test_last_delivery_traceability() 设计了一个很典型的三级结构:
- Subcomponent A → Component A → EndProduct A
- 三者都按 lot 跟踪
测试先做子组件,再做组件,再做成品,最后创建一张出库 picking,把 EndProduct A 发给客户。
关键断言在最后:lot_subcomponentA、lot_componentA、lot_endProductA 的 delivery_ids 都应该指向同一张最终出库单。
这意味着 Odoo 的追溯目标不是“知道成品 lot 发给了谁”,而是让上游投入 lot 也继承最后一跳交付语义。
这对售后和召回极其关键。否则你只能查到哪个成品出了问题,却很难把问题回卷到上游哪一批投入物。
第二层:已消费序列号不能无限复用
test_use_lot_already_consumed() 讲的是另一条硬边界。
流程很简单:
- 先制造出一个带序列号的组件;
- 再在一张 MO 里把这个序列号消费掉;
- 然后尝试在另一张 MO 里再次消费同一个序列号。
第三步应该报 UserError。
这背后的原则不是“系统小气”,而是追溯对象不能在库存语义上同时存在于两个消费事实中。如果一个序列号已经在先前制造中被吃掉,你不能假装它还在货架上等着下一张 MO 再用一次。
第三层:unbuild 之后,序列号应该重新回到“可合法消费”状态
只拦截重复消费还不够,因为真实业务里还有逆向动作。
test_produce_consume_unbuild_and_consume() 验证的是:
- 先制造一个 SN;
- 把它作为组件消费掉;
- 对消费它的成品做 unbuild;
- 再次消费这个 SN;
- 这次不应报错。
这说明 Odoo 不是简单给 lot / serial 打一个“消费过”的永久黑名单,而是在看当前库存状态与追溯链是否已被逆向释放。
如果 unbuild 合法地把组件放回库存,序列号就应该重新回到可被消费的位置。
再往前一步:即使两层都拆回去了,也不能把逻辑写死成“永远不能再用”
test_produce_consume_unbuild_all_and_consume() 进一步说明,哪怕:
- 消费用掉的成品被拆了;
- 更早的“制造出这个 SN”的那层也被拆了;
- 再通过库存更新把它放回可用状态;
系统仍然应该允许后续再次消费。
这很重要,因为它展示了 Odoo 的追溯判断不是按“历史上你曾经被消费过”这么粗暴,而是按“你当前在库存与追溯链里的法律地位”来判断。
为什么这套边界比“全程可查”更难实现
很多系统会宣传“端到端可追溯”,但真正难的不是展开链条,而是处理链条上的逆向动作:
- 消费了以后还能不能再用;
- 拆解后是不是自动恢复可用;
- 最后一跳交付要不要把上游 lot 一起带过去。
Odoo 这些测试说明它在做的是“可追 + 可逆 + 不自相矛盾”的追溯模型,而不是静态台账。
对实施和开发的启发
1. lot / serial 校验不要只看 move line 当前值
你需要考虑这个 lot 在历史链条里的状态,尤其是是否已经被消费、是否被 unbuild 释放、是否已经重新回库。
2. 售后、返修、拆解项目一定要联动测试 traceability
很多追溯 bug 只在逆向链路里出现。只测正向生产和出货,往往看不出问题。
3. 如果你做召回或质量定责,delivery_ids 的最终挂接很关键
因为那决定了你能不能从一个上游 lot 直接知道它最终落到了哪一张客户交付上。
一句话总结
Odoo 制造追溯真正难的,不是把批次号记下来,而是让它在 最终交付、重复消费限制、unbuild 后重新可用 这三件事上同时保持一致。做到这一步,追溯才算真的能用于生产现场,而不只是做台账展示。
DISCUSSION
评论区