付款登记

Odoo 点 Create Payment 后到底发生了什么:register wizard、payment post 与 reconciliation 串联

很多人把 Register Payment 看成一个按钮动作:点一下,系统就“自动付款完成”。但从 account.payment.register 的 _create_payments、_init_payments、_post_payments、_reconcile_payments 和 account.move.line.reconcile 看,这其实是一条“分批组织 -> 建 payment -> 过账/变更状态 -> 对账核销”的完整链路。

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

很多使用者对 Odoo 的 Register Payment 都有一种“按钮魔法”印象:

  • 打开发票;
  • Create Payment
  • 结果就应该一下子从未付款变成已付款。

这个理解不算全错,但太粗了。

/home/ubuntu/odoo-temp/addons/account/wizard/account_payment_register.pyaccount_move_line.py 看,Odoo 做的根本不是“一个按钮直接改状态”,而是一条分层流水线:

  1. 先把待付款分录整理成一批批 batch
  2. 再按批次生成 account.payment
  3. 然后执行 action_post()
  4. 最后把 payment 对应分录与原始应收/应付分录做 reconcile()

所以“付款登记”真正完成的是:

创建付款对象 + 让付款分录进入可核销状态 + 把两边账务行真正对上。

一、_create_payments() 先做的不是建账,而是筛批次

_create_payments() 一上来并不是马上 create() payment。

它先遍历 self.batches,把不符合条件的批次剔掉,例如:

  • 要求收款方银行账户必须可信;
  • 但当前 batch 没有可用账户;
  • 或银行账户存在但不允许 outbound payment。

如果过滤后一个 batch 都不剩,直接报 UserError

这说明一个很重要的设计点:

Create Payment 不是“你点了我就一定建单”,它先做支付前置约束检查。

也就是说,很多用户以为“按钮坏了”,其实是支付前提没满足。

二、同一个按钮,内部可能走两种分支

_create_payments() 接着会决定是否走 edit_mode

大意是:

  • 如果向导可编辑,且当前只处理单条来源,或者明确允许 group payment,
  • 就会走更像“以当前向导值为准”的模式;
  • 否则就按 batch 批量拆分。

这会直接影响后面生成的 payment 数量。

比如:

  • 不分组时,源码会把 batch 再按 move_id 拆成更细的 sub_batches
  • 分组时,则可能多条来源分录共用一个 payment。

所以现场常见的“为什么这次一张、下次两张”并不神秘,关键在于:

  • 你当前 wizard 能不能编辑;
  • 是否开启 group payment;
  • 参与付款的分录集合怎样被批次化。

三、真正建 account.payment 的是 _init_payments()

到了 _init_payments(),才真正发生:

  • self.env['account.payment'].with_context(skip_invoice_sync=True).create(...)

也就是说,payment 对象的创建和后续核销,是分开的两个阶段。

这里还有一个很值得注意的细节:

  • 在 edit mode 且付款币种与源分录币种不同时,
  • 源码会检查 balance 是否需要微调,
  • 然后直接改 payment.move_id.line_ids 的借贷金额,尽量保证后面能“完美结清”。

这背后的真实问题是:

汇率换算后的付款金额,未必天然和原始 residual 精确对上。

所以 Odoo 不是盲目相信“前端填的金额一定刚好”,而是先补足会计上可核销的平衡条件。

四、_post_payments() 并不负责核销,它负责把 payment 推进到可用状态

_post_payments() 本身很短:

  • 收集刚生成的 payment;
  • payments.action_post()

account.payment.action_post() 在当前源码里做的关键事包括:

  • 再检查必须可信的银行账户约束;
  • 某些现金类 outstanding account 直接设成 paid
  • 其余从 draft / in_process 等状态切到 in_process

注意这里最容易被误解的一点:

post payment 和 reconcile payment 不是同一个动作。

很多人看到 payment 已经 post 了,就以为原始发票一定已经 fully paid; 但源码结构明确告诉你:

  • post 只是让 payment 进入账务流程;
  • 真正让应收/应付变成已结清的是下一步的核销。

五、_reconcile_payments() 才是“让两边真的对上”的地方

_reconcile_payments() 会先从 payment 的 move_id.line_ids 里筛出:

  • 已过账 parent_state = posted
  • 属于有效支付账户类型
  • 还没 reconciled 的分录

然后把这些 payment lines 和原始 to_reconcile 分录拼起来,再按 account_id 分组调用:

  • reconcile()

也就是说,Odoo 真正的语义不是“付款单状态变了,所以发票自动算付了”,而是:

付款分录与原始应收/应付分录,在同一会计科目上完成了正式核销。

这一步完成后,付款状态、剩余金额、invoice payment state 才会朝业务上预期的方向变化。

六、而 reconcile() 背后其实还是一条更长的账务链

account.move.line.reconcile() 自己也不是一个简单字段更新。

它会进入 _reconcile_plan(),后面还包括:

  • 预取 move / matched lines,减少 ORM 抖动;
  • 组装 partial reconcile 数据;
  • 批量创建 account.partial.reconcile
  • 必要时生成 exchange difference entries;
  • 现金制税场景下再补 cash basis moves;
  • 条件满足时创建 account.full.reconcile
  • 最后跑 post hook。

这说明“注册付款”最终触发的不是 UI 级小动作,而是一整套会计对账流水。

所以如果你的现场问题是:

  • 为什么状态成了 in_payment 不是 paid
  • 为什么有 partial 不是 full;
  • 为什么多了汇兑差额分录;
  • 为什么现金制税在这一步才动;

答案大概率都不在按钮本身,而在 reconcile() 后面的账务逻辑里。

七、新手最容易误解的四件事

1)“Create Payment = 直接把发票改成已付款”

不对。

中间至少还有 payment 创建、post、reconcile 三层动作。

2)“payment post 了,就一定 fully paid”

也不对。

post 是 payment 自己进入流程;原始应收/应付是否结清,要看 reconcile 是否成功、是否完整。

3)“核销只是把两个数字相减”

也不对。

源码里还有 partial/full reconcile、汇兑差额、现金制税、hook 等后续动作。

4)“一张向导一定只建一张 payment”

不对。

是否 group、是否 edit mode、批次如何切分,都会影响最后建单数。

八、实战调试建议

如果付款登记结果和预期不一致,我更建议按这个顺序看:

  1. batch 是怎么切的:是否被拆成多个 to_process
  2. payment 有没有创建成功_init_payments() 是否报错或调平;
  3. payment 有没有 post:是否卡在银行账户可信约束;
  4. 核销对象是不是在同一 account 上_reconcile_payments() 是按 account_id 分组核销;
  5. 后续 reconcile 里有没有 partial / 汇兑 / caba 影响结果。

这样排错,比只盯着发票头上的 payment_state 有效得多。

总结

Odoo 的 Create Payment,本质上不是“点一下把状态改掉”,而是:

  • 先筛能付款的批次;
  • 再创建 account.payment
  • 然后 action_post()
  • 最后把 payment lines 与原始应收/应付分录真正 reconcile()

如果只记一句,就记这句:

Register Payment 解决的不是“改状态”,而是“把付款对象建立出来,并让它和原始债权债务正式对账”。

DISCUSSION

评论区

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