先说结论
在 Odoo 里,“价格不对”通常不是公式算错,而是你以为系统命中了 A,实际上系统先选了另一张 pricelist,或先停在了另一条规则上。
标准逻辑至少分两层:
- 先选哪张价目表
- 再在这张价目表里命中哪条规则
而这两层都不是拍脑袋决定的。
如果你只看某条 formula rule 的 price_discount、price_round、price_surcharge,但没有同时看:
- pricelist 自身
sequence country_group_ids- rule 的
applied_on min_quantity- product / template / category 适配范围
那你就很容易把“命中错了”误判成“公式错了”。
这篇文章主要参考哪些源码
核心参考文件包括:
/home/ubuntu/odoo-temp/addons/product/models/product_pricelist.py/home/ubuntu/odoo-temp/addons/product/models/product_pricelist_item.py
最关键的源码信号有:
product.pricelist的_order = "sequence, id, name"_get_country_pricelist_multi()会先找匹配country_group_ids的价目表,再考虑 fallback_compute_price_rule()先调用_get_applicable_rules()拉出候选规则,再逐条找第一条适用规则product.pricelist.item的_order = "applied_on, min_quantity desc, categ_id desc, id desc"- formula 规则的计算顺序是:base -> discount/markup -> rounding -> surcharge -> min/max margin
这些点决定了“选哪张表”和“命中哪条 rule”的完整路径。
第一层:先别急着看规则,系统可能连价目表都没选到你以为的那张
很多排查习惯一上来就打开 pricelist line。
但 product.pricelist 先要解决的是:
当前客户 / 国家 / 公司 / 默认属性,应该落到哪张 pricelist?
_get_country_pricelist_multi() 的逻辑就说明,国家组在这一步已经参与决策。
它会优先考虑:
- 当前上下文国家
- 给定国家列表
country_group_ids匹配的 pricelist- 没有 country group 的 fallback pricelist
- partner 默认 pricelist 属性或公司默认配置
这意味着一个典型误区是:
- 你以为客户用了“海外批发价目表”
- 实际上因为国家组、默认属性或 fallback 逻辑,系统拿到的是另一张表
如果第一层就看错,第二层再精细地研究公式也没用。
第二层:规则优先级不是“谁写在上面谁先算”,而是按模型排序和搜索结果来
product.pricelist.item 的 _order 明确是:
applied_onmin_quantity desccateg_id descid desc
再配合 _compute_price_rule() 的循环逻辑:
- 系统先 search 出候选规则
- 再按排序结果,从前到后找第一条
_is_applicable_for(...)为真的规则 - 找到就停
这说明 rule priority 不是“所有命中的规则一起算,再挑最优价”,而是:
按既定顺序找到第一条适用规则,然后停止。
这是很多人理解错的核心原因。
Odoo 的 pricelist 不是一个“并行比较器”,更像一个“有序命中器”。
第三层:为什么 variant、product、category、global 看起来像同级,实际不是同级
applied_on 取值包括:
0_product_variant1_product2_product_category3_global
注意它们前面的数字不是装饰。
因为 _order 第一位就是 applied_on,所以排序天然表达出一层优先级语义:
- 变体级规则优先于模板级
- 模板级优先于品类级
- 品类级优先于全局级
这很合理。
因为系统假设:
- 越具体的规则,越应该先尝试
- 越泛化的规则,越应该做兜底
所以当你发现“全局规则没生效”,很多时候不是它坏了,而是前面已经有一条更具体的规则先把你截住了。
第四层:min_quantity 的优先级往往比很多人想的更高
同一类适用范围下,min_quantity desc 排在前面。
这意味着对同一产品来说:
- 满足 100 件门槛的规则
- 会先于满足 10 件门槛的规则被尝试
这不是后来比较出来的“最优折扣”,而是搜索顺序直接决定的。
所以排查批量折扣异常时,最应该先看的是:
- 当前数量是否已经跨过更高门槛
- 某条高门槛规则是否比你以为的那条先被命中
很多实施现场会把这类问题解释成:
- 系统没取最优惠价格
但 Odoo 标准逻辑本来就不是“自动替你挑最低价算法”,而是“按规则顺序命中第一条适用项”。
第五层:formula 真正的计算顺序,和很多人口头理解的不一样
在 product.pricelist.item 里,formula 相关字段包括:
baseprice_discountprice_roundprice_surchargeprice_min_marginprice_max_margin
源码注释与 rule tip 的说明都指向同一件事:
- 先拿 base
- 再做 discount / markup
- 再做 rounding
- 再加 surcharge
- 最后受 min/max margin 约束
所以几个常见误解要先丢掉:
误解 1:extra fee 先加再四舍五入
标准逻辑恰好相反,rounding 在 surcharge 之前。
误解 2:formula 总是基于销售价
不一定。base 还可以来自:
list_pricestandard_price- 另一张 pricelist
误解 3:markup 和 discount 是两个独立世界
如果 base 是 standard_price,显示语义更像 markup;否则更像 discount。
这就是为什么同样一个公式界面,业务解释会完全不同。
第六层:country group 决定“用哪张表”,不是 rule 里的局部附加条件
很多人会把 country group 想成 pricelist item 上的额外过滤。
标准源码不是这么做的。
country_group_ids 在 pricelist 头上,它首先参与的是:
- 哪张 pricelist 可被当前国家命中
这意味着国家组的正确理解应该是:
先把客户导向一张更合适的价目表,再在那张表里走规则匹配。
它不是“在一张全球通用表里顺便再加一个国家条件”。
这点特别重要,因为很多实施设计错的根源就是:
- 想用一张超级大表覆盖所有国家、币种、规则层级
最后排查时就会极度痛苦。
对于跨国家定价,很多时候更可维护的设计是:
- 先用 country groups 拆表
- 再在表内做 product/category/quantity 规则
新手最容易误解的 6 件事
1. 价目表问题先看公式
不对,先确认系统到底选了哪张 pricelist。
2. 所有命中规则会一起比较,系统自动选最好的一条
不是,标准逻辑是按顺序命中第一条适用规则。
3. global rule 失效就是 bug
很多时候只是被更具体的 rule 提前拦截。
4. min_quantity 只是附带条件,不影响优先级
不对,在同层规则里它直接影响排序。
5. country group 是 rule 条件
标准上它首先是 pricelist 选择条件。
6. formula 里 surcharge、rounding、discount 顺序无所谓
非常有所谓,顺序错了金额就会偏。
实战里我建议这样排查价目表
第一步:先确认是哪张 pricelist
看:
- partner 默认属性
- company 默认值
- country group 是否命中
- 当前上下文是否带国家代码
第二步:再看候选规则排序
重点看:
- applied_on 的具体程度
- min_quantity 门槛
- category / product / variant 覆盖范围
第三步:最后才看 formula
确认:
- base 来自哪里
- discount / markup 是正还是负
- rounding 与 surcharge 顺序是否符合预期
- margin 限制有没有截断结果
这个顺序反过来做,通常会把自己绕晕。
最后一句话
Odoo 的 pricelist 不是“写了一堆折扣公式,系统帮你自动找最优解”。
它更像:
先把客户分配到一张表,再在这张表里按有序规则命中第一条可用规则。
理解了这句话,你排查价格问题时就不会只盯公式,而会先看:
- 表选对没有
- 规则顺序对没有
- 公式只是最后一步有没有算对
DISCUSSION
评论区