先说最容易搞混的一点
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_items、combo_item_id、linked_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() 很值得看。
源码做的不是“每个子项直接用自己的产品售价”,而是:
- 先拿 combo 主产品的价格
- 再拿这组 combo 的
base_price - 按比例把总价分摊到每个 combo 上
- 最后加上 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_prices、collapse_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
评论区