核销收口

Odoo 的 partial reconcile 怎么一步步变成 full reconcile:核销链路别再只盯“已付款”

在 Odoo 里,核销不是“要么没核销,要么已核销”这么简单。系统会先创建 `account.partial.reconcile` 这条连接边,再在整组分录都真正归零后,补出 `account.full.reconcile` 作为整图收口。

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

别把核销理解成一个布尔值

很多人看 Odoo 核销,只盯两个表面结果:

  • 单据是不是 paid
  • 分录是不是 reconciled

然后就很容易把脑内模型简化成:

  • 没核销
  • 已核销

但从源码看,真正发生的事情更像一个图逐步闭合的过程。

核心角色至少有三层:

  1. account.move.line:被匹配的分录行
  2. account.partial.reconcile:一条“借方行 ↔ 贷方行”的匹配边
  3. account.full.reconcile:当整组边和节点都达到完整收口后,给整组打上的“完全核销”标记

所以最关键的一句不是“核销了没”,而是:

现在只是建了一条 partial 边,还是整组已经满足 full 的条件了?


reconcile() 并不是直接把一切改成 fully reconciled

addons/account/models/account_move_line.py 里,reconcile() 本身很短:

  • 它直接调用 _reconcile_plan([self])

这意味着真正复杂的逻辑不在入口函数,而在后面的计划执行里。

这种写法的好处是,Odoo 可以把核销当成一个批处理计划,而不是对每两行做一次机械更新。


partial 的本质:先创建“匹配边”

account.partial.reconcile 模型里,最核心的字段非常直白:

  • debit_move_id
  • credit_move_id
  • amount
  • debit_amount_currency
  • credit_amount_currency
  • full_reconcile_id

这说明 partial reconcile 不是“一个状态”,而是一条连接关系记录

它表达的是:

  • 哪条借方分录
  • 和哪条贷方分录
  • 以多大金额被匹配起来了

所以部分付款、分次收款、拆分核销这些现实业务,才有地方落地。

如果没有 partial 这层,系统只能表达“全有或全无”,那会非常笨。


matching number 为什么有时是 P...,有时又像一个完整编号

account_partial_reconcile.py 里的 _update_matching_number() 很值得看。

它会把所有 partial 关系看成一组图:

  • partial 是边
  • move line 是节点

然后为每个连通分量生成 matching number。

逻辑大意是:

  • 如果这组 line 还没有 full reconcile,就写成 P 开头
  • 如果 full_reconcile_id 已经存在,就直接写 full reconcile 的 id 文本

这就解释了为什么有时候你在界面上看到的是:

  • P123

有时候却像一个完整的核销号。

因为前者表示:

  • 这组线已经有关联,但还只是 partial 图

后者表示:

  • 这组线已经被系统确认收口为 full reconcile

什么时候 partial 才会升级成 full

account_move_line.py_reconcile_plan() 后段有一段非常关键的逻辑:

  • 先把涉及的分录按 matching 图聚合成 full_batches
  • 再判断每组是不是 is_fully_reconciled
  • 满足条件的,才组装 full_reconcile_values_list
  • 最后统一 create(account.full.reconcile)

这里最重要的不是“创建了 full reconcile”,而是创建之前那层判断。

源码会检查:

  • 相关 line 是否已经真的归零
  • 多币种场景下该看公司币 residual,还是外币 residual
  • 这组 partial 图是不是已经闭合到可以视为完整收口

也就是说,full reconcile 不是“多建一张表方便查”,而是:

当系统确认整组分录在会计意义上已经彻底对完,才补上的那层整组标记。


为什么 partial 特别适合现实业务

因为现实世界里的收付款,本来就经常不是一步到位:

  • 一张发票分两次付款
  • 一笔收款同时冲多张发票
  • 汇率差异要到最后才能补齐
  • 现金制税、汇兑差异、尾差处理都可能插进来

如果系统只有 full reconcile,那这些中间态就没法优雅表达。

partial reconcile 的价值就在于:

  • 允许系统先承认“已经匹配了一部分”
  • 但不假装这已经是最终结论

而当所有残余金额真正归零后,再由 full reconcile 给整组盖章。

这套两层结构非常像会计上的现实过程。


为什么“已付款”不等于“你已经懂了核销”

很多实施或排错时,大家只会看发票状态是不是 paid。

但 paid 只是业务面上的读法。

源码层真正让系统站得住的是:

  • 哪些 account.move.line 被哪些 partial 连接起来
  • 这些 partial 所形成的图是否已经闭合
  • 是否已经创建 full reconcile
  • 过程中有没有触发汇兑差异、现金制税、反向冲销等附带动作

所以你如果只盯 paid,很容易漏掉两类关键问题:

1)为什么看起来“已经差不多对完了”,却还没 full

因为某个 residual 还没真正归零,或者多币种条件下还有残差。

2)为什么取消核销后,系统像回滚了一串东西

因为 account.partial.reconcile.unlink() 里不只是删边,它还会:

  • 处理 full reconcile 的解绑
  • 回退 matching number
  • 反转或删除现金制税/汇兑差异分录
  • 回写 payment 状态

这说明 partial 不只是中间痕迹,它还是后续很多会计动作的触发点。


最容易误判的 4 件事

误判 1:partial 就是不完整版 full

不准确。partial 是边,full 是整组收口标记,它们不是同一层概念。

误判 2:核销是两条分录的一对一动作

现实里它更像一个图,可能一条边边相连,最后收成一组。

误判 3:matching number 只是展示字段

它背后其实反映了图结构当前处于 partial 还是 full 状态。

误判 4:取消核销只是把状态改回去

不对。它可能连带现金制税、汇兑差异和 payment 状态一起回滚。


排查核销问题时的正确视角

以后你再遇到这些问题:

  • 发票怎么还没 fully paid
  • 为什么这组分录还是 partial
  • 为什么取消核销影响了一串分录
  • 为什么 matching number 变了

建议优先按这条线看:

  1. 哪些 account.move.line 在参与
  2. 生成了哪些 account.partial.reconcile
  3. 这组 line 的 residual 是否真的都归零
  4. 有没有生成 account.full.reconcile
  5. 有没有伴随汇兑差异或现金制税动作

这个视角比只看单据状态靠谱得多。


最后一句话

Odoo 核销机制最重要的,不是“是否 paid”,而是:

系统先用 account.partial.reconcile 把分录一步步连起来,等整组金额与残余真正收口后,再用 account.full.reconcile 给这张关系图盖上完全核销的章。

把这个过程想成“图逐步闭合”,你对核销、反核销、多次付款和汇率差异的理解都会清晰很多。

DISCUSSION

评论区

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