会计分组逻辑

Odoo 付款登记为什么有时合并成一笔、有时拆成多笔:account.payment.register 的分组规则

很多人以为 Register Payment 只是把选中的发票一起做成付款。可从 account_payment_register 源码看,Odoo 其实会先按合作伙伴、应收应付科目、币种、收付款方向和对方银行账户切 batch,再决定是一笔 payment 还是多笔。

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

很多实施同学第一次批量点 Register Payment 时,都会有一个很自然的预期:

  • 选了 3 张发票;
  • 填一个金额;
  • 系统就生成 1 笔付款。

但真实结果经常不是这样。

有时 3 张单据会合并成 1 笔 payment; 有时却会拆成 2 笔、3 笔,甚至向导直接变成“不可编辑”的汇总态。

这不是 Odoo 任性,而是 addons/account/wizard/account_payment_register.py 里本来就有一套明确的 batch 分组规则

Register Payment 不是“把当前勾选单据一起付款”。它先把待付款行按可共用付款参数分批,再决定每批怎么落成 payment。

一、向导真正处理的不是发票本身,而是 receivable / payable lines

default_get() 里,向导不是直接拿整张 account.move 做支付,而是先把上下文里的发票或分录展开成 account.move.line

然后它会筛掉:

  • 不是可付款账户类型的行;
  • residual 已经为 0 的行;
  • 不满足公司边界的行;
  • 同时混入应收与应付的行。

所以从源码视角看,付款向导的输入不是“几张发票”,而是:

一组仍有未清金额、而且方向一致的应收/应付 journal items。

这一步很关键,因为后面所有分组都是围绕这些 line 来做,不是围绕 invoice 头来做。

二、真正决定“能不能放进同一批”的,是 _get_line_batch_key()

源码里最值得盯的函数是:

  • _get_line_batch_key(line)

它给每一条 line 生成一个 batch key,核心字段包括:

  • partner_id
  • account_id
  • currency_id
  • partner_bank_id
  • partner_type

这几个字段几乎已经把“能否共用一笔付款”说透了。

1)partner 不同,当然不能混

收 A 客户的钱、收 B 客户的钱,或者付给两个供应商,通常都不会落成同一笔 account.payment

2)account 不同,也不会混

哪怕都是同一个 partner,只要挂的应收/应付科目不同,Odoo 也会拆批。

原因很简单:

payment 最终要去对账的是目标科目上的未清项。

如果目标科目都不一样,硬塞进同一 payment,后面的 reconcile 语义就会变脏。

3)currency 不同,不会混

不同币种对应不同的付款金额口径、汇差逻辑和 residual 计算方式。

所以币种不同,批次就不同,这个边界非常硬。

4)partner_bank_id 也会影响分批

这里最容易被忽略。

如果是发票场景,源码会优先从 move.partner_bank_id 拿对方银行账户。 这意味着:

  • 同一个供应商;
  • 同一币种;
  • 同一应付科目;
  • 但发票上指定了不同收款账户;

也可能被拆成不同 batch。

因为在 Odoo 看来,这已经不是“同一笔该往哪打”的问题,而是 付款目的账户本身不同

三、_compute_batches() 真正做的是“先按 key 聚类,再补出 payment_type”

_compute_batches() 里,Odoo 会先把所有 line 按上述 key 聚类。

但这里还有一个容易误判的点:

一开始的 key 里并没有直接写死 payment_type,而是后面根据该批 lines 的 balance 合计来判断:

  • 合计余额大于 0 → inbound
  • 否则 → outbound

这说明源码的思路不是“先决定收款还是付款,再找单子”,而是:

先把可共批的 line 放到一起,再根据余额方向判定这批到底是收款还是付款。

这也解释了为什么某些边界场景里,向导会先显得像一坨混合数据,最后才确定 payment type。

四、为什么有时不同 bank account 还能并回同一批

源码里还有一段特别容易错过的 merge 逻辑。

