PAYATHON 2026

金额为零时自动 Register Payment 并生成 invoice

支付阿杰

结论

可以跳过 Register Payment 弹窗。

如果订单因 100% discount 而无需支付,通常按以下流程处理:

  1. 确认销售订单;
  2. 创建并过账 invoice;
  3. 不创建付款记录;
  4. 系统发现 invoice 的未付余额为零后,将其判定为 paid。

零金额 invoice 不需要创建一笔金额为 0 的虚假 payment。Register Payment 向导用于核销实际收付款。强行创建零金额付款,可能触发金额校验,也可能留下没有实际意义的会计记录。

下面的示例基于 Odoo 的常见数据模型。不同 Odoo 版本和自定义模块使用的按钮方法、字段名称可能不同,请按实际版本调整。

为什么还会显示付款弹窗

如果 Confirm Sale 在执行后无条件返回 Register Payment 的 action,即使订单金额已经归零,客户端仍会打开付款向导。

这通常是自定义代码造成的,例如:

return invoice.action_register_payment()

也可能是代码直接返回了 account.payment.register 的窗口 action,却没有先检查 invoice 的剩余应付金额。

是否需要付款,应根据 invoice 的待核销余额判断,不能只检查销售订单的 amount_total。税费、舍入、预付款和多币种计算都可能使订单总额与 invoice 余额存在差异。

推荐处理流程

先创建并过账 invoice,再检查 amount_residual:

  • amount_residual 不为零时,正常打开 Register Payment;
  • amount_residual 为零时,直接打开 invoice,不显示付款向导;
  • invoice 已过账且剩余金额为零时,Odoo 通常会自动计算付款状态,不需要手工写入 payment_state。

判断金额是否为零时,不要直接写:

invoice.amount_residual == 0

应按币种精度判断:

invoice.currency_id.is_zero(invoice.amount_residual)

这样可以避开浮点数误差和币种小数位不同带来的问题。

代码示例

下面是流程层面的实现示例。假设 action_confirm_and_invoice 是自定义的确认按钮方法:

from odoo import models


class SaleOrder(models.Model):
    _inherit = "sale.order"

    def action_confirm_and_invoice(self):
        self.ensure_one()

        # 确认销售订单
        self.action_confirm()

        # 创建 invoice
        invoices = self._create_invoices()
        invoices.action_post()

        # 多张 invoice 时,只有全部余额均为零才跳过付款向导
        is_zero_balance = all(
            invoice.currency_id.is_zero(invoice.amount_residual)
            for invoice in invoices
        )

        if is_zero_balance:
            return self._get_invoice_action(invoices)

        # 非零余额继续进入原有 Register Payment 流程
        return invoices.action_register_payment()

    def _get_invoice_action(self, invoices):
        action = self.env["ir.actions.actions"]._for_xml_id(
            "account.action_move_out_invoice_type"
        )

        if len(invoices) == 1:
            action.update({
                "view_mode": "form",
                "res_id": invoices.id,
                "views": [(False, "form")],
            })
        else:
            action["domain"] = [("id", "in", invoices.ids)]

        return action

有些版本的 account.move 没有可直接调用的 action_register_payment(),此时需要自行返回付款向导 action,例如:

def _open_register_payment(self, invoices):
    return {
        "type": "ir.actions.act_window",
        "name": "Register Payment",
        "res_model": "account.payment.register",
        "view_mode": "form",
        "target": "new",
        "context": {
            "active_model": "account.move",
            "active_ids": invoices.ids,
        },
    }

然后将原来的返回语句改为:

if is_zero_balance:
    return self._get_invoice_action(invoices)

return self._open_register_payment(invoices)

如果只需要修正已有代码

如果现有模块已经创建并过账 invoice,只需在打开付款弹窗前增加余额判断:

invoice.action_post()

if invoice.currency_id.is_zero(invoice.amount_residual):
    return {
        "type": "ir.actions.act_window",
        "res_model": "account.move",
        "res_id": invoice.id,
        "view_mode": "form",
        "target": "current",
    }

return invoice.action_register_payment()

这里不需要模拟点击弹窗中的 Cancel。服务器端发现余额为零后,直接不要返回付款向导 action。

注意事项

不要直接修改付款状态

不建议使用以下写法:

invoice.payment_state = "paid"

payment_state 通常由会计分录、核销状态和剩余金额计算得出。直接赋值可能不会生效,也可能导致界面状态与会计数据不一致。

invoice 必须先过账

即使草稿 invoice 的总额为零,也不一定会显示为正式的 paid 状态。一般应先执行:

invoice.action_post()

然后再检查 amount_residual 和 payment_state。

检查最终余额,而不是折扣比例

不要只根据 discount == 100 判断。订单中可能有多行、税费、舍入行或其他附加费用。更可靠的判断条件是:

invoice.currency_id.is_zero(invoice.amount_residual)

多张 invoice 要逐张判断

一次操作可能生成多张 invoice。只有所有 invoice 的剩余金额都为零时,才能整体跳过付款向导。只要其中一张仍有余额,就应继续正常付款流程,或者只为有余额的 invoice 注册付款。

方法名称取决于版本

_create_invoices()、action_post()、付款向导 action 和 invoice 窗口的 XML ID 可能随 Odoo/OpenERP 版本而变化。如果使用旧版 account.invoice,不能直接套用这些代码,需要改用该版本对应的模型和过账方法。

备注:内容仅供参考。