先说结论
Odoo 采购行上的税,并不是简单地把产品 supplier_taxes_id 复制下来。
在 /home/ubuntu/odoo-temp/addons/purchase/models/purchase_order_line.py 里,_compute_tax_id() 的逻辑很明确:
- 先拿产品的
supplier_taxes_id - 先按公司做
_filter_taxes_by_company(...) - 再通过 fiscal position 执行
map_tax(...) - 最终结果才写到采购行
tax_ids
所以:
采购税 = 供应商税基础集合 × 公司过滤 × fiscal position 映射,而不是产品税字段原样照搬。
为什么“产品税”和“采购行税”常常不一样
因为这两者本来就不处在同一层。
产品上的 supplier_taxes_id
它更像:
- 这个商品从采购视角看,默认可能适用哪些进项税
- 一种“候选基础集”
采购行上的 tax_ids
它更像:
- 这张采购单、这个公司、这个供应商语境下,最终真正要用的税
一旦你有多公司、多地区、税映射场景,这两者不完全相同才是常态。
源码链路:先过滤公司,再映射 fiscal position
_compute_tax_id() 里有两步特别关键。
第一步:_filter_taxes_by_company(line.company_id)
这一步解决的是“同一个产品上可能挂了跨公司税”的问题。
如果不先过滤公司,一个多公司环境里的产品税定义很容易串台。
所以 Odoo 不会盲信产品税配置,而是先问:
- 当前采购行属于哪个公司
- 当前公司下哪些税才有资格参与后续计算
第二步:fpos.map_tax(taxes)
在得到公司可用税后,Odoo 再通过 fiscal position 做税映射。
这一步解决的是:
- 特定供应商
- 特定国家 / 地区
- 特定税务位置
下,默认税是否要替换成另一套税。
所以你看到采购行税和产品税不一致,第一反应不该是“系统算错了”,而应该是:
是不是 fiscal position 正在刻意改税。
fiscal position 不是只在销售侧重要
很多顾问对 fiscal position 的直觉来自销售和开票,但采购里它同样很关键。
在 purchase.order 上,fiscal_position_id 会根据供应商去取默认值;而采购行算税时,又继续引用这个 fiscal position。
换句话说:
- 单头决定采购单的税务语境
- 行上根据这个语境完成具体税落地
这就是为什么采购税问题,常常不能只看产品卡片。
价格含税 / 不含税的“错觉”也常从这里来
采购行价格看起来不对,很多人会先怀疑汇率或价目。其实税映射本身就能制造非常强的“价格错觉”。
因为一旦 tax_ids 变了,后续:
- 含税 / 不含税换算
- 采购行显示金额
- Vendor Bill 上的税表达
都会跟着变化。
所以有时你以为是“取价不对”,本质上却是“取到的税不是你脑子里那套税”。
新手最容易忽略的排查顺序
如果采购行税不对,建议按这个顺序查:
- 产品
supplier_taxes_id是什么 - 这些税里哪些属于当前公司
- 采购单
fiscal_position_id是什么 - fiscal position 把哪些税映射成了哪些税
- 最终采购行
tax_ids是什么
这个顺序比直接盯采购行一格格点来点去高效得多。
和已有会计税文章的边界
这篇不是在讲会计凭证层如何落税,也不是讲 fiscal position 的全景概念,而是专门聚焦:
- 采购单行如何从产品默认税走到最终
tax_ids
它回答的是采购顾问和实施同学最常见的一句抱怨:
“为什么这张 PO 上的税和产品上配置的不一样?”
一句话记忆
在 Odoo 采购里,产品税只是起点;真正落到采购行上的税,是公司过滤和 fiscal position 映射后的结果。
DISCUSSION
评论区