PAYATHON 2026

如何在 Odoo v15 CE 中用 Python 为 invoice 注册 payment

支付老李

结论

在 Odoo v15 CE 中,account.move.action_register_payment() 只会返回“注册付款”向导对应的界面动作,不会直接创建付款。

通过 Python 自动注册付款时,需要:

  1. 创建并过账 invoice;
  2. 将该 invoice 放入 active_ids 上下文,并创建 account.payment.register 临时记录;
  3. 调用向导的 action_create_payments()。

核心代码如下:

payment_register = self.env['account.payment.register'].with_context(
    active_model='account.move',
    active_id=invoice.id,
    active_ids=invoice.ids,
).create({
    'journal_id': payment_journal.id,
    'payment_date': fields.Date.context_today(self),
})

payment_register.action_create_payments()

为什么不能直接调用 action_register_payment()

account.move 上的 action_register_payment() 是一个 UI action,负责准备窗口动作和上下文,供 Odoo 前端打开 account.payment.register 向导。

例如:

invoice.action_register_payment()

这段代码只会返回一个 action 字典,不会创建 account.payment,也不会核销 invoice。

付款的创建、过账以及应收账款核销,实际由 account.payment.register 向导完成。

完整实现示例

下面的示例会为每个销售订单创建 invoice,将草稿 invoice 过账,再按剩余应付金额登记全额付款。

from odoo import fields, models
from odoo.exceptions import UserError


class SaleInvoicePaymentWizard(models.TransientModel):
    _name = 'sale.invoice.payment.wizard'
    _description = 'Create Invoice and Register Payment'

    def create_invoices(self):
        sale_orders = self.env['sale.order'].browse(
            self.env.context.get('active_ids', [])
        )

        payment_journal = self.env['account.journal'].search([
            ('type', 'in', ('bank', 'cash')),
            ('company_id', '=', self.env.company.id),
        ], limit=1)

        if not payment_journal:
            raise UserError('未找到可用于收款的银行或现金日记账。')

        for order in sale_orders:
            invoices = order._create_invoices()

            for invoice in invoices:
                if invoice.state == 'draft':
                    invoice.action_post()

                if invoice.state != 'posted':
                    continue

                if invoice.payment_state in ('paid', 'in_payment'):
                    continue

                if not invoice.amount_residual:
                    continue

                payment_register = self.env[
                    'account.payment.register'
                ].with_context(
                    active_model='account.move',
                    active_id=invoice.id,
                    active_ids=invoice.ids,
                    dont_redirect_to_payments=True,
                ).create({
                    'journal_id': payment_journal.id,
                    'payment_date': fields.Date.context_today(self),
                })

                payment_register.action_create_payments()

        return {'type': 'ir.actions.act_window_close'}

这里不需要显式传入 amount。account.payment.register 会根据上下文中的 invoice 计算默认付款金额,通常就是 amount_residual。让向导自行计算,也能减少退款、外币和部分核销场景中的正负号问题。

如果业务明确要求指定付款金额,也可以传入:

payment_register = self.env[
    'account.payment.register'
].with_context(
    active_model='account.move',
    active_id=invoice.id,
    active_ids=invoice.ids,
    dont_redirect_to_payments=True,
).create({
    'journal_id': payment_journal.id,
    'payment_date': fields.Date.context_today(self),
    'amount': invoice.amount_residual,
})

这种写法适合普通客户 invoice 的全额收款。遇到退款、供应商账单或外币付款时,建议优先让向导计算金额。

使用指定的付款方式

Odoo v15 使用 account.payment.method.line 表示日记账上的付款方式。如果银行日记账配置了多个入站付款方式,可以明确传入 payment_method_line_id:

payment_method_line = payment_journal.inbound_payment_method_line_ids[:1]

if not payment_method_line:
    raise UserError('所选日记账没有配置入站付款方式。')

payment_register = self.env[
    'account.payment.register'
].with_context(
    active_model='account.move',
    active_id=invoice.id,
    active_ids=invoice.ids,
    dont_redirect_to_payments=True,
).create({
    'journal_id': payment_journal.id,
    'payment_method_line_id': payment_method_line.id,
    'payment_date': fields.Date.context_today(self),
})

payment_register.action_create_payments()

客户 invoice 一般使用 inbound_payment_method_line_ids,供应商账单付款通常使用 outbound_payment_method_line_ids。

也可以复用 action_register_payment() 的上下文

如果想沿用 Odoo 原生 action 生成的上下文,可以先取得 action,再创建向导:

action = invoice.action_register_payment()
payment_context = dict(
    self.env.context,
    **action.get('context', {})
)

payment_register = self.env[
    'account.payment.register'
].with_context(payment_context).create({
    'journal_id': payment_journal.id,
    'payment_date': fields.Date.context_today(self),
})

payment_register.action_create_payments()

不过在服务端代码中,直接设置 active_model、active_id 和 active_ids 往往更清楚。

注意事项

使用 _create_invoices() 的返回值

不要在创建完成后再次遍历:

order.invoice_ids

这个字段可能包含以前创建的 invoice、退款单或其他历史记录。直接使用本次创建并返回的记录更稳妥:

invoices = order._create_invoices()

必须先过账

account.payment.register 只能处理已经过账,并且存在待核销应收或应付款项的会计凭证。因此,创建向导前必须执行:

invoice.action_post()

日记账必须属于正确公司

在多公司环境中,付款日记账应与 invoice 属于同一家公司。相比固定使用 self.env.company,按 invoice 的公司查找更加严谨:

payment_journal = self.env['account.journal'].search([
    ('type', 'in', ('bank', 'cash')),
    ('company_id', '=', invoice.company_id.id),
], limit=1)

不要直接调用私有方法

有些代码示例会调用:

payment_register._create_payments()

这个私有方法在 Odoo v15 中确实参与付款创建,但业务代码更适合调用向导按钮对应的公开方法:

payment_register.action_create_payments()

私有方法以下划线开头,升级时接口更容易发生变化。

注册付款不等于银行对账

上述流程会创建并过账 account.payment,同时核销 invoice 的应收账款。完成全额付款后,invoice 通常会进入 paid 状态,或者进入与当前付款流程配置相符的状态。

这并不一定表示银行流水已经完成对账。如果日记账配置了未结收款账户,后续仍可能需要通过银行对账,将付款与实际银行流水匹配。

备注:内容仅供参考。