很多人一提 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 找一个匹配项”这么粗糙。
它真正的优先级,大致是:
- 特定单据类型的硬编码 fiscal position(例如采购收据等特殊场景);
- 手工指定的 property_account_position_id;
- delivery partner 的匹配条件;
- 国家 / 国家组 / 州 / 邮编 / VAT requirement 的组合过滤;
- 按公司层级与 sequence 决定谁先中标。
这是一套“先看人工覆盖,再看履约地址,再看规则命中顺序”的体系。
一、为什么 Odoo 先看 delivery 地址,而不是只看开票伙伴
account_move._compute_fiscal_position_id() 里会先取:
partner_idpartner_shipping_id- 或 partner 上默认的 delivery 地址
然后把 partner 和 delivery 一起传给 _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 不是随机选一条,而是优先:
- 更贴近当前公司的配置;
- 再按 sequence 决定先后。
所以 sequence 不是装饰字段,而是冲突消解顺序。
六、为什么这套机制经常让人误会成“自动猜错了”
因为用户脑中通常只有一个判断轴:
- 客户属于哪个国家。
但 Odoo 实际判断至少有五个轴:
- 手工属性有没有覆盖;
- delivery 地址是什么;
- VAT 是否满足要求;
- 国家 / 国家组 / 州 / 邮编是否都命中;
- 多条命中时 sequence 谁更靠前。
你少看其中任何一层,都会觉得它像在“乱猜”。
其实它不是乱猜,而是在按一套比 UI 表面复杂得多的规则链做决定。
七、排查 Fiscal Position 自动识别时的顺序
建议别直接从税映射页开始看,而是按这个顺序:
- move 上是否已经手工指定了 fiscal position;
- partner 或 delivery address 上是否有
property_account_position_id; - 当前实际参与识别的是哪个 delivery partner;
- VAT required 是否满足;
- 国家、国家组、州、邮编条件是否全过;
- 如果多条都命中,sequence 与公司层级谁优先。
这个顺序会比“看国家字段是否一致”有效得多。
一句话记忆
Odoo 的 Fiscal Position 自动识别,不是“按国家猜一个”,而是“先尊重手工覆盖,再按 delivery 地址和 VAT/地区条件过滤,最后按公司层级与 sequence 选第一条命中的规则”。
DISCUSSION
评论区