PAYATHON 2026

通过 API 将指定 Outstanding payment 添加到 Odoo 发票

支付老李

结论

在 Odoo 15 中,“Ajouter / Add”按钮调用的是 account.move 模型的以下方法:

js_assign_outstanding_line(line_id)

这里的 line_id 指的不是 account.payment ID,而是付款生成的应收或应付 account.move.line ID。

完整的处理步骤如下:

  1. 确认 payment 已过账;
  2. 确认 invoice 已过账;
  3. 找到 invoice 的应收或应付科目;
  4. 在 payment 对应的会计分录中,找到相同科目的未核销行;
  5. 在 invoice 上调用 js_assign_outstanding_line。

XML-RPC 示例

以下示例假设 XML-RPC 已连接,并且已经取得:

  • models:xmlrpc.client.ServerProxy;
  • db、uid、password;
  • payment_id;
  • invoice_id。
# 1. 读取付款及其对应的会计分录
payment = models.execute_kw(
    db,
    uid,
    password,
    'account.payment',
    'read',
    [[payment_id]],
    {
        'fields': ['state', 'move_id', 'partner_id', 'company_id']
    }
)[0]

if payment['state'] != 'posted':
    models.execute_kw(
        db,
        uid,
        password,
        'account.payment',
        'action_post',
        [[payment_id]]
    )

payment_move_id = payment['move_id'][0]

接着查找 invoice 的应收或应付行:

invoice_lines = models.execute_kw(
    db,
    uid,
    password,
    'account.move.line',
    'search_read',
    [[
        ('move_id', '=', invoice_id),
        ('account_id.user_type_id.type', 'in', ['receivable', 'payable']),
    ]],
    {
        'fields': [
            'id',
            'account_id',
            'partner_id',
            'company_id',
            'reconciled',
            'amount_residual',
        ],
        'limit': 1,
    }
)

if not invoice_lines:
    raise RuntimeError(
        '未找到 invoice 的应收/应付行,请确认 invoice 已过账'
    )

invoice_line = invoice_lines[0]
invoice_account_id = invoice_line['account_id'][0]

再到 payment 的会计分录中查找同一科目的未核销行:

payment_lines = models.execute_kw(
    db,
    uid,
    password,
    'account.move.line',
    'search_read',
    [[
        ('move_id', '=', payment_move_id),
        ('account_id', '=', invoice_account_id),
        ('reconciled', '=', False),
    ]],
    {
        'fields': [
            'id',
            'account_id',
            'partner_id',
            'company_id',
            'debit',
            'credit',
            'amount_residual',
            'amount_residual_currency',
        ],
    }
)

payment_lines = [
    line
    for line in payment_lines
    if line['amount_residual'] != 0
]

if len(payment_lines) != 1:
    raise RuntimeError(
        f'无法唯一确定付款的 outstanding line,找到 {len(payment_lines)} 条候选记录'
    )

outstanding_line_id = payment_lines[0]['id']

最后,模拟界面上的“Ajouter”操作:

result = models.execute_kw(
    db,
    uid,
    password,
    'account.move',
    'js_assign_outstanding_line',
    [[invoice_id], outstanding_line_id]
)

print(result)

其中最核心的调用可以简化为:

models.execute_kw(
    db,
    uid,
    password,
    'account.move',
    'js_assign_outstanding_line',
    [[invoice_id], outstanding_line_id]
)

为什么不能直接传 payment_id

account.payment 是付款业务对象。付款过账后,Odoo 会生成相应的会计分录,通常包括:

  • 银行或现金科目行;
  • 客户应收或供应商应付科目行。

发票核销使用的是第二类会计行,也就是 account.move.line。界面中的 outstanding credits 列表展示的同样是这些尚未核销的应收或应付行,而不是 account.payment 记录。

对应的调用关系如下:

account.payment
    → move_id
    → account.move.line
    → js_assign_outstanding_line(line_id)

使用前提

付款和发票通常需要满足这些条件:

  • 两者都已过账,状态为 posted;
  • 属于同一公司;
  • 客户或供应商一致;
  • 使用相同的应收或应付科目;
  • payment 的会计行仍有未核销余额;
  • payment 与 invoice 的余额方向相反,可以互相核销;
  • 当前 XML-RPC 用户具有相应的会计读取和核销权限。

调用前还可以核对客户和公司:

payment_line = payment_lines[0]

if payment_line['company_id'][0] != invoice_line['company_id'][0]:
    raise RuntimeError('付款和发票不属于同一公司')

if payment_line['partner_id'][0] != invoice_line['partner_id'][0]:
    raise RuntimeError('付款和发票的客户或供应商不一致')

如果系统使用商业伙伴层级,还需要根据实际配置比较 commercial_partner_id,以免联系人与母公司的关系导致误判。

注意事项

js_assign_outstanding_line 是 Odoo 15 中供发票界面调用的服务端方法。它会将指定的 outstanding line 与发票上同一应收或应付科目的未核销行进行 reconciliation。

不要直接操作 invoice_payments_widget 或 invoice_outstanding_credits_debits_widget。这些字段主要用于界面展示,通常是计算字段,并不是添加付款的写入入口。

付款已经部分核销时,仍可使用剩余金额,但最终可能只完成部分核销。如果币种不同、存在汇率差异或金额不完全相等,Odoo 还可能生成部分核销关系或汇兑差额分录。调用完成后,应重新读取 invoice 的以下字段:

payment_state
amount_residual
invoice_payments_widget

根据这些字段确认最终的核销状态。

备注:内容仅供参考。