企业版批量付款

Odoo 企业版批量付款为什么会出现“总额没变、未结清变少、状态还在 sent”:`account_batch_payment` 金额与状态链路讲透

很多人第一次用 Odoo 企业版批量付款,会困惑同一批 payment 明明总额没变,batch 的 residual 却会下降,甚至状态会从 draft 变 sent、再变 reconciled。源码里这不是显示问题,而是 `amount`、`amount_residual(_currency)` 与 `state` 分别回答三件不同的事:批次规模、剩余未核销金额,以及是否已发送并完成匹配。

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

很多团队第一次用 Odoo 企业版的 Batch Payment,都会被一个现象绕晕:

  • 批次里一共还是 10 笔付款;
  • 批次总额 amount 看起来没变;
  • amount_residual 却变少了;
  • 甚至批次状态已经是 sent,却还没到 reconciled

如果只看界面,很容易误会成:

Odoo 的批量付款金额是不是算乱了?

但去看 /home/ubuntu/odoo-temp/enterprise/account_batch_payment 源码会发现,官方其实是故意把 “批次规模”“尚未匹配完的金额”“发送/核销进度” 拆成三套口径。

也就是说,批量付款不是一个单一数字模型,而是一套并行状态机。

本文就基于:

  • models/account_batch_payment.py
  • models/account_payment.py
  • tests/test_account_batch_payment.py

把这条链讲透。

先说结论:在 Odoo 企业版里,批量付款的 amount 回答“这一批原本有多大”,amount_residual 回答“这一批还有多少流动性金额没匹配掉”,state 回答“是否已发送、是否已完成匹配”。三者故意不会永远相等。


一、批量付款不是“重新造一笔总付款”,而是把多笔 account.payment 绑成一个容器

先看 models/account_payment.py

企业版给 account.payment 加了一个字段:

  • batch_payment_id

create_batch_payment() 做的也不是“合并分录”,而是:

  1. 校验当前 payment 是否处于允许入批的状态;
  2. 拿第一笔 payment 的 journal、payment method、payment type 当默认骨架;
  3. 创建一个 account.batch.payment
  4. 把这些 payment 通过 payment_ids 挂进去。

这件事非常关键。

因为它决定了一个基础事实:

Batch Payment 不是新的资金动作,而是多笔既有 payment 的编组对象。

所以你后面看到的金额、剩余金额、状态变化,都是围绕“这组 payment 的聚合观察”,不是围绕“一笔新总付款”的流水逻辑。

这也解释了为什么 Odoo 会强行限制同批 payment 的一致性。

account_batch_payment.py_check_payments_constrains() 里,官方要求同一个 batch 内的 payment 必须满足:

  • journal 相同;
  • payment_type 相同;
  • payment method 相同;
  • 要么都有会计分录、要么都还没有;
  • 金额不能为 0;
  • 不能重复出现在其他 batch 里。

这不是吹毛求疵,而是因为批次只是“聚合视图”。如果底层 payment 的业务语义不一致,批次上的聚合数就没有解释价值。


二、amount 统计的是“这一批的总体规模”,不是“还没处理完多少”

很多人最容易误会 amount

_compute_from_payment_ids() 里,Odoo 会遍历 payment_ids,把每笔 payment 的流动性金额折算到 batch 货币,然后累加到:

  • amount

这里的关键点有两个。

1)它优先看 payment 对应的流动性分录

如果 payment 已经有 move_id,源码会先 payment._seek_for_lines(),然后拿 liquidity_lines 来算金额。

这说明 Odoo 关心的不是“业务表面上的付款单金额”,而是:

这笔 payment 在银行/现金流动性那一侧,当前对应了多少金额。

所以 batch 的 amount 本质上是一个流动性口径的批次规模

2)amount 不会因为部分 payment 已匹配银行流水就自动归零

测试 test_change_payment_state()test_change_payment_state_valid() 很能说明问题。

即使某些 payment 的状态变了,batch 的:

  • amount

仍然代表这批 payment 原始聚合后的金额规模;变化的主要是 residual,而不是 total。

换句话说:

amount 更像“这包东西有多大”,不是“现在还剩多少没做完”。

这也是为什么用户看到“总额没变”时,不应先怀疑系统错了。


三、amount_residual 统计的是“流动性行还有多少没被匹配掉”

真正解释用户困惑的,是同一个计算函数里的另外两列:

  • amount_residual
  • amount_residual_currency

