销售组合商品

Odoo 销售 Combo 为什么不是 Kit 换个名字:组合商品定价、子项展开与发票表现讲透

很多人看到 Odoo 的 combo product,会把它和 phantom kit 或可选产品混在一起。但标准销售源码里,combo 更像一种前台成交结构:主行价格归零、子项按 base price 分摊、开票时还能折叠显示。本文把这套定价与展示机制讲透。

销售
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说最容易搞混的一点

Odoo 里的 combo,不是 kit、不是报价模板里的 optional product,也不是“多个商品绑成一行”那么简单。

从标准销售源码看,combo 更像一套成交与展示结构

  • 订单上有一个 combo 主行
  • 实际选择的组合项会长成 linked lines
  • 主行本身显示价可以是 0
  • 真实价格会按各 combo 的 base_price 比例分摊到子项
  • 到发票时又可以折叠成 section + 明细的形式

所以它的重点不是库存展开,而是销售前台如何把组合商品卖得既清楚又可计价


这篇文章主要参考哪些源码

核心入口主要在:

  • /home/ubuntu/odoo-temp/addons/sale/models/sale_order_line.py
  • /home/ubuntu/odoo-temp/addons/sale/models/product_template.py
  • combo / configurator 相关模型与 portal 编辑辅助逻辑

最关键的源码信号有:

  • selected_combo_itemscombo_item_idlinked_line_id / linked_line_ids
  • combo 主行 _get_display_price() 直接返回 0
  • 子项价格由 _get_combo_item_display_price() 分摊计算
  • qty_to_invoice 对 combo 主行要看是否有任一子项可开票
  • _prepare_invoice_line() 对 combo 主行会生成 line_section

这些都说明 combo 不是库存 BOM 思维,而是销售界面的结构化组合成交语义。


第一层:为什么 combo 主行显示价会是 0

sale_order_line.py 里,源码写得非常直接:

  • 如果当前行的 product_type == 'combo'
  • _get_display_price() 就返回 0

很多人第一次看到会很困惑:

  • 组合商品不是应该有个套餐价吗
  • 为什么主产品自己反而没价格

答案是:

combo 主行的职责不是承载最终价格,而是承载“这是一个组合成交单元”的结构语义。

真正的价格会继续拆到各个 combo item line 上。

这和 kit 非常不一样。

kit 更强调履约展开;combo 更强调成交时的可选择结构与价格展示。


第二层:子项价格为什么不是各卖各的,而是按 base_price 分摊

_get_combo_item_display_price() 很值得看。

源码做的不是“每个子项直接用自己的产品售价”,而是:

  1. 先拿 combo 主产品的价格
  2. 再拿这组 combo 的 base_price
  3. 按比例把总价分摊到每个 combo 上
  4. 最后加上 combo item 的 extra price 以及 no_variant attribute 的 extra price

这说明 Odoo 在 combo 里表达的不是“多个产品凑一单”。

它更像:

先有一份组合商品总价,再把这个总价按组合结构合理拆到可展示的明细上。

所以 combo 的子项价并不是孤立商品价,而是组合总价中的份额


第三层:为什么源码还要处理分摊余差

源码里有一段很细:

  • 先按比例算出每个 combo price
  • 再比较这些 price 相加是否等于 combo 产品总价
  • 如果有 delta,就补到最后一个 combo 上

这一步很有工程味道。

因为金额分摊一旦遇到币种精度、舍入规则,就可能出现:

  • 子项合计 99.99
  • 但组合总价是 100.00

如果不补差,最后:

  • 销售明细之和
  • 发票明细之和
  • 税基计算

都会开始漂。

所以这段 delta 修正不是小细节,而是保证组合商品金额闭合的关键。


第四层:combo 为什么和 optional products 不是一回事

两者都可能让销售看到“主商品旁边还有附加内容”,但语义完全不同。

optional products

  • 更像推荐加购
  • 客户可以选,也可以不选
  • 没选就不会进入正式成交结构

combo products

  • 是主商品本身的组成方式
  • 选中的 combo item 会形成 linked sale lines
  • 价格和后续开票都要围绕这些子项展开

所以 optional 是推荐层,combo 是组成层。

把两者混了,前台展示、价格表达和发票语义都会乱。


第五层:为什么 combo 的可开票数量还要看子项

_compute_qty_to_invoice() 对 combo 主行有一段专门逻辑:

  • 先看 linked 的 combo item lines 里
  • 是否至少有一条真的有 qty_to_invoice
  • 只有这样,combo 主行才认为自己可开票

这说明 combo 主行虽然是结构锚点,但真正推动开票边界的仍然是子项实际可开票状态。

这是很合理的设计,因为客户买的是组合,但系统不能忽略组合内部具体可开票内容。

所以 combo 的主行和子项,不是主从显示关系,而是:

  • 主行提供结构
  • 子项提供计量与计价依据

第六层:为什么 combo 到发票时会变成 section

_prepare_invoice_line() 对 combo 主行的处理尤其有意思。

当销售行本身是 combo 产品时,系统不会直接生成一条普通 product invoice line,而是生成:

  • display_type = 'line_section'
  • 名称类似 组合商品名 x 数量
  • 再把 collapse_pricescollapse_composition 等表现信息一起带过去

这说明 Odoo 不想让 combo 在发票上看起来像一个普通商品行。

它想表达的是:

这是一组组合明细的标题行,后面子项才是可展开 / 可折叠的具体组成。

也就是说,combo 连发票表现都是结构化的,而不是平铺成几行零散商品。


第七层:combo 和 kit 的根本差异到底在哪

我自己的判断标准很简单:

kit 更关心

  • 库存履约怎么展开
  • 组件如何出入库
  • BOM explode 后库存对象如何流转

combo 更关心

  • 销售前台怎么让客户理解组合选择
  • 组合总价怎么拆到各项
  • 发票上怎么既保持组合语义又保留明细

所以如果你的核心问题是:

  • 仓库该动哪些组件

那你更该看 kit / BOM。

如果你的核心问题是:

  • 销售怎么卖一组可配置组合并保持价格表达清晰

那 combo 才是正确方向。


新手最容易误解的 5 件事

1. 以为 combo 主行没有价格就是配置错

不是。标准设计就是让主行显示价归零。

2. 以为子项价格等于各自商品售价

不是。它们通常来自组合总价分摊。

3. 以为 combo 和 optional product 差不多

不是。一个是组成结构,一个是推荐加购。

4. 以为 combo 和 kit 差不多

不是。一个更偏销售前台,一个更偏库存履约。

5. 以为发票上 combo 会被压成普通单行

标准实现会保留 section + 明细结构。


实战里什么时候该选 combo

我会在这些场景优先考虑 combo:

  • 销售需要把一组可选组成卖成一个完整方案
  • 客户需要看清组合构成,但又不想面对杂乱单价
  • 发票上希望保留“这是一个组合”而不是纯散件列表

如果只是仓库层的组件拆解,combo 往往不是第一选择。


一句话记忆法

Odoo combo 不是库存 kit 的别名,而是一种面向销售成交与发票展示的组合商品结构:主行承载结构,子项承载价格分摊与可开票依据。

DISCUSSION

评论区

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