使用 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完成付款和核销。
备注:内容仅供参考。
