在销售实施现场,商品配置器很容易被当成一个“前端小工具”:
- 选几个规格;
- 数量改一下;
- 看一下价格;
- 点确认。
但真正读 Odoo 源码会发现,它并不是一个孤立弹窗,而是一条跨前后端的接力链:
- 前端维护当前属性组合;
- 组合一变就回服务器请求最新可行性和价格;
- 可选产品和排斥关系跟着组合联动;
- 最后再把主产品与可选产品一起交给销售行保存。
所以它真正要解决的问题不是“怎么弹个框”,而是:
在用户还没落库前,如何让组合、价格、可选项与销售单业务语义保持一致。
一、前端模型保存的不是 SKU,而是“模板 + 已选属性状态”
product_product.js 里的 ProductProduct 很值得注意。
它的关键字段不是“一个现成 product id”,而是:
product_tmpl_idptalsselectedPtavIdsselectedNoVariantPtavIdsselectedCustomPtavs
这说明配置器前端阶段真正处理的对象更接近“配置中的产品模板”,而不是最终成品 SKU。
为什么?因为在用户没选完之前,系统经常还不知道最终应该对应哪条 product.product。
尤其碰到:
dynamic变体;no_variant属性;- 自定义属性值;
前端只能先维护一份组合状态,最终再决定是否需要创建或定位实际变体。
二、价格不是本地算出来的,而是每次组合变化都回服务器确认
ProductConfiguratorDialog 里最关键的方法之一是 _updateCombination()。
每当用户:
- 改数量;
- 切 UoM;
- 换属性值;
配置器都会调 RPC 到 /sale/product_configurator/update_combination。
这意味着价格并不是前端用几条规则临时算的,而是后端在当前上下文下重新确认。
为什么必须这样?因为销售价格通常同时受很多东西影响:
- 价目表;
- 日期;
- 公司;
- UoM;
- 属性附加价;
- 可选产品依赖。
如果全在前端硬算,二开一多就特别容易和销售行真实价格脱节。
所以官方选择了更稳的路线:
组合状态由前端暂存,价格结论由服务器回写。
三、排斥逻辑不是 UI 装饰,而是在维护“可成交组合”
很多人看到 _checkExclusions() 会以为这只是界面禁用几个按钮。
其实不是。
它同时会处理:
- 组合自身的 exclusions;
- 父产品带下来的 parent exclusions;
archived_combinations;- 子可选产品的递归排斥。
也就是说,配置器并不是在帮你“凑一个能显示的组合”,而是在提前维护:
- 这个组合是否能合法存在;
- 这个组合是否已归档不能再选;
- 父产品的选择会不会让子产品失效。
这点非常关键。
因为销售配置器一旦允许用户选出一个最终无法落库或无法报价的组合,后面保存时就会出现更难解释的异常。
四、可选产品不是静态推荐,而是跟着主组合实时重算
_addProduct() 和 _getOptionalProducts() 这组逻辑说明,可选产品列表不是写死在模板上的一份静态推荐。
前端每次把主产品加进来后,都会带着:
- 当前已选 PTAV 组合;
- 父级组合信息;
- 价目表 / 公司 / 日期等上下文;
向服务端重新拉一轮 optional products。
所以真正决定“此刻该推荐哪些可选项”的,不只是商品模板本身,而是:
- 主产品选成了什么;
- 现在在哪家公司;
- 用什么价格环境;
- 是否已有父级约束。
这就是为什么有些实施现场会觉得“同一个模板怎么这次能推这个配件、下次又不推”。
不是前端抽风,而是组合上下文真的变了。
五、动态变体说明“确认按钮”本身也是业务动作
onConfirm() 里还有个很重要的步骤:
如果产品当前没有 id,但存在 create_variant === "dynamic" 的属性线,前端会先调 _createProduct(product)。
这意味着配置器的确认,并不只是“把选择结果提交回销售行”。
它有时还会先触发一条新的 product.product 生成。
所以确认按钮实际上可能做两类事情:
- 生成动态变体;
- 再把主产品和可选产品交给
save()写回业务。
这也解释了为什么配置器很多问题不能只盯销售行。
如果动态变体创建失败,销售行保存自然也会一起出问题。
六、配置器和销售单行的边界:谁负责什么
从 sale_order.py 看,销售单真正负责的是:
- 最终行的创建 / 更新;
- 价格重算;
- 组合行(比如 combo item line)的展开;
- 后续税、开票、交付等业务语义。
配置器负责的是落库前的组合协商。
换句话说:
- 配置器解决“现在这个选法是否合法、多少钱、带哪些附属项”;
- 销售行解决“这组结果如何成为正式订单行”。
把这两个层次混在一起,是很多二开的源头问题。
七、实施与开发最容易踩的坑
误区 1:前端自己算最终价格
短期看很快,长期几乎一定和后端价目表逻辑分叉。
误区 2:把可选产品当静态附件
其实它们会随着主组合变化重新计算。
误区 3:忽略 dynamic / no_variant / custom value 的差异
这三类属性在是否生成真实变体、是否只影响价格、是否需要带自定义值上,语义完全不同。
误区 4:保存失败就只查 sale.order.line
很多问题其实发生在组合更新 RPC 或动态变体创建前一步。
八、排错时该先看什么
如果现场出现“价格不对”“可选项不对”“明明选了却保存失败”,建议按这个顺序看:
- 当前 PTAV 组合有没有被
_checkExclusions()判成不可行; _updateCombination()返回的价格与产品 id 是什么;- 是否存在
dynamic变体需要先创建; - 保存时传回了哪些主产品 / 可选产品对象;
- 最后才看销售行落库逻辑。
这样查通常比直接翻 sale.order.line.create() 更快。
九、结论
Odoo 商品配置器之所以能同时处理变体、附加价、可选产品和动态创建,不是因为弹窗写得花,而是因为它把配置过程做成了一条前后端接力链:
- 前端维护模板级组合状态;
- 服务器持续回写价格与可行性;
- 可选产品跟着组合联动;
- 最终再把结果交给销售单行落地。
所以真正该记住的一句话是:
商品配置器不是一个“选规格 UI”,而是销售订单落库前的一次业务预演。
DISCUSSION
评论区