企业版批量付款

Odoo 企业版批量付款为什么不是“导完银行回单就彻底结束”:Bank Rec 回写、付款重置与批次状态回滚讲透

很多人以为 Odoo 企业版批量付款只是在生成批次时做一次聚合,等银行流水对上就结束了。但 `account_accountant_batch_payment` 真正补上的,是批次与 Bank Reconciliation widget 之间的双向回写:选中 batch 时怎样往 statement line 塞 move line、无分录付款何时补验证、删除已核销行后为什么 payment 会退回 in_process,而 batch 仍只是“信封”而不是主账实体。

企业 会计
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多团队第一次用 Batch Payment,注意力都放在“怎么把多笔 payment 聚成一个 batch”。

但真正到了财务落地阶段,最容易出问题的往往不是建批次,而是 批次进入银行对账之后会发生什么

常见误解大概有三种:

  1. 批量付款只是导出银行文件的壳子,后面对账和它无关;
  2. 一旦银行流水匹配成功,batch 自己就等于最终凭证
  3. 如果撤销对账,删掉 statement line 上那条已核销行就行,不会影响 payment 本身状态

如果你去读 /home/ubuntu/odoo-temp/enterprise/account_accountant_batch_payment,会发现企业版真正补强的并不是“再造一个更大的付款单”,而是:

让 batch 成为银行对账界面里的一个可回放、可撤销、可回写 payment 集合。

这意味着它要处理的不只是“把几笔付款装进袋子”,还包括:

  • 对账时怎样把 batch 对应的 move line 塞进 statement line;
  • 某些 payment 还没有会计分录时,什么时候自动补验证;
  • 删除已核销行之后,payment 为什么会退回 in_process
  • 发票已经被付款影响了 residual / payment state 时,为什么还要被强制 draft 再 post 一遍。

这篇就专门讲这条 “batch → bank rec → rollback” 链,而不重复上一篇关于 amount / amount_residual 的口径问题。


一、企业版真正加的不是新账,而是“批次可进入银行对账”的桥

先看 models/account_batch_payment.py

这里最核心的方法不是创建 batch 的动作,而是:

  • _get_amls_from_batch_payments(domain)
  • _get_amls_for_reconciliation(st_line)

它透露出一个重要事实:

批量付款进入银行对账时,系统关心的是它最终能贡献哪些 account.move.line。

_get_amls_from_batch_payments() 会先把已有 move_id 的 payment 取出来,调用 payment._seek_for_lines(),只抓流动性相关的 liquidity_lines。然后它做两件事:

  1. 把这些已存在的 move line 收集起来;
  2. 再用 _get_aml_values(...) 反向生成一组准备塞入 statement line 的数据。

这里的 balance=-move_line.balanceamount_currency=-move_line.amount_currency 很关键。

这说明银行对账里追加的行,不是把原 payment 再复制一遍,而是为了在 statement line 上构造一个 可抵消、可勾稽 的对向行。

同时,企业版还给 account.move.line 加了一个技术字段:

  • payment_lines_ids

这个字段表面不起眼,但它直接决定了后续“撤销已核销行时,系统还能顺藤摸瓜找到原 payment”。

换句话说,企业版不是单纯让 batch 出现在银行对账候选里,而是提前布好了 回写路径回滚路径


二、选中一个 batch 时,真正落地的是 statement line 的补线与 payment 校验

再看 models/account_bank_statement_line.py 里的:

  • set_batch_payment_bank_statement_line(batch_payment_id)

它的主链路很短,但信息量很大:

  1. 取出 account.batch.payment
  2. batch_payment._get_amls_for_reconciliation(self),拿到要追加的 move lines;
  3. _add_move_line_to_statement_line_move(amls_to_create) 把这些行塞到当前银行流水分录里;
  4. 如果某些 payment 还没有 move_id,但状态已经属于 _valid_payment_states(),就额外执行 action_validate()

这里最容易被忽略的是第四步。

很多人以为“batch 进入对账”只是在前端 widget 里做匹配展示。但企业版其实承认一种现实:

  • 有些 payment 此时已经在业务上成立;
  • 但它们还没有正式会计分录;
  • 一旦你决定把它们与实际银行流水勾上,系统就必须把这层会计形态补齐。

所以 set_batch_payment_bank_statement_line() 的意义不是“标记这个 batch 已匹配”,而是:

把 batch 里的 payment 真正投影进 statement line 所代表的会计世界。

这也解释了为什么银行对账不是只读视图,而是会改变 payment 生命周期的入口。


三、有分录和无分录两种 payment,进入对账后的落账口径并不一样

测试文件 tests/test_batch_payment.py 很能说明企业版在意的边界。

1)有会计分录的 payment:直接勾到 liquidity / outstanding 侧

test_bank_rec_widget_batch_payment_with_entries() 里:

  • 先创建 payment;
  • create_batch_payment()
  • 然后把 batch 绑到 statement line。

最后断言的结果是:

  • statement line 上会有一条银行默认科目行;
  • 还有一条来自 payment method outstanding account 的反向行;
  • 这条行会直接与 payment move_id 中资产流动性相关的 line 建立 reconciled_lines_ids

这说明对已有分录的 payment,企业版更偏向做 现成分录之间的勾稽

2)无会计分录的 payment:先补出能进对账的行,再视需要验证

