先说结论
很多实施人员已经知道:
- Odoo 会把多个采购需求归并到同一张 PO
但真正容易踩坑的是下一层:
- 进了同一张 PO,不代表一定合成同一条采购行。
源码关心的不是“长得像不像一条行”,而是:
- 合并之后,会不会破坏补货来源追踪
- 会不会影响销售交付数量计算
- 会不会让不同业务语义混到同一条采购行里
所以同一个产品、同一个供应商、甚至同一张采购单上,也完全可能出现多条采购行。
这不是系统啰嗦,而是 Odoo 在保护业务边界。
这篇为什么不是“采购合单”那篇的换皮
站里已有文章讲 PO 层面的合单:
- 为什么多条采购需求会进同一张采购单
- 合单看哪些大维度
这篇讲的是更细的层级:
进入同一张 PO 之后,哪些 procurement 还能继续并成同一条
purchase.order.line,哪些必须拆开?
这是完全不同的问题。
- PO 合单,回答“是不是同一张执行单”
- 行归并,回答“是不是同一条执行明细”
很多业务 bug 正是发生在第二层。
源码入口:_find_candidate() 才是采购行能不能并的关键
在 /home/ubuntu/odoo-temp/addons/purchase_stock/models/purchase_order_line.py 里,_find_candidate() 会决定一个 procurement 是否可以并入已有采购行。
它至少会看这些维度:
propagate_cancelorderpoint_id- 是否要求强制同 UoM(
force_uom) product_description_variants- 以及已有行的名称上下文
这说明 Odoo 不是简单用“同产品同供应商”做唯一判断。
它真正担心的是:
- 行级上下文是否仍然一致
如果不一致,就算 PO 层面可以共存,也不该继续混成一行。
为什么 orderpoint_id 会阻止采购行随便合并
_find_candidate() 里有一句非常关键的条件:
- 当当前 procurement 带
orderpoint_id且没有move_dest_ids时 - 只允许并到同一个
orderpoint_id或空orderpoint_id的采购行
这背后是个典型的补货语义问题。
假设你有两个补货规则:
- 同产品
- 同供应商
- 但服务于不同位置或不同补货来源
如果系统把它们粗暴合成一行,后面就会出现三个难题:
- 在途量应该记给哪个 orderpoint?
- 这个采购行未来到货时,谁算完成补货?
- 某个 orderpoint 需要重算时,怎么知道这一行里有多少是它的量?
所以 Odoo 宁可多一条采购行,也要保住 orderpoint 追踪语义。
这和“系统会不会合单”不是一个层级的问题。
为什么 move_dest_ids 又会改变归并判断
采购行创建时,Odoo 会把 procurement 的 move_dest_ids 写到采购行上。
这个字段很重要,因为它表达的是:
- 这次采购不是单纯补库存
- 而是为了满足某些后续 move / demand
一旦带有明确的目标 move,系统对“能不能并行”就会更谨慎。
原因也很直白:
- 目标 demand 不同,后续链路不同
- 你把它们糊成一行,追踪就会变模糊
所以在很多销售触发采购、MTO、跨单据补货场景里,你会看到采购行比你预期更“碎”。
它不是碎,是在保护需求链的可解释性。
Dropship 为什么尤其不能乱并行
在 /home/ubuntu/odoo-temp/addons/stock_dropshipping/models/stock.py 里,官方对 _get_procurements_to_merge_groupby() 做了扩展:
- 如果 linked procurement 来自不同
sale_line_id - 就不要轻易合并
源码注释写得非常明确:
目的就是为了正确计算 delivered quantities。
这句非常值钱。
说明在 dropship 里,采购不是独立后勤动作,而是销售履约的一部分。
如果两个销售行对应的 dropship 采购被粗暴合并,后面你会立刻遇到这些问题:
- 哪条销售行算已交付?
- 哪条销售行该看见供应商直发结果?
- 部分到货时,交付数量怎么切?
所以 dropship 的采购行拆分,本质上是在保护销售侧交付语义,不只是采购美观问题。
product_description_variants 为什么也会影响合并
_find_candidate() 还会检查 product_description_variants。
如果 procurement 带了不同的描述变体,系统会进一步对采购行名称做比较。
这常见于:
- 不同规格描述
- 针对客户需求附加的采购备注
- 某些属性变体导致采购展示名不同
业务上看,这些行似乎还是同一个产品。
但如果采购说明已经不同,强行并行会让供应商端看到一条含混不清的明细。
所以 Odoo 的设计很务实:
- 只要行级语义不同,就不要为“看起来整齐”去合并。
propagate_cancel 也是隐藏边界
采购行候选还会看 propagate_cancel。
这意味着系统不仅关心怎么买,还关心:
- 上游需求如果取消,这条采购行要不要同步取消
如果两条 procurement 的取消传播策略不同,把它们并成一条行会造成管理语义冲突。
这类字段平时用户几乎看不到,但对源码来说属于真正的边界条件。
业务上最容易误解的 5 个点
1. 以为“同产品同供应商”就该只剩一条采购行
那只是 PO 层面看起来合理,源码还要保护行级追踪和履约语义。
2. 看到同一张 PO 里重复产品,就认为系统没优化
很多时候恰恰是系统在避免错误合并。
3. 以为 dropship 采购只要能买到货就行
对 dropship 来说,交付归属比采购外观更重要。
4. 以为 orderpoint 只影响有没有采购
它还影响采购行能不能和别的需求继续合并。
5. 以为描述差异只是显示问题
在采购行层面,描述差异常常就是语义差异。
开发和实施时最该注意什么
1. 定制采购归并前,先区分“PO 合单”和“PO 行归并”
很多项目把这两层揉在一起改,最后会出现:
- 单据看起来更整齐了
- 但在途量、交付数量、来源追踪全乱了
2. 做 dropship 定制时,不要轻易去掉 sale_line_id 分组维度
这很可能会直接破坏销售行 delivered quantity 计算。
3. 多补货规则场景别轻易抹掉 orderpoint_id
否则后面你很难解释为什么某个 orderpoint 一直算不到已在途采购量。
4. 如果想减少采购行数量,优先统一上游语义,而不是硬改归并函数
比如:
- 统一产品描述策略
- 统一补货规则边界
- 统一取消传播策略
这样得到的简化是健康的。
一句话记忆法
PO 能合在一起,不代表采购行也该合在一起;Odoo 在行级拆分,通常是在保护补货追踪、销售履约和取消传播边界。
DISCUSSION
评论区