PAYATHON 2026

Odoo V16 API 创建 Payment on Account 后未计入客户余额

支付老李

结论

通过 API 直接创建 account.payment 时,Odoo 界面中的 onchange 不会自动补全字段。处理客户收款时,需要确保:

  • destination_account_id 指向该客户的应收账款科目;
  • payment_method_line_id 属于目标日记账的 inbound payment method;
  • 创建付款后调用 action_post 正式过账。

客户余额由已过账凭证中的 account.move.line 计算,而不是直接根据 account.payment 记录计算。相关分录行必须带有该客户,并使用应收账款类型的科目。即使付款已经出现在 Payments 和 Journal Entries 中,只要生成的分录没有正确记入客户应收科目,就不会影响 due balance。

原因

在 Odoo 界面中选择客户、日记账和付款类型时,客户端会触发多个 onchange,并自动确定:

  • 收款方式;
  • 收款日记账对应的 outstanding receipts 或银行科目;
  • 客户的应收账款科目;
  • 公司和币种;
  • 分录行上的客户信息。

通过 XML-RPC、JSON-RPC 或第三方 PHP 封装直接调用 create 时,这些界面层的 onchange 通常不会执行。只传入以下字段:

journal_id
partner_type
payment_type
partner_id
amount
date

虽然可能成功创建付款记录,但不一定能生成与界面操作相同的会计分录。

一笔尚未核销的客户预收款,其分录通常类似:

借:Outstanding Receipts / Bank
贷:Accounts Receivable

贷方的应收账款分录必须带有正确的 partner_id。之后,这笔贷方余额可以作为客户的未分配贷项,用于核销未来的发票。

解决步骤

1. 读取客户的应收账款科目

从 res.partner 读取:

property_account_receivable_id

该字段是 company-dependent property。调用 API 时,应使用与付款日记账相同的公司上下文,否则可能读到其他公司的应收科目。

2. 获取日记账的 inbound payment method line

Odoo 16 使用 payment_method_line_id。需要从 account.payment.method.line 中查找 journal_id = 8 且适用于 inbound payment 的记录。

不要写死属于其他日记账的 payment method line,否则付款分录可能使用错误的中间科目,也可能在过账时被拒绝。

3. 创建付款时显式传入关键字段

下面的 PHP 调用方式需要根据当前 $odoo 封装库调整,字段结构可以写成:

$partner = $odoo->get('res.partner', 'read', array(
    array($invoice['partner_id']),
    array('property_account_receivable_id')
));

$receivableAccountId =
    $partner[0]['property_account_receivable_id'][0];

$methodLineIds = $odoo->get(
    'account.payment.method.line',
    'search',
    array(array(
        array('journal_id', '=', 8),
        array('payment_type', '=', 'inbound')
    ))
);

if (empty($methodLineIds)) {
    throw new RuntimeException(
        'No inbound payment method line is configured for journal 8.'
    );
}

$paymentId = $odoo->get('account.payment', 'create', array(array(
    'date' => date('Y-m-d', $invoice['date']),
    'journal_id' => 8,
    'partner_type' => 'customer',
    'payment_type' => 'inbound',
    'partner_id' => $invoice['partner_id'],
    'destination_account_id' => $receivableAccountId,
    'payment_method_line_id' => $methodLineIds[0],
    'amount' => abs($invoice['amount']),
    'ref' => 'Legacy payment on account'
)));

如果日记账配置了多个 inbound payment method line,不要直接取第一条。应根据导入业务选择对应的方式,例如 Manual、Check 或其他本地化支付方式。

4. 通过模型方法过账

付款创建完成后,调用 action_post,不要直接修改 state:

$odoo->get(
    'account.payment',
    'action_post',
    array(array($paymentId))
);

在标准 Odoo 16 中,付款通常会从 draft 进入 posted。如果系统还有额外的 approve 步骤,这通常来自本地化模块或自定义模块。此时应调用该模块定义的正式业务方法,而不是直接写入状态字段。

如何验证分录是否正确

过账后,读取付款关联的 move_id,再检查对应的 line_ids。应收账款分录至少要满足以下条件:

  • partner_id 等于客户;
  • account_id 是客户的应收账款科目;
  • 对于客户 inbound payment,该行通常产生贷方金额;
  • 分录状态为 posted;
  • company_id 与客户应收科目、付款日记账一致;
  • 未分配付款的应收行尚未完全核销,即 reconciled = false。

如果付款仍然没有影响余额,可以读取分录行进行对比:

$payment = $odoo->get('account.payment', 'read', array(
    array($paymentId),
    array('move_id', 'state')
));

$moveId = $payment[0]['move_id'][0];

$lines = $odoo->get('account.move.line', 'search_read', array(
    array(array('move_id', '=', $moveId)),
    array(
        'account_id',
        'partner_id',
        'debit',
        'credit',
        'balance',
        'amount_residual',
        'reconciled'
    )
));

将这些分录行与通过界面创建的正常付款逐项比较,通常很快就能找到差异。需要比较的是最终生成的 account.move.line,而不是 account.payment 的表面字段。

注意事项

extract_state 与客户余额入账没有直接关系。它主要用于付款单据提取流程。除非已安装的模块明确要求,否则导入普通付款时通常不需要设置。

如果 destination_account_id 已经正确,但余额仍未变化,可以继续检查:

  • API 用户当前公司的 allowed_company_ids;
  • 客户的 property_account_receivable_id 是否属于同一公司;
  • journal_id = 8 是否配置了正确的 outstanding receipts account;
  • 分录的应收行是否带有客户;
  • 当前查看的是商业实体还是子联系人的余额;
  • 所谓 due balance 是否来自只统计发票的自定义报表。

未分配付款无需绑定 invoice_ids,也能进入客户应收账。它会先作为应收账款贷项存在,之后再通过 reconciliation 与发票核销。付款必须使用正确的收款方式正式过账,并生成带有客户信息的应收账款分录。

备注:内容仅供参考。