发票匹配

Odoo 发票和付款到底是怎样真正匹配上的:partial reconcile、full reconcile、matching_number 和 payment_state 细节讲透

很多人知道发票和付款会“核销”,但不知道 Odoo 底层到底靠什么对象把两者连起来。本文从 account.partial.reconcile、account.full.reconcile、matching_number 到 payment_state 的回算逻辑,把匹配细节讲透。

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

结论先行

在 Odoo 里,发票和付款的“匹配”不是一条简单的外键关系,也不是 payment_state 改了就算匹配完成

更准确地说:

它是通过 account.partial.reconcile 先把具体 journal item 连起来,必要时再汇总成 account.full.reconcile,并据此回算 residual、matching_number 以及最终的 payment_state

所以如果你想真正读懂“发票为什么显示已支付 / 部分支付 / in payment”,关键不是盯发票表头,而是盯:

  • 哪些 move line 被连起来了
  • 连了多少
  • 是 partial 还是 full
  • 有没有进一步连到银行流水或其他对手项

第一层:为什么发票和付款不是直接“一对一绑定”

很多业务系统会把这件事设计成:

  • 付款单上写 invoice_id
  • 发票上写 payment_id
  • 两边一连,问题就结束

但 Odoo 没这么做,因为现实世界没有这么简单。

一张付款可能:

  • 付多张发票
  • 只付一张发票的一部分
  • 先预收后再匹配
  • 还会继续和银行流水发生第二次匹配

同样,一张发票也可能:

  • 被多次收款
  • 被折让、退款、坏账核销部分抵消
  • 被信用票据或其他应收项一起结清

所以官方采用的是更底层、更灵活的设计:

不直接绑定“单据”,而是匹配“会计分录行”。

这样才能支持复杂现实。


第二层:account.partial.reconcile 为什么是核心对象

/home/ubuntu/odoo-temp/addons/account/models/account_move_line.pyaccount_full_reconcile.py 可以看出,Odoo 匹配时的基础积木不是 invoice 本身,而是:

  • matched_debit_ids
  • matched_credit_ids
  • account.partial.reconcile

这意味着每一次“匹配了一部分金额”,系统其实都是在建立一条 partial reconcile 记录。

它表达的不是“这两张单据认识了”,而是:

  • 这条借方行
  • 和那条贷方行
  • 在金额上匹配了一部分

这种粒度非常重要。

因为只有到分录行粒度,你才能正确表达:

  • 部分收款
  • 多笔收款拼一张发票
  • 一笔付款拆给多张发票
  • 汇率差异、write-off、银行流水继续加入同一链条

第三层:什么时候才会升格成 account.full.reconcile

partial reconcile 不等于 full reconcile。

你可以把它理解成:

  • partial reconcile:我已经建立了若干匹配片段
  • full reconcile:这些片段加起来,相关 residual 已经真正归零

官方专门有 account.full.reconcile 模型,用来承载“完全结清”的那一层含义。

这也是为什么你有时候会看到:

  • 行之间已经有 matched 关系
  • 但还不算 fully reconciled
  • matching_number 还是 P

这里的 P 就是在提醒你:

关系已经建立,但闭环还没彻底完成。


第四层:matching_number 为什么看起来像小字段,实际很有信息量

源码约束里写得很清楚:

  • 只部分核销时,可以是 P
  • fully reconcile 后,会对应 full reconcile 的标识
  • 某些 import 情况还可能是 I...

这说明 matching_number 不是随便显示的一个辅助码,而是系统在压缩表达当前匹配阶段:

  • 还只是 partial
  • 还是已经 full
  • 或者来自特殊导入场景

所以当你排查“为什么这张发票还不是 paid”时,matching_number 往往比页面状态更能说明底层真实处境。


第五层:payment_state 为什么是最后算出来的,不是先写进去的

前面那篇《登记付款》讲过,payment_state 不是人工写进去的状态。

但更深入一点看,account.payment 里计算 reconciled_invoice_ids 时,会直接通过 SQL 去追 account_partial_reconcile

  • 付款 move 的 line
  • 通过 partial reconcile 找到对手 line
  • 再找到这些对手 line 所属的 invoice move

这段实现非常能说明问题:

付款和发票之间的关系,不是 payment 表上直接存一根绳子,而是从 reconciliation 网络里反推出来的。

所以 payment_state 能不能变成 paidpartialin_payment,本质上是底层匹配网络的结果。

这也是为什么有时你看起来“已经收款了”,但状态还没完全到 paid:

  • 也许只是 partial
  • 也许银行侧还在 in payment
  • 也许还有 residual 没清完
  • 也许只是建立了部分匹配链,还没 full reconcile

常见误区

误区 1:发票和付款就是一对一关系

不是。 实际是 move line 粒度的多对多匹配网络。

误区 2:登记付款后就等于 fully reconciled

不一定。 登记付款只是把付款事实放进场,后面还要看怎么匹配。

误区 3:只要有 matched 关系,就说明发票 paid 了

错。 matched 可能只是 partial。

误区 4:matching_number 只是显示用字段

太低估它了。 它浓缩了当前匹配阶段的重要语义。

误区 5:payment_state 是发票主动记住某笔付款

不是。 很多时候是从 reconciliation 关系里反推回来的。


实战排查顺序

如果某张发票“明明有付款却状态不对”,建议这样查:

1. 先看对应应收/应付行有没有 matched_debit_ids / matched_credit_ids

先确认关系到底建没建起来。

2. 再看 residual 和 residual_currency 是否还剩余额

别只看界面颜色。

3. 再区分 partial reconcile 还是 full reconcile

很多误会都发生在这一步。

4. 再看付款是不是又和银行流水发生了下一层匹配

尤其 in_payment 场景很常见。

5. 最后再看 payment_state

它是结果摘要,不是根因入口。


一句话记忆法

Odoo 里发票和付款的匹配,先是 move line 之间的 partial reconcile,完全闭合后才升格为 full reconcile,payment_state 只是这张关系网算出来的结果。

DISCUSSION

评论区

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