金额为零时自动 Register Payment 并生成 invoice
结论
可以跳过 Register Payment 弹窗。
如果订单因 100% discount 而无需支付,通常按以下流程处理:
- 确认销售订单;
- 创建并过账 invoice;
- 不创建付款记录;
- 系统发现 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,不能直接套用这些代码,需要改用该版本对应的模型和过账方法。
备注:内容仅供参考。