test_partner_account_batch_payments_without_journal_entry()test_bank_rec_widget_batch_payment_without_entries() 里,可以看到另一条逻辑:

  • 没有 journal entry 的 payment,系统不会凭空装作已经存在 liquidity line;
  • 它会按收款/付款方向,临时转成应收或应付侧的 move lines;
  • 如果这批 payment 后续被确认要与银行流水绑定,才进一步 action_validate()

所以“同样是 batch 进银行对账”,底层并不是统一一条机械逻辑。

Odoo 实际在回答两个不同问题:

  • 已有分录的 payment,该怎样被 statement line 正确勾住?
  • 尚未生成分录的 payment,什么时候必须补成正式会计对象?

这正是企业版场景复杂于社区版常见理解的地方。


四、删除已核销行不是简单 undo,而是一次 payment 与 invoice 的联动回滚

真正体现企业版细致程度的,是:

  • delete_reconciled_line(move_line_ids)

它在 super() 删除 statement line 上指定已核销行之后,还会继续做两段回滚。

第一段:payment 回到 in_process

代码注释写得非常直白:

不去碰 batch 本身,因为 batch 只是 payment 的 envelope。

这句话值得单独记住。

官方明确把 batch 定义成:

  • 一个容器;
  • 一个会计工作流的入口;
  • 但不是“主账实体”。

所以撤销某条已核销行时,真正应该回退的是 payment,而不是 batch。

于是它会:

  • payments.action_draft()
  • payments.action_post()

最后 payment 回到 in_process

第二段:如果 payment 已经影响发票,还要把发票也重过一遍

payments.invoice_ids 存在,代码会:

  • move_linked.button_draft()
  • move_linked.action_post()

为什么这么麻烦?

因为付款与发票 residual / payment state 已经联动过。如果你只是把 statement line 上一条已核销行删掉,而不重算发票,界面就会出现非常危险的假象:

  • payment 已经退回处理中;
  • 但 invoice 还停留在看似已付款或 residual 不准确的状态。

所以这里不是“多余重过账”,而是为了强制把发票恢复到正确的 in_payment / residual 口径。

test_bank_rec_widget_batch_payment_without_entries_link_to_move() 就验证了这件事:删除对账行之后,payment 回到 in_process,invoice 的 payment_state 也回到 in_payment


五、batch 的状态不是对账分录的 owner,而是 payment 集合的观察结果

如果你只看界面,很容易把 batch 当成“这批付款的最终状态单”。

但企业版源码一直在避免这个误解。

最典型的证据就是删除已核销行时那句注释:

we don't touch the batch payment itself since it's just an envelope of payments

意思是:

  • batch 可以从 sent 变成 reconciled,也能因为底层 payment 回退再显示成未完成;
  • 但它不是 statement line 的法律主体,也不是 invoice residual 的原始来源;
  • 它更像 payment 集合在某个阶段上的 包装、投递与匹配状态摘要

这和很多团队的直觉正好相反。

很多实施项目把 batch 当成“导出银行文件后就封存的业务单”。

源码告诉你的却是:

只要银行对账还能继续回写或撤销,batch 就仍处在可回放的会计链路里。

因此,排查问题时不要只盯 batch 状态本身,而要顺着这条顺序看:

  1. payment_ids 是否都处于可入对账的状态;
  2. 这些 payment 有没有 move_id
  3. statement line 上追加的行是否带了正确的 payment_lines_ids / reconciled_lines_ids
  4. 删除时是否把 payment 与 invoice 一并回退。

六、实战里最容易踩的 4 个坑

坑 1:把 batch 当成最终凭证编号

batch 名称可以出现在 bank statement 的 payment_ref,但它本身不是最终会计分录。

真正决定 residual、reconcile、invoice payment state 的,还是底层 payment 与 statement move lines 的关系。

坑 2:只测“勾上成功”,不测“撤销成功”

很多定制只在 happy path 下看起来没问题:

  • 选中 batch;
  • statement line 勾上;
  • 状态变了。

但一旦用户撤销对账,如果你自己的扩展没有尊重 payment_lines_idsaction_draft() / action_post() 这套回滚方式,就很容易留下脏状态。

坑 3:忽略“无分录 payment”分支

企业版明确支持 payment 在某些安装组合下先没有 move,再在对账时补验证。

如果你做定制时默认“所有 payment 必有 move_id”,对账阶段就会出错,而且通常只在生产数据里才暴露。

坑 4:只看 batch,不看 invoice 被重算后的状态

用户往往报的是“批量付款撤销后发票怎么还像付过款”。

这类问题别只查 batch,要同步看:

  • payment.state
  • invoice.payment_state
  • invoice line residual
  • statement line 是否已删除对应 reconciled line

七、这条源码链真正回答的问题是什么?

account_accountant_batch_payment 在企业版里,回答的不是“如何把几笔 payment 打包发送给银行”。

它真正回答的是:

当一组 payment 被包装成 batch 后,它们怎样进入银行对账、怎样把会计影响写回 statement line、又怎样在撤销时安全回滚到底层 payment 与 invoice。

所以如果你要给财务团队一句最实用的总结,我会这样说:

Odoo 企业版里的 Batch Payment 不是付款终点,而是 payment 集合进入 Bank Reconciliation 的中间桥。真正的风险点不在导出那一刻,而在后续回写与撤销是否还能保持 payment、statement line 与 invoice 三边一致。

这才是读完这组源码后,最值得记住的地方。

DISCUSSION

评论区

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