PAYATHON 2026

使用 Python 自动登记付款时出现 Odoo 错误

支付阿杰

结论

报错的直接原因是:付款过账时,Odoo 生成了一条名为 Write-Off 的 account.move.line,但这条分录没有 account_id,违反了数据库约束 account_move_line_check_accountable_required_fields。

代码中设置了:

'payment_difference_handling': 'reconcile',

这表示付款金额与发票待付金额不一致时,Odoo 需要直接核销差额。在这种情况下,必须提供有效的 writeoff_account_id。原代码却注释掉了这个字段:

# 'writeoff_account_id': self.diff_income_account.id,

因此,Odoo 创建 Write-Off 行时无法确定差额应计入哪个会计科目,写入 account_move_line 时便触发了约束错误。

为什么会触发这个约束

account_move_line_check_accountable_required_fields 用于保证参与记账的会计分录行包含必要字段。具体的约束表达式可能随 Odoo 版本不同而略有变化,但通常要求:

  • 会计分录行必须有 account_id;
  • 分录行必须有名称,也就是 name;
  • 只有章节、备注等不参与记账的展示行可以不设置会计科目。

错误信息中的失败记录已经给出了直接线索:

..., Write-Off, ..., null, ...

Write-Off 表明这是一条付款差额行,相关字段中的 null 则说明会计科目没有正确设置。

创建 account.payment 时不一定会立即报错,因为会计分录通常要等到执行 post() 或 action_post() 时才会生成。数据库约束是在系统写入生成的 account.move.line 时触发的。

解决方法一:为差额指定核销科目

如果业务上确实需要直接核销付款差额,就要设置 writeoff_account_id:

Payment = self.env['account.payment'].with_context(
    default_invoice_ids=[(4, SaleInvoice.id, False)]
)

payment = Payment.create({
    'payment_date': SaleInvoice.date,
    'payment_method_id': payment_method.id,
    'payment_type': 'inbound',
    'partner_type': 'customer',
    'communication': SaleInvoice.name,
    'partner_id': SaleInvoice.partner_id.id,
    'amount': paymentAmount,
    'journal_id': default_journal_id.id,
    'company_id': SaleInvoice.company_id.id,
    'currency_id': SaleInvoice.currency_id.id,
    'payment_difference_handling': 'reconcile',
    'writeoff_account_id': self.diff_income_account.id,
    'writeoff_label': 'Write-Off',
})

payment.post()

self.diff_income_account 需要符合以下条件:

  • 是有效的 account.account 记录;
  • 属于付款对应的公司;
  • 没有被停用或废弃;
  • 是允许直接记账的明细科目;
  • 科目用途符合差额性质,例如现金折扣、手续费或付款差异。

可以在创建付款前明确检查:

if not self.diff_income_account:
    raise UserError('请配置付款差额核销科目。')

if self.diff_income_account.company_id != SaleInvoice.company_id:
    raise UserError('付款差额科目与发票不属于同一公司。')

不要为了消除报错而随意指定科目。差额究竟应该计入哪个科目,需要根据企业的会计政策确定。

解决方法二:保留未付余额

如果付款金额小于发票待付金额,而且剩余款项以后还要继续收取,就不应创建 Write-Off。此时应将差额处理方式改为保留未核销余额:

payment = Payment.create({
    'payment_date': SaleInvoice.date,
    'payment_method_id': payment_method.id,
    'payment_type': 'inbound',
    'partner_type': 'customer',
    'communication': SaleInvoice.name,
    'partner_id': SaleInvoice.partner_id.id,
    'amount': paymentAmount,
    'journal_id': default_journal_id.id,
    'company_id': SaleInvoice.company_id.id,
    'currency_id': SaleInvoice.currency_id.id,
    'payment_difference_handling': 'open',
})

payment.post()

这样,Odoo 会登记一笔部分付款,未支付金额仍保留在发票的应收余额中,也就不需要 writeoff_account_id。

如果付款金额原本就应与发票余额完全一致,也可以直接使用发票的未核销金额:

'amount': SaleInvoice.amount_residual,

在多币种环境中,还要确认 paymentAmount 与发票使用相同的币种口径,不能直接把公司本位币金额当作发票币种金额。

更推荐的登记方式

如果当前 Odoo 版本支持 account.payment.register,可以优先使用付款登记向导。它会按照 Odoo 的标准流程计算付款金额、差额和核销关系:

register = self.env['account.payment.register'].with_context(
    active_model='account.move',
    active_ids=SaleInvoice.ids,
).create({
    'payment_date': SaleInvoice.date,
    'journal_id': default_journal_id.id,
    'amount': paymentAmount,
    'payment_difference_handling': 'reconcile',
    'writeoff_account_id': self.diff_income_account.id,
    'writeoff_label': 'Write-Off',
})

register.action_create_payments()

如果需要保留剩余应收款,则使用:

'payment_difference_handling': 'open',

不同 Odoo 版本中的 account.payment.register 字段和调用方式并不完全相同,应以目标版本模型实际提供的字段为准。

还需要检查的字段

即使补上了 writeoff_account_id,以下配置有误时,付款过账仍可能失败。

付款方式

不要硬编码:

'payment_method_id': 1,

数据库 ID 在不同环境中并不固定,应从付款日记账支持的入账方式中获取实际记录。较新的 Odoo 版本可能使用 payment_method_line_id,而不是 payment_method_id。

付款日记账

journal_id 必须是当前公司可用的银行或现金日记账,其付款方式、默认账户或未结清账户也要配置完整。

币种

原代码使用的是当前环境公司的币种:

'currency_id': self.env.company.currency_id.id,

如果发票使用外币,这个币种可能与发票币种不一致。通常应根据实际付款币种设置,例如:

'currency_id': SaleInvoice.currency_id.id,

过账方法

部分旧版本使用:

payment.post()

较新的版本通常使用:

payment.action_post()

具体应根据当前 Odoo 版本选择。

最终判断

问题不在 PostgreSQL 本身,也不是简单的发票关联失败。失败记录已经明确指向一条没有会计科目的 Write-Off 分录行。

应根据差额的实际处理方式选择配置:

  • 需要直接核销付款差额时,使用 payment_difference_handling = 'reconcile',同时提供有效的 writeoff_account_id;
  • 需要把差额继续保留为应收余额时,使用 payment_difference_handling = 'open';
  • 付款金额原本应完全匹配时,先统一金额和币种口径,避免意外生成 Write-Off;
  • 当前版本支持付款登记向导时,优先通过 account.payment.register 完成付款和核销。

备注:内容仅供参考。