先抓主线
销售单上的最终金额,通常不是“价目表给一个价,税再顺手一算”这么简单。
更准确的顺序是:
- 先找出适用的价目表规则
- 再决定行上的
price_unit和discount怎么显示 - 再计算
tax_ids,必要时经过 fiscal position 映射 - 最后才按税的含税 / 未税口径汇总小计、税额与总价
只要这条顺序没弄清,团队就会经常出现这些争论:
- 为什么 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_unit、discount、tax_ids各自独立计算 tax_ids不是产品税原样照搬,而会经过 company 过滤和 fiscal position 映射- 最终价格显示还会受税是否 price-included 影响
所以“看到一个数不对”时,不能只怪一个字段,必须按流水线看。
第一层:价目表不是直接给答案,而是先选规则
在 product_pricelist.py 的 _compute_price_rule() 里,Odoo 的第一步不是直接返回价格,而是:
- 根据产品、模板、分类、日期、数量、单位等条件
- 先找出一条 suitable rule
这一点非常关键。
因为它说明 pricelist 的第一职责不是“存价格”,而是:
在当前上下文里,先决定哪条规则有资格生效。
所以你看到价格不对时,第一反应不该是“算错了”,而应该是:
- 命中了哪条 rule?
- 有没有因为数量门槛切到另一条?
- 是否因为单位换算后落入了不同
min_quantity区间? - 生效日期是不是已经变了?
很多价格问题,其实在“选中哪条规则”这一步就已经决定了。
第二层:price_unit 和 discount 不是同一个意思
销售行上同时有:
price_unitdiscount
很多用户肉眼看到这两个字段时,会以为其中一个一定是多余的。
其实不是。
它们表达的是两个层次:
price_unit:当前要拿来计价的基础单价discount:这张销售行要不要把一部分优惠显式展示为折扣百分比
这就是为什么同样一个促销效果,可能在不同配置下表现为:
- 单价直接变低
- 单价维持原价,但 discount 增加
- 或两者混合,取决于显示策略和规则来源
所以价格看起来“怪”,不一定是总额错了,可能只是优惠被表达在不同字段里。
第三层:税不是产品默认值原样照搬
在 sale_order_line.py 里,tax_ids 的计算并不是“拿产品税字段直接填上”。
标准思路通常是:
- 先取产品的销售税基础集合
- 按公司过滤
- 再经过订单上的
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_unit 和 discount 只能二选一
不是。它们是在表达不同层次的优惠语义。
3. 以为税一定等于产品默认税
不是。销售上下文会改写它。
4. 以为 Fiscal Position 只改税率数字
不是。它改的是整条税映射入口。
5. 以为总价变化就一定是价目表配置错
很多时候是税显示口径和 fiscal position 一起动了。
实战调试顺序
如果销售单金额看起来“不对”,建议按这个顺序排:
- 当前订单命中的 pricelist 是谁
- 对应产品命中了哪条 pricelist rule
- 行上的
price_unit与discount各是多少 tax_ids是哪些税fiscal_position_id是否做了映射- 相关税是否 price-included
- 最终小计 / 税额 / 总价分别怎么拆出来
你会发现,这比盯着一个最终金额争论有效得多。
一句话记忆法
Odoo 销售价格不是一个字段算出来的,而是“先命中价目表规则,再生成单价 / 折扣,再映射税,再汇总含未税口径”的一条定价流水线。
DISCUSSION
评论区