销售定价

Odoo 销售价格为什么总和肉眼不一样:价目表、折扣、税和 Fiscal Position 的计算顺序讲透

很多人看到销售单价格不对,第一反应是价目表错了;但标准 Odoo 里,真正影响结果的往往是整条顺序:先命中 pricelist rule,后生成 price_unit / discount,再映射 tax,最后按 fiscal position 与税显示口径落总价。本文把这条计算顺序讲透。

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

先抓主线

销售单上的最终金额,通常不是“价目表给一个价,税再顺手一算”这么简单。

更准确的顺序是:

  1. 先找出适用的价目表规则
  2. 再决定行上的 price_unitdiscount 怎么显示
  3. 再计算 tax_ids,必要时经过 fiscal position 映射
  4. 最后才按税的含税 / 未税口径汇总小计、税额与总价

只要这条顺序没弄清,团队就会经常出现这些争论:

  • 为什么 price_unit 看着不对
  • 为什么折扣看着像没生效
  • 为什么税率和产品税不一样
  • 为什么同一个客户换个地址总价就变了

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

核心参考包括:

  • /home/ubuntu/odoo-temp/addons/product/models/product_pricelist.py
  • /home/ubuntu/odoo-temp/addons/sale/models/sale_order_line.py
  • /home/ubuntu/odoo-temp/addons/sale/models/sale_order.py
  • /home/ubuntu/odoo-temp/addons/account/models/account_tax.py

最值得抓住的源码信号有:

  • product.pricelist._compute_price_rule() 先找 suitable rule
  • 销售行上 price_unitdiscounttax_ids 各自独立计算
  • tax_ids 不是产品税原样照搬,而会经过 company 过滤和 fiscal position 映射
  • 最终价格显示还会受税是否 price-included 影响

所以“看到一个数不对”时,不能只怪一个字段,必须按流水线看。


第一层:价目表不是直接给答案,而是先选规则

product_pricelist.py_compute_price_rule() 里,Odoo 的第一步不是直接返回价格,而是:

  • 根据产品、模板、分类、日期、数量、单位等条件
  • 先找出一条 suitable rule

这一点非常关键。

因为它说明 pricelist 的第一职责不是“存价格”,而是:

在当前上下文里,先决定哪条规则有资格生效。

所以你看到价格不对时,第一反应不该是“算错了”,而应该是:

  • 命中了哪条 rule?
  • 有没有因为数量门槛切到另一条?
  • 是否因为单位换算后落入了不同 min_quantity 区间?
  • 生效日期是不是已经变了?

很多价格问题,其实在“选中哪条规则”这一步就已经决定了。


第二层:price_unitdiscount 不是同一个意思

销售行上同时有:

  • price_unit
  • discount

很多用户肉眼看到这两个字段时,会以为其中一个一定是多余的。

其实不是。

它们表达的是两个层次:

  • price_unit:当前要拿来计价的基础单价
  • discount:这张销售行要不要把一部分优惠显式展示为折扣百分比

这就是为什么同样一个促销效果,可能在不同配置下表现为:

  • 单价直接变低
  • 单价维持原价,但 discount 增加
  • 或两者混合,取决于显示策略和规则来源

所以价格看起来“怪”,不一定是总额错了,可能只是优惠被表达在不同字段里


第三层:税不是产品默认值原样照搬

sale_order_line.py 里,tax_ids 的计算并不是“拿产品税字段直接填上”。

标准思路通常是:

  1. 先取产品的销售税基础集合
  2. 按公司过滤
  3. 再经过订单上的 fiscal_position_id 映射

这意味着:

销售行上的税,是“产品税候选集合”经过商业上下文重写后的结果。

所以这些现象都很正常:

  • 同一个产品,卖给不同客户税不一样
  • 同一个客户,换收货国家税不一样
  • 同一类商品,B2B 和 B2C 看起来税口径不同

这里不是系统飘了,而是 fiscal position 正在起作用。


第四层:Fiscal Position 改的不是只有税率,而是整个税语义入口

很多人把 fiscal position 简化理解成“自动换税率”。

在销售定价链里,这种理解太浅了。

它真正做的是:

  • 决定哪些税被替换
  • 决定 price-included 的解释链是否改变
  • 进而影响行小计、税额和总额的展示口径
  • 同时也为后续发票与会计落账埋下上下文

所以一个典型误区是:

  • 看到产品模板上税是 13%
  • 就认定销售单行也应该是 13%

标准 Odoo 从来不是这么机械。

销售单看到的,是产品税 + 客户上下文 + 国家 / 公司 / fiscal position 映射之后的结果


第五层:为什么含税价最容易让人误判“价目表失效了”

因为用户通常会用肉眼盯住一个数字:

  • 页面上的单价
  • 小计
  • 总税额
  • 含税总价

但 Odoo 在底层会把“单价的商业语义”和“税的会计语义”拆开。

如果税是 price-included:

  • 你看到的单价可能已经带税
  • 但行小计和税额在内部还会继续拆分
  • fiscal position 再一映射,拆分结果还会变

于是用户就会觉得:

  • “为什么我明明配了一个含税价,最后未税小计却不是我脑子里的那个数?”

答案通常不是 pricelist 没生效,而是:

价格规则生效了,但税语义又把这个商业价格重新解释了一遍。


第六层:为什么换客户、换收货地址、换公司会让价格“突然变了”

因为这三类动作常常同时影响:

  • 适用的 pricelist
  • 适用的 currency
  • 适用的 fiscal position
  • 适用的 tax 集合

也就是说,销售价格不是静态数字,而是上下文计算结果。

所以当用户说:

  • “同一个产品刚才还是这个价,为什么换个客户就不一样?”

标准答案通常不是“系统错了”,而是:

  • pricelist rule 可能变了
  • fiscal position 可能变了
  • 税包含口径也可能一起变了

这是设计使然,不是偶发异常。


新手最容易误解的 5 件事

1. 以为 pricelist 直接存最终售价

不是。它先决定命中哪条 rule,再算价格。

2. 以为 price_unitdiscount 只能二选一

不是。它们是在表达不同层次的优惠语义。

3. 以为税一定等于产品默认税

不是。销售上下文会改写它。

4. 以为 Fiscal Position 只改税率数字

不是。它改的是整条税映射入口。

5. 以为总价变化就一定是价目表配置错

很多时候是税显示口径和 fiscal position 一起动了。


实战调试顺序

如果销售单金额看起来“不对”,建议按这个顺序排:

  1. 当前订单命中的 pricelist 是谁
  2. 对应产品命中了哪条 pricelist rule
  3. 行上的 price_unitdiscount 各是多少
  4. tax_ids 是哪些税
  5. fiscal_position_id 是否做了映射
  6. 相关税是否 price-included
  7. 最终小计 / 税额 / 总价分别怎么拆出来

你会发现,这比盯着一个最终金额争论有效得多。


一句话记忆法

Odoo 销售价格不是一个字段算出来的,而是“先命中价目表规则,再生成单价 / 折扣,再映射税,再汇总含未税口径”的一条定价流水线。

DISCUSSION

评论区

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