结论先行
在 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.py 和 account_full_reconcile.py 可以看出,Odoo 匹配时的基础积木不是 invoice 本身,而是:
matched_debit_idsmatched_credit_idsaccount.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 能不能变成 paid、partial、in_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
评论区