采购行归并

Odoo 为什么同一产品也不一定合成一条采购行:Orderpoint、Dropship 与采购行归并边界讲透

很多人知道 Odoo 会“合单”,却忽略了更细的一层:即便进了同一张 PO,也不代表一定会合成同一条采购行。本文专讲采购行归并边界,解释为什么同产品、同供应商也会被拆开。

采购
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

很多实施人员已经知道:

  • 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_cancel
  • orderpoint_id
  • 是否要求强制同 UoM(force_uom
  • product_description_variants
  • 以及已有行的名称上下文

这说明 Odoo 不是简单用“同产品同供应商”做唯一判断。

它真正担心的是:

  • 行级上下文是否仍然一致

如果不一致,就算 PO 层面可以共存,也不该继续混成一行。


为什么 orderpoint_id 会阻止采购行随便合并

_find_candidate() 里有一句非常关键的条件:

  • 当当前 procurement 带 orderpoint_id 且没有 move_dest_ids
  • 只允许并到同一个 orderpoint_id 或空 orderpoint_id 的采购行

这背后是个典型的补货语义问题。

假设你有两个补货规则:

  • 同产品
  • 同供应商
  • 但服务于不同位置或不同补货来源

如果系统把它们粗暴合成一行,后面就会出现三个难题:

  1. 在途量应该记给哪个 orderpoint?
  2. 这个采购行未来到货时,谁算完成补货?
  3. 某个 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

评论区

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