PAYATHON 2026

部分付款后如何将 invoice 标记为 Paid

支付小周

结论

不要直接执行 invoice.write({'state': 'paid'})。

在 Odoo 中,发票的付款状态取决于应收或应付会计分录是否完成核销。客户在优惠期内支付 95% 时,应将实际收到的 95% 和剩余 5% 的现金折扣一并核销。发票的 residual 归零后,系统会自动将其识别为 Paid。超过优惠期限后,则应按正常流程收取并核销 100% 的款项。

为什么直接修改 state 无效

action_validate_invoice_payment() 最终会创建付款分录并执行 reconciliation。发票状态通常根据以下信息重新计算:

  • 应收或应付科目的未核销余额;
  • residual / amount_residual;
  • 付款分录与发票分录是否已完全核销。

即使暂时将 state 写成 paid,只要还有 5% 的余额未核销,后续计算仍可能把状态恢复为 Open。这样还会导致业务状态与总账不一致:界面显示发票已付款,应收账款却仍未结清。

正确的会计处理是:

实际收款 95% + 现金折扣 5% = 发票应收金额 100%

其中的 5% 应计入专门的现金折扣账户,不能直接消除余额。

推荐做法

旧版 Odoo 的 account.payment 通常已经提供付款差额处理字段:

payment_difference_handling = 'reconcile'
writeoff_account_id = discount_account.id

将 payment_difference_handling 设为 reconcile,表示把付款差额作为 write-off 处理,并完全核销发票。核销完成后,发票会自动进入 Paid 状态。

处理步骤如下:

  1. 判断付款日期是否早于或等于折扣截止日期;
  2. 计算可抵扣的 5%;
  3. 确认实际付款与折扣之和正好覆盖发票剩余余额;
  4. 将付款差额处理方式设为 reconcile;
  5. 指定现金折扣会计科目;
  6. 调用标准付款过账方法,由 Odoo 创建分录并完成核销。

旧版 account.payment 示例

下面的示例适用于使用 account.invoice、post()、payment_difference_handling 和 writeoff_account_id 的 Odoo 版本。具体字段名需要按实际模块调整。

假设:

  • cash_discount_deadline 是自定义的折扣截止日期字段;
  • cash_discount_account_id 是现金折扣科目;
  • 折扣按含税发票总额的 5% 计算;
  • 每个 payment 只处理一张 invoice。
from odoo import api, models
from odoo.exceptions import UserError


class AccountPayment(models.Model):
    _inherit = 'account.payment'

    @api.multi
    def post(self):
        for payment in self:
            if len(payment.invoice_ids) != 1:
                continue

            invoice = payment.invoice_ids
            deadline = invoice.cash_discount_deadline

            if not deadline or payment.payment_date > deadline:
                continue

            discount_account = invoice.cash_discount_account_id
            if not discount_account:
                raise UserError(
                    'A cash discount account must be configured.'
                )

            currency = invoice.currency_id
            discount_amount = currency.round(
                invoice.amount_total * 0.05
            )

            # 发票当前未付余额,应由本次付款和折扣共同结清。
            expected_payment = currency.round(
                invoice.residual - discount_amount
            )

            if currency.compare_amounts(
                payment.amount, expected_payment
            ) != 0:
                continue

            payment.write({
                'payment_difference_handling': 'reconcile',
                'writeoff_account_id': discount_account.id,
            })

        return super(AccountPayment, self).post()

当付款金额是发票应付金额的 95%,并且付款日期仍在优惠期内时,标准付款流程会:

  • 将 95% 计入银行或现金账户;
  • 将 5% 的差额计入现金折扣账户;
  • 将这两部分与发票应收分录完全核销;
  • 把发票剩余金额降为零;
  • 自动将发票识别为 Paid。

超过截止日期后,代码不会启用 write-off。此时只有付款金额覆盖全部剩余余额,标准核销流程才会将发票标记为 Paid。

已经发生部分付款时

如果一张发票可以分多次付款,就不能只判断本次付款是否等于发票总额的 95%。需要判断此前累计付款加上本次付款后,是否达到折后应付金额。

在旧版 account.invoice 中,可以根据当前 residual 计算本次应付金额:

discount_amount = currency.round(invoice.amount_total * 0.05)
expected_payment = currency.round(
    invoice.residual - discount_amount
)

例如,发票金额为 1,000,之前已经支付 400,当前 residual 为 600。优惠期内还需实际支付:

600 - 50 = 550

这样累计现金付款为 950,剩余 50 作为现金折扣核销。

还需要防止以下异常情况:

if expected_payment <= 0:
    raise UserError(
        'The invoice balance is too small to apply this discount.'
    )

如果折扣只适用于一次性付款,不适用于分期付款,还应检查发票此前是否已有付款或部分核销。

新版 Odoo 的区别

较新的 Odoo 已将发票迁移到 account.move。付款状态通常保存在 payment_state,付款过账入口通常是 action_post(),付款注册一般通过 account.payment.register wizard 完成。

不同版本的实现方式有所区别,但会计处理原则相同:

  • 不直接写入 state 或 payment_state;
  • 为实际付款创建银行分录;
  • 为 5% 的折扣创建 write-off 分录;
  • 完全核销发票的 receivable/payable line;
  • 由系统根据 residual 和 reconciliation 自动计算付款状态。

在新版 Odoo 中,更合适的做法是在 account.payment.register 中设置 write-off 行,或者扩展其生成付款差额分录的逻辑,而不是修改 account.move.payment_state。

注意事项

  • 使用 currency.round() 和 currency.compare_amounts(),不要直接比较浮点数。
  • 明确折扣基数是 amount_total、未税金额还是剩余金额。
  • 现金折扣科目应由会计人员确认,不要随意使用普通费用科目。
  • 外币发票还可能产生汇兑损益,不能将汇兑差额当作现金折扣。
  • 退款、贷项通知单和负数发票需要单独处理符号。
  • 一次付款关联多张发票时,应按发票分别计算折扣和 write-off,不能只设置一个总差额。
  • 覆盖 post() 或 action_post() 时,应先准备付款参数,最后调用 super()。不要在标准核销完成后强制修改发票状态。
  • 如果系统已经通过付款注册向导提供“付款差额处理/标记为全额支付”功能,应优先复用,避免重复实现会计分录和核销逻辑。

备注:内容仅供参考。