付款状态

Odoo 里注册付款后为什么会变成 paid、partial 或 in_payment:支付向导、核销与 payment_state 链路

注册付款并不是简单打个“已付”标记。account_payment_register 会先分批生成 payment,再按应收应付科目做 reconcile,最后 account.move 根据残额、核销对手和 payment 匹配状态重算 payment_state。

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

很多人把 Odoo 的“注册付款”理解成一个很轻的动作:

  • 点按钮
  • 填金额
  • 发票状态改成 Paid

但真正看 /home/ubuntu/odoo-temp/addons/account/wizard/account_payment_register.pyaccount_move.py,你会发现这条链路远比“打状态”严肃得多。

它本质上做了三件事:

  1. 先把待付分录按批次整理;
  2. 生成并过账 account.payment / account.move
  3. 再通过核销结果,让发票自己重算 payment_state

所以:

payment_state 不是向导直接写上的标签,而是核销后的会计结果。

一、注册付款的第一步不是改发票,而是挑出有效批次

_create_payments() 里,向导首先会遍历 self.batches

它不会盲目全建,而会先检查:

  • 对应收款 / 付款方式是否有可用银行账户;
  • 伙伴银行账户是否需要人工验证;
  • 当前批次是否满足付款创建条件。

这说明“注册付款”在 Odoo 里不是一个随便落记录的 UI 行为,而是一个带支付前校验的业务动作。

如果批次都无效,源码会直接抛 UserError,而不是先建一个半残 payment 再说。

二、向导先决定是 edit mode 还是分批创建

_create_payments() 里有个很关键的分叉:

  • edit_mode
  • edit_mode

如果可以编辑且只涉及单条或允许 group payment,向导会走更像“当前这一笔付款”的路径; 否则会把待付行拆成多个子批次,一笔一笔生成 payment。

这一步的意义是:

  • Odoo 不是把“注册付款”硬编码成永远一张 payment;
  • 它会根据分组、分期、付款对象和批次特征决定生成结构。

所以你在界面里看到“同样是注册付款,这次合成一笔、下次拆成多笔”,往往不是随机,而是批次逻辑不同。

三、真正让发票进入支付链的是 _reconcile_payments()

向导不是建完 payment 就结束。

源码里 _reconcile_payments() 才是让发票状态开始变化的核心动作。

它会:

  • 找出 payment move 上已过账、未核销、且属于有效付款账户类型的行;
  • 取本次要核销的发票行;
  • 按 account 分组;
  • 对同科目的 payment lines + source lines 调用 .reconcile()

这意味着 Odoo 的设计非常会计化:

不是“付款对象指向发票”就算付了,而是应收 / 应付行真正核销了才算进入支付结果。

这也是为什么有些开发者手工建 payment、手工关联 invoice,最后 payment_state 仍不对。

因为缺的不是“关系字段”,而是:

  • 对的 journal items
  • 对的科目
  • 对的 reconcile 行为

四、payment_state 到底怎么从 not_paid 变成 partial / paid / in_payment

account_move.py 里的 _compute_payment_state() 是整条链路的判官。

它不是简单看有没有 payment 记录,而是综合判断:

  • amount_residual 是否为 0;
  • 核销对手是不是 payment 或 statement line;
  • 这些 payment 是否都 matched;
  • 当前发票是不是仍有部分残额;
  • 是否属于冲销形成的 fully reconciled 场景。

源码里几个关键结果可以这样理解:

not_paid

默认起点。

只要残额还在、没有足够核销结果,这就是最自然的状态。

partial

有核销,但残额还没清零。

也就是:

  • 你确实付了一部分;
  • 但从会计结果看,这张发票还没彻底结清。

一般出现在:

  • 残额为 0;
  • 且对应 payment 都已 matched;
  • 或者虽然没有 payment 记录,但核销结果已经把它彻底结平。

in_payment

这个状态最容易误解。

源码里 _get_invoice_in_payment_state() 的默认实现其实返回 paid``。

也就是说,在基础会计模块里,Odoo 默认并不坚持把 fully paid 但尚在支付流程中的发票显示成 in_payment

这个方法的注释已经说得很清楚:

  • 之所以做成 hook;
  • 是因为只做 invoicing 的用户,不想看到 in_payment
  • accountants 模块会 override 它,让会计场景可以启用 in_payment 语义。

这意味着:

你看到的是 paid 还是 in_payment,不只取决于有没有付款,还取决于当前安装模块对会计状态语义的扩展。

reversed

如果发票因为冲销类分录被完整抵消,源码也会识别出这种 fully reconciled 但语义上更接近“已被反转”的状态。

所以并不是所有“残额归零”的发票都应该叫 paid

这是实战中最常见的困惑之一。

一般从源码逻辑看,常见原因有四类:

1)payment 建了,但没真正 reconcile 到应收 / 应付行

如果 payment line 和 invoice line 没走到 .reconcile(),状态当然不会如你预期变化。

2)核销了,但只是部分核销

那它就应该是 partial,而不是 paid

3)支付流程语义仍停在 in_payment

尤其在启用更完整会计流程的环境里,fully paid 不一定立刻显示 paid

4)你做的是“关系补线”,不是“会计结果补齐”

比如只回写了某些关联字段,没把 journal item 层真正接上。

六、这条链路解决的核心问题:让 UI 状态服从会计事实

从设计上看,Odoo 很刻意地避免这样一种危险实现:

  • 点击按钮后,直接把发票写成 paid;
  • 后面再想办法补 payment 和核销。

如果这么做,系统很快就会出现:

  • UI 看起来已付款;
  • 会计分录其实没平;
  • 银行对账也说不通;
  • 多币种差额、write-off、statement line 都会乱套。

所以 Odoo 反过来做:

  • 先生成 payment;
  • 再过账;
  • 再核销;
  • 最后由发票从会计事实中推导 payment_state

这才是企业会计系统该有的方向。

七、调试建议:别先盯状态字段,先盯 journal items

如果现场出现“为什么还是 not_paid / partial / in_payment”,最稳的排查顺序通常是:

  1. 看 payment 是否真的创建并过账;
  2. 看 payment move 上哪些 line 属于有效付款账户类型;
  3. 看 invoice 的 receivable / payable line 是否与 payment line 真正 reconcile;
  4. 看残额是不是确实归零;
  5. 再看当前环境里 _get_invoice_in_payment_state() 被谁 override。

这样查,比直接猜 UI 状态可靠得多。

结语

“注册付款”在 Odoo 里不是一个改标签的按钮,而是一条严格的会计链路。

它真正表达的是:

  • 付款对象先被建出来;
  • 会计分录先成立;
  • 核销关系先成立;
  • 发票状态再作为结果被计算出来。

所以以后再问“为什么点了 Register Payment 还是不是 Paid”,更精准的问题应该是:

这张发票的应收 / 应付行,真的已经按 Odoo 认可的方式被支付并核销了吗?

DISCUSSION

评论区

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