先说结论
Odoo 商品页上的属性切换,不是“选个颜色尺寸,AJAX 回个价格”这么简单。
_get_combination_info() 真正要回答的是:
- 这组属性到底对应哪个 variant;
- 价格在当前 pricelist 下是多少;
- 税怎么映射;
- 折扣和 compare price 怎么显示;
- 这个组合是否真实存在、是否可售;
- 是否要触发零价禁售等边界。
所以它的本质不是前端交互,而是:
把用户的属性选择,翻译成一份可直接驱动商品页的交易上下文。
一、为什么 Odoo 返回的是 combination info,而不是一个价格
这个方法返回的不只是 price,还包括:
product_iddisplay_nameis_combination_possiblelist_pricehas_discounted_pricecurrencytaxesprevent_zero_price_sale
这说明 Odoo 并不把它当成一个“前台价格接口”,而是一个:
- 组合语义解释器。
前端真正需要的不是某个数字,而是一份“当前组合在当前网站语境下意味着什么”的完整回答。
二、为什么先找 variant,再谈价格
在价格之前,系统先要确定:
- 当前 product_id 是否真的匹配所选属性;
- 如果不匹配,是否该重新
_get_variant_for_combination(); - 如果没有明确组合,是否取第一个 possible combination。
这一步非常关键。
因为价格、图片、库存、可售性这些后续行为,全都依赖于“当前卖的到底是谁”先被说清楚。
如果 variant 归位错了,后面所有显示都会跟着错位。
三、为什么价格不是基础价加上几个 extra 就结束
_get_additionnal_combination_info() 会继续把这些上下文拉进来:
- 当前
request.pricelist; - 购买数量;
- 计量单位;
- 当前 fiscal position;
- 当前网站币种;
- 价目表折扣策略。
这说明最终 price、list_price 并不是数据库静态值,而是:
- 在当前网站交易上下文里的求值结果。
所以那些“前端自己按属性额外价格加减”的定制,经常会和 Odoo 官方显示对不上。
四、为什么税一定要在这里就算掉
源码里会先过滤产品税,再经由 request.fiscal_position.map_tax() 做映射,然后再应用到展示价。
这一步的重要性在于:
- 商品页看到的价格,应该已经站在当前客户税务上下文里;
- 否则商品页、列表页、购物车页就可能三个价格三套说法。
也就是说,税不是 checkout 最后一步才补上的装饰,它本来就是组合解释的一部分。
五、为什么 is_combination_possible 比很多人想的更底层
很多人只盯着价格变化,却忽略组合合法性。
但如果组合本身就不存在,那么:
- 价格再漂亮也没意义;
- 不该加入购物车;
- 不该继续往付款流程走。
所以 is_combination_possible 其实比价格更底层,它先回答“这是不是一个真实可成立的商品组合”。
六、为什么零价保护也要放进 combination info
如果网站启用了 prevent_zero_price_sale,而当前组合算出来又是零价,Odoo 会把这个边界显式返回给前端。
这说明 Odoo 的思路不是让前端自己猜:
- 0 元是不是正常售卖;
- 是不是应该隐藏按钮;
- 是不是应该引导询价。
它直接把这个边界也纳入组合结果协议里。
七、最容易误解的几个点
误解 1:combination info 就是价格接口
不对。它是商品页的组合语义包。
误解 2:variant 归位只是展示问题
不对。它会影响价格、库存、可售性与购物车后续逻辑。
误解 3:税可以后面随便补
不对。这里就已经决定了前台价格语义。
误解 4:组合合法性和可下单是一回事
也不完全一样。购物车层还会有下一层校验。
最后一句
理解 Odoo 商品变体,重点不是页面上的按钮怎么切,而是看懂这条主链:
属性组合输入 → variant 归位 → pricelist 求值 → tax 映射 → 可售性与零价边界判断 → 输出统一 combination info。
看懂以后你就会知道,Odoo 网站商品变体不是一个小前端组件,而是一套交易语义求值器。
DISCUSSION
评论区