现金制税

Odoo 现金制税为什么一定要先经过 transition account:reconcile 触发、部分核销比例与过渡科目回转讲透

许多人知道 cash basis tax 会“在收付款后转税”,却没意识到官方实现真正围绕的是 transition account 与 reconcile 事件本身。本文聚焦过渡账户为什么必须可核销、每次 partial reconcile 怎样按比例触发转移,以及为什么这不是简单的付款后补税。

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

很多人已经知道 Odoo 的 cash basis tax 不是“开票时立即进正式税额”,而是要等付款以后再转。

但如果你继续往 /home/ubuntu/odoo-temp/addons/account/models/account_tax.pyaccount_move.pyaccount_partial_reconcile.py 里读,会发现真正的设计中心不是“付款动作”,而是:

  • transition account
  • reconcile 事件
  • 按 partial 比例迁移税与 base

现金制税的本质不是付款后补一笔税,而是先把税挂在可过渡、可核销的位置,等真实核销发生时,再按这次核销覆盖比例把税务口径迁移出去。

一、为什么一定要有 transition account

account.tax 上的 cash_basis_transition_account_id 帮助文本写得很直白:

  • 发票未核销前,税额先停在 transition account;
  • 到 reconcile 时,再从这里撤出,转到 regular tax account。

而源码还专门校验:

The cash basis transition account needs to allow reconciliation.

这句非常关键。

它说明 transition account 不是随便挑一个中间科目,而是一个带强约束的业务节点:

  1. 发票阶段,税额先暂存;
  2. 后续阶段,要能和 cash basis move 的对手行完成对应;
  3. 因为要表达“暂存”和“转正”之间的闭环,所以它必须允许 reconciliation。

误区 1:把 transition account 理解成普通过桥科目

不够准确。

普通过桥科目只是临时承接金额; 而 cash basis transition account 还是 后续税务迁移的定位点

如果它不能 reconcile,系统就没法稳定表达“这部分税已从暂存状态转为正式状态”。

二、真正的触发器不是付款按钮,而是 account.partial.reconcile

/home/ubuntu/odoo-temp/addons/account/models/account_partial_reconcile.py 里,_collect_tax_cash_basis_values() 才是关键。

这段逻辑不是围绕 payment wizard 写的,而是围绕 partial reconcile 本身写的。

它会对每个 partial 做这些事:

  • 找到 debit / credit 两端对应的 move;
  • 收集这次 partial 覆盖的金额;
  • 计算 percentage
  • 推出 payment_rate
  • 把结果放进 move 的 cash basis values 中,等待生成 caba entries。

这意味着:

cash basis tax 认可的不是“你登记了一笔付款”,而是“这笔付款和原应收 / 应付已经真的形成核销关系”。

所以你只创建 payment,还没和 invoice/bill 对应起来,不一定会得到完整的现金制税迁移语义。

三、为什么 partial reconcile 是核心,而不是例外

源码里最重要的一步,是按 partial 计算 percentage

  • 如果 move 以公司币为主,就用公司币金额比例;
  • 如果 move 以外币为主,就用外币金额比例。

然后系统把这次 partial 对应的比例挂到:

  • 本次应迁移多少 tax
  • 本次应迁移多少 base
  • 本次适用什么 payment_rate

这就说明 partial payment 在 cash basis 场景里根本不是边角问题,而是官方默认场景。

误区 2:以为现金制税总是“最后一笔付款才一次性转完”

不对。

官方设计明显是:

  • 每次 partial reconcile 都能触发一部分迁移;
  • 多次 partial 会累计形成完整迁移;
  • full reconcile 只是 partial 全部跑完后的结果态。

四、payment_rate 暗示它同时在处理税与汇率口径

很多人把 cash basis 只当税问题,但源码里还有 payment_rate

当 invoice 和 payment 的外币不一致时,系统会:

  • 用 payment date 重新取 conversion rate;
  • 或在 register payment 场景里接受强制 rate;
  • 再用这个 rate 去换算本次 caba 迁移金额。

这说明 Odoo 非常清楚现实世界的复杂性:

  • 业务单据可能是一种币;
  • 实际结算可能是另一种币;
  • cash basis 要回答的是“这次真实结清了多少税务金额”,而不是简单照搬开票日金额。

所以 cash basis move 和 exchange difference move 虽然常一起出现,但它们在逻辑上不是一回事。

五、base line 与 tax line 为什么都要被处理

account_partial_reconcile.py 里,系统既准备:

  • _prepare_cash_basis_base_line_vals()
  • _prepare_cash_basis_tax_line_vals()

这说明迁移的不只是税行本身。

Odoo 的想法是:

  1. 税行需要从 transition / tax account 之间做迁移;
  2. 对应的 base 语义也要进入 cash basis 口径;
  3. tax tags、product tags、analytic_distribution 这些上下文要尽量保留。

所以现金制税 move 不是一条孤零零的税务修正,而是把“本次已实现的税务部分”按完整会计上下文重建出来

六、为什么 product tags、tax tags、analytic distribution 也被带过去

你会在准备 cash basis line 的方法里看到:

  • tax_tag_ids
  • analytic_distribution
  • display_type
  • partner / currency / account

这说明官方很在意后续这些数据还能不能被报表、税表、分析维度正确识别。

换句话说,cash basis move 不是为了把数字凑平, 而是为了让“已经 exigible 的那部分金额”在后续:

  • 税务报表
  • 税标签统计
  • 分析分摊
  • 币种口径

里都能站得住。

unlink() 里还能看到更强的设计信号:

  • 找出 linked 的 tax cash basis entries;
  • 找出 exchange difference entries;
  • reverse 或 unlink 它们;
  • 再更新 matching number。

这说明 Odoo 把 cash basis move 看成 reconcile 事实的衍生物

只要 reconcile 被撤销,衍生出来的现金制税凭证也要跟着回滚。

误区 3:以为 CABA move 是独立事实,生成了就固定存在

不对。

它依附于 reconcile 事实。

核销撤销,相关现金制税逻辑就必须被撤回;否则税务口径会和真实结清状态脱节。

八、排查“为什么现金制税不对”时,更该查什么

建议按这个顺序:

  1. tax 是否真的是 tax_exigibility = 'on_payment'
  2. cash_basis_transition_account_id 是否配置且允许 reconcile;
  3. 公司是否配置了 cash basis journal;
  4. 发票 / 付款之间是否真的形成了 account.partial.reconcile
  5. 是不是多次 partial 导致只迁移了一部分;
  6. 是否存在多币种与 payment rate 影响;
  7. 是否有人撤销过 reconcile,导致 caba move 被反转。

一句话记忆

Odoo 现金制税不是“付款后补税”,而是“先把税挂在可核销的 transition account 上,等 partial/full reconcile 发生时,再按本次覆盖比例与 payment rate 把税务口径迁移出去”。

DISCUSSION

评论区

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