会计映射

Odoo Fiscal Position 为什么总像“自动猜错了”:delivery 地址、VAT、国家组与手工覆盖优先级讲透

很多人知道 Fiscal Position 会映射税和科目,却不知道它的自动识别优先级其实很讲究。本文基于 `/home/ubuntu/odoo-temp/addons/account/models/partner.py` 与 `account_move.py` 拆解手工指定、delivery 地址、VAT、国家/州/邮编条件是怎样一起决策的。

会计
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多人一提 Fiscal Position,第一反应是:

  • 它会改税;
  • 它也可能改收入/费用/往来科目。

但在实际项目里,更常见的困惑不是“它能改什么”,而是:

  • 它到底为什么选中了这条 fiscal position;
  • 明明客户是 A 国,怎么没套 A 国规则;
  • 为什么换了 delivery address,税映射突然就变了;
  • 为什么手工选了一条,自动规则又不生效了。

如果你去看 /home/ubuntu/odoo-temp/addons/account/models/partner.py 里的 account.fiscal.position._get_fiscal_position()/home/ubuntu/odoo-temp/addons/account/models/account_move.py 里的 _compute_fiscal_position_id(),会发现 Odoo 的自动识别并不是“按 partner.country_id 找一个匹配项”这么粗糙。

它真正的优先级,大致是:

  1. 特定单据类型的硬编码 fiscal position(例如采购收据等特殊场景);
  2. 手工指定的 property_account_position_id
  3. delivery partner 的匹配条件
  4. 国家 / 国家组 / 州 / 邮编 / VAT requirement 的组合过滤
  5. 按公司层级与 sequence 决定谁先中标

这是一套“先看人工覆盖,再看履约地址,再看规则命中顺序”的体系。


一、为什么 Odoo 先看 delivery 地址,而不是只看开票伙伴

account_move._compute_fiscal_position_id() 里会先取:

  • partner_id
  • partner_shipping_id
  • 或 partner 上默认的 delivery 地址

然后把 partnerdelivery 一起传给 _get_fiscal_position(partner, delivery=delivery_partner)

这背后的业务意思很明确:

Fiscal Position 关注的不只是“谁买单”,还关心“货/服务被交付到哪里”。

这在跨境交易里尤其关键。

因为税务判断很多时候并不只看客户主档国家,而要看:

  • 货物送达国家;
  • 是否是欧盟内交易;
  • 客户 VAT 情况;
  • 甚至州和邮编范围。

所以 delivery 地址不是附属信息,而是自动识别的核心输入之一。


二、为什么同 VAT 前缀的欧盟场景会退回 invoicing partner

源码里有一段很容易被忽略:

  • 如果 company 和 partner 都有 VAT;
  • 且都在 EU;
  • 且 VAT 前缀相同;
  • 那么即使传了 delivery,也可能回到 invoicing partner 视角。

它表达的是一种细分规则:

并不是所有跨地址情形都按 delivery 判,某些同 VAT 前缀、同法域语境下,系统会认为应按开票主体看。

所以很多“明明 delivery 在别处,为什么 fiscal position 没变”的案例,不能只看收货地址本身,还得看 company VAT 与 partner VAT 的组合关系。


三、手工指定为什么永远优先

_get_fiscal_position() 里,源码会先检查:

  • delivery.property_account_position_id
  • partner.property_account_position_id

只要手工属性上有值,直接返回,不再走自动匹配。

这意味着:

手工指定不是建议值,而是强覆盖。

所以如果你在客户、发货地址或某个商业伙伴上手工设了 fiscal position,就不要再指望 auto-apply 规则参与竞争。

项目里很多“自动规则失效”其实都不是规则写错,而是被人工属性提前截胡了。


四、auto_apply 真正比较的不是一个国家字段,而是一组验证函数

Odoo 并不是把所有 auto_apply fiscal position 扫一遍后简单比 country_id

_get_fpos_validation_functions() 里,源码拆成多个验证条件:

  • 是否要求 VAT,且 partner VAT 是否有效;
  • 邮编是否落在 zip_from ~ zip_to
  • 州是否命中;
  • 国家是否命中;
  • 国家组是否命中,且不在排除州列表中。

也就是说,一条 fiscal position 是否生效,实际上是:

多组条件全部通过,才算命中。

所以你看到“国家对了却没选中”,完全可能是因为:

  • VAT required 没满足;
  • 州不对;
  • 邮编不在范围内;
  • 国家组排除了该州。

五、为什么 sequence 和公司层级会影响最终结果

源码里 _get_first_matching_fpos() 会先排序,再返回第一条满足全部条件的 fiscal position。

排序逻辑不是随便的,而是:

  • 公司越具体的优先;
  • 然后再按 sequence

这意味着如果你配了多条都能命中的 fiscal position,Odoo 不是随机选一条,而是优先:

  1. 更贴近当前公司的配置;
  2. 再按 sequence 决定先后。

所以 sequence 不是装饰字段,而是冲突消解顺序。


六、为什么这套机制经常让人误会成“自动猜错了”

因为用户脑中通常只有一个判断轴:

  • 客户属于哪个国家。

但 Odoo 实际判断至少有五个轴:

  • 手工属性有没有覆盖;
  • delivery 地址是什么;
  • VAT 是否满足要求;
  • 国家 / 国家组 / 州 / 邮编是否都命中;
  • 多条命中时 sequence 谁更靠前。

你少看其中任何一层,都会觉得它像在“乱猜”。

其实它不是乱猜,而是在按一套比 UI 表面复杂得多的规则链做决定。


七、排查 Fiscal Position 自动识别时的顺序

建议别直接从税映射页开始看,而是按这个顺序:

  1. move 上是否已经手工指定了 fiscal position;
  2. partner 或 delivery address 上是否有 property_account_position_id
  3. 当前实际参与识别的是哪个 delivery partner;
  4. VAT required 是否满足;
  5. 国家、国家组、州、邮编条件是否全过;
  6. 如果多条都命中,sequence 与公司层级谁优先。

这个顺序会比“看国家字段是否一致”有效得多。


一句话记忆

Odoo 的 Fiscal Position 自动识别,不是“按国家猜一个”,而是“先尊重手工覆盖,再按 delivery 地址和 VAT/地区条件过滤,最后按公司层级与 sequence 选第一条命中的规则”。

DISCUSSION

评论区

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