很多使用者对 Odoo 的 Register Payment 都有一种“按钮魔法”印象:
- 打开发票;
- 点 Create Payment;
- 结果就应该一下子从未付款变成已付款。
这个理解不算全错,但太粗了。
从 /home/ubuntu/odoo-temp/addons/account/wizard/account_payment_register.py 和 account_move_line.py 看,Odoo 做的根本不是“一个按钮直接改状态”,而是一条分层流水线:
- 先把待付款分录整理成一批批
batch; - 再按批次生成
account.payment; - 然后执行
action_post(); - 最后把 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、批次如何切分,都会影响最后建单数。
八、实战调试建议
如果付款登记结果和预期不一致,我更建议按这个顺序看:
- batch 是怎么切的:是否被拆成多个
to_process; - payment 有没有创建成功:
_init_payments()是否报错或调平; - payment 有没有 post:是否卡在银行账户可信约束;
- 核销对象是不是在同一 account 上:
_reconcile_payments()是按account_id分组核销; - 后续 reconcile 里有没有 partial / 汇兑 / caba 影响结果。
这样排错,比只盯着发票头上的 payment_state 有效得多。
总结
Odoo 的 Create Payment,本质上不是“点一下把状态改掉”,而是:
- 先筛能付款的批次;
- 再创建
account.payment; - 然后
action_post(); - 最后把 payment lines 与原始应收/应付分录真正
reconcile()。
如果只记一句,就记这句:
Register Payment 解决的不是“改状态”,而是“把付款对象建立出来,并让它和原始债权债务正式对账”。
DISCUSSION
评论区