当同一 partner 的 inbound / outbound 银行账户集合都各自只有 1 个唯一值时,Odoo 会尝试把只差 partner_bank_id 的批次重新合并,再按最后得出的 payment_type 选正确的银行账户。

这背后的业务含义很朴素:

  • 如果这个 partner 在当前方向上只有唯一收/付款账户;
  • 那前面按 line 取 bank account 造成的拆分,其实没有业务必要;
  • 那就可以重新并回去。

所以你看到“有时同一个 partner 的几张单又被合并回一笔”,并不是规则不稳定,恰恰是规则在做 去歧义后的合并

五、为什么向导有时可以编辑,有时突然变成汇总态

_create_payments() 里有个很重要的判断:

  • edit_mode = can_edit_wizard and (len(first_batch_result['lines']) == 1 or group_payment)

这句话等于在说:

可编辑的典型情况

  • 只有 1 条 line;或
  • 用户明确选择 group_payment,允许按 batch 生成单笔 payment。

不可编辑的典型情况

  • 存在多个 batch;
  • 或虽然是一个 batch,但不允许 group payment;
  • 或系统需要按 move 再拆更细。

一旦进入不可编辑的路径,Odoo 不再把当前向导当成“创建一笔 payment 的表单”,而是把它当成:

一个批量支付调度器。

它后面会循环每个 batch,分别调 _create_payment_vals_from_batch() 去生成多笔 payment。

六、group_payment=False 时,系统还会继续细拆

很多人以为 batch 已经算完了,后面就结束了。

并没有。

_create_payments() 里,如果 group_payment 为假,Odoo 还会继续把 batch 按 move_id 再拆成 sub-batches。

也就是说:

  • 前一层 batch 解决的是“哪些 lines 理论上能共用付款参数”;
  • 后一层按 move 拆分,解决的是“你是否允许多张单共用同一笔 payment”。

所以你会看到这种情况:

  • 向导里看起来是同一 partner、同一币种、同一账户;
  • 但最终还是生成多笔 payment;
  • 原因不是 batch 失败,而是 group_payment 没开。

七、实务上最常见的 4 个误区

误区 1:同一个客户的发票一定能合成一笔

不一定。

只要下面任一条件不同,就可能拆开:

  • 应收/应付科目不同
  • 币种不同
  • 对方银行账户不同
  • 最后方向不同

误区 2:向导不可编辑就是坏了

也不对。

很多时候不可编辑只是因为当前选中的 lines 会生成多笔 payment,Odoo 不想让你在一个表单里误改只适用于其中一笔的字段。

误区 3:勾了多张发票,系统就一定会 group payment

不会。

是否共用一笔 payment,除了批次兼容性,还取决于 group_payment

误区 4:payment 拆单只和前台金额有关

不是。

拆单首先是数据模型兼容性问题,其次才是金额显示问题。

八、排错时最值得先看的顺序

如果用户问:

为什么这几张单没有合成一笔付款?

最有效的检查顺序通常是:

  1. 这些行是不是同一个 partner_id
  2. account_id 是否一致
  3. currency_id 是否一致
  4. 发票上的 partner_bank_id 是否一致
  5. 这批 line 的余额方向是否一致
  6. group_payment 有没有打开
  7. 是否在 _create_payments() 里又被按 move_id 二次拆开

你按这个顺序看,比在界面上盯金额来回猜快得多。

九、一句话记住这条链

Register Payment 的真实链路不是:

  • 选发票 → 生成付款

而是:

  • 选中可付款 lines
  • partner/account/currency/bank/partner_type 形成 batch
  • 根据余额方向补出 payment_type
  • 决定是否允许 group_payment
  • 必要时再按 move 二次拆分
  • 最后才创建 account.payment 并执行 reconciliation

所以“为什么一笔变多笔”,本质上不是前台按钮问题,而是:

Odoo 在保护付款对象的一致性,不让不该混在一起的未清项被硬合并。

这也是为什么理解 account.payment.register 时,最该看的不是页面字段,而是 _get_line_batch_key()_compute_batches()_create_payments() 这三段源码。

DISCUSSION

评论区

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