它们跟 amount 一起算,但语义完全不同。

源码会先取一个允许的 payment state 列表:

['in_process', 'paid'] if _get_invoice_in_payment_state() == 'paid' else ['in_process']

也就是 _valid_payment_states()

这句话背后有一个很重要的设计:

  • 如果当前环境里发票“处理中”的口径最终落在 paid,那么 batch 可以把 paid 也当作有效状态;
  • 如果安装的是更严格的会计场景,只认 in_process,那么 paid 反而不再被视为仍应计入 residual 的“待跟踪状态”。

也就是说,amount_residual 不是一个纯数学字段,而是一个带状态语义的剩余金额

residual 到底在回答什么?

不是“这批 payment 的原始金额是多少”,而是:

在当前会计状态规则下,这批 payment 对应的流动性金额里,还有多少没有完成匹配/清空。

所以当一部分 payment 被银行流水匹配后,你会看到:

  • amount 不变;
  • amount_residual 下降。

这是正常现象,因为“批次规模”没变,但“待匹配余额”变少了。

为什么 Odoo 要分成公司币和批次币两个 residual?

因为 batch 自己有 currency_id,而公司也有 company_currency_id

于是官方同时保留:

  • amount_residual:公司币口径
  • amount_residual_currency:批次币口径

这能避免多币种场景里只看一个 residual 造成误读。


四、混合币种时,batch 不是简单求和,而是按批次货币重新折算

account_batch_payment 的一个很容易被低估的点,是它不是只为单币种支票袋设计的

从测试看,官方专门覆盖了几类情况:

  • test_batch_payment_foreign_currency
  • test_batch_payment_move_different_currencies
  • test_foreign_currency_batch_payment
  • test_batch_payment_journal_foreign_currency

这些测试证明一件事:

Batch Payment 的金额不是把 payment.amount 机械相加,而是会根据 batch/journal/company 的货币层级,动态换算。

源码里的计算顺序大致是:

  1. 如果流动性行币种与 batch 币种一致,直接取 amount_currency
  2. 如果 batch 币种等于公司币,则直接取 balance
  3. 否则,用 company_currency_id._convert(...) 折算到 batch 币种。

这意味着:

  • 两笔金额都叫 100,放进同一个 batch 后不一定等权;
  • batch 列表上的 total,更像“站在这个批次货币视角下重述后的总规模”;
  • residual 也是按相同逻辑重算,而不是偷懒沿用 payment 原币。

这对业务方意味着什么?

如果你们在看批量付款金额时:

  • journal 本身有外币;
  • payment 有公司币也有外币;
  • 甚至 payment 的 move 货币和 batch 货币都不同;

那么 batch 上看到的金额,本来就不该被理解成某一列原币数字的简单相加

它是 Odoo 替你做过一次“批次观察口径统一”之后的结果。


五、state 看的是发送与匹配进度,不是金额余额

再看 _compute_state()

这里的逻辑非常克制:

  • 如果所有未取消/未拒绝 payment 都 is_sent=Trueis_matched=True,batch = reconciled
  • 否则,如果所有未取消/未拒绝 payment 都 is_sent=True,batch = sent
  • 否则,batch = draft

注意,这里完全没有直接比较 amount 或 residual

这说明官方根本不想把批次状态做成“按金额阈值推导”的模型,而是让它严格反映两个动作:

  1. 这些 payment 有没有被正式发送出去;
  2. 这些 payment 后来有没有和银行事实完成匹配。

所以你才会看到一个非常典型、但很容易误会的状态组合:

  • batch 已经 sent
  • 但还没 reconciled
  • residual 也可能还不为 0

这不是卡住,而是在表达:

付款动作已经发起,但银行侧匹配闭环还没全部完成。

这其实比单纯用“已完成/未完成”更有会计现实感。


六、为什么 validate 时会先尝试 action_post(),再汇总错误而不是直接整批失败

validate_batch()_check_batch_validity() 时,还有一个很值得抄作业的设计。

官方不会先粗暴拦截所有 draft payment,而是会调用 _check_and_post_draft_payments()

  • 对每笔 draft payment 尝试 action_post()
  • 某笔失败,就把 UserError 收集起来;
  • 最后按错误原因分组,统一展示给用户。

这背后体现的是一种很成熟的批处理思路:

批次校验不是为了证明“整包都坏了”,而是为了尽量把可过的先过、把不可过的按原因归类。

所以 batch validation 的结果可能不是一条简单报错,而是:

  • 只有一个错误时,用 RedirectWarning 把你直接带到对应 payment;
  • 多个错误/警告时,弹 wizard 汇总展示。

这也解释了为什么企业版批量付款给人的感觉更像“财务操作工作台”,而不是一个一次性按钮。


七、为什么有些 payment 明明能看见,却不能建 batch

create_batch_payment() 里有一段容易被忽略但很重要的逻辑:

  • 如果所选 payment 里有状态不在 _valid_payment_states() 的,Odoo 不会直接建 batch;
  • 它会打开 account.create.batch.error.wizard
  • 只把有效 payment 留给后续创建,非法 payment 交给你检查。

而且这个“合法状态”不是写死的。

它取决于:

  • 当前环境是否把 paid 也视作批量付款允许状态;
  • 当前会计安装层级是否更严格。

所以业务上常见的错觉是:

  • “这几笔付款都已经存在了,为什么有些能入批、有些不行?”

源码给出的答案其实是:

Batch Payment 关注的不是 payment 是否存在,而是它是否还处于这个批处理流程可继续接管的状态。

一旦 payment 已超出这个状态边界,就不应再继续塞回同一批控制链里。


八、为什么企业版要把“已发送”和“已匹配”拆开,而不是直接一个 done

这是我觉得这个模块最有价值的地方。

很多系统会偷懒,把批量付款做成:

  • 草稿
  • 完成

看起来简单,但一到真实银行链路就不够用了。

因为从财务实际流程看,至少有三层状态:

  1. 我已经组织好这批 payment
  2. 我已经把这批 payment 发出去了
  3. 我已经在银行流水/对账层面把它们匹配闭环了

Odoo 企业版把这三层分别映射成:

  • draft
  • sent
  • reconciled

同时再用:

  • amount
  • amount_residual
  • amount_residual_currency

来表达金额视角。

这就形成了一个非常实用的组合:

  • 状态字段 回答流程进度;
  • 金额字段 回答财务余额;
  • 两者不互相硬绑。

这也是为什么它比“状态=done 时金额自动清零”的模型更稳。


九、实战里最容易误解的 4 件事

1. 以为 batch amount 就是还没到账的金额

不对。

amount 更接近批次总体规模,不是待匹配余额。真正看“还没消掉多少”,要看 residual。

2. 以为 residual 下降就表示 batch 金额被改写了

不对。

通常只是部分 payment 已匹配,导致流动性剩余金额下降。

3. 以为 sent 就等于已经彻底完成

不对。

sent 只说明 payment 已发送,不代表银行侧匹配闭环已经完成。

4. 以为多币种 batch 的 total 应该和 payment 原币列表一眼对上

不对。

batch total 是站在 batch/journal 货币视角下重算过的统一口径。


十、如果你要做二开,最该尊重哪几个边界

如果你准备在 batch payment 上做定制,我建议先守住这几个边界:

1. 不要把 amountamount_residual 合并成一个字段语义

这是两类信息:批次规模 vs 待匹配余额。混在一起,前台会更乱。

2. 不要把 sentreconciled 折叠成一个“已完成”

你会失去银行发送与会计匹配之间最关键的过渡层。

3. 多币种场景不要偷懒只取 payment.amount 相加

要沿着官方的流动性行和 _convert() 逻辑走,否则 total/residual 都会失真。

4. 批量校验不要只做“第一笔失败就整体失败”

官方按 payment 逐笔尝试、按错误原因归类的做法,明显更适合财务工作流。


总结

account_batch_payment 最容易被误解的点,就是大家会本能地希望:

  • 一个 batch 只有一个金额真相;
  • 一个 batch 只有一个完成状态;
  • 两者最好始终同步。

但 Odoo 企业版没有这么设计。

它把批量付款拆成了三组问题:

  1. 这批 payment 原本有多大?amount
  2. 这批 payment 还有多少流动性金额没被匹配?amount_residual / amount_residual_currency
  3. 这批 payment 目前处在发送完成,还是核销完成?draft / sent / reconciled

所以你看到“总额没变、residual 下降、状态还是 sent”,通常不是异常,而是 Odoo 在同时告诉你三件不同的事实。

如果把这一点看懂,后面无论你是在做财务实施、培训用户,还是二开批量付款导出/对账逻辑,都会顺很多。

DISCUSSION

评论区

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