PAYATHON 2026

ERPNext REST API 返回成功但 Payment Reconciliation 未生效

排障赵师傅

结论

Payment Reconciliation 用于核销,但不能当作普通会计凭证处理。通过 /api/resource/Payment Reconciliation 创建记录后再调用 submit,通常只会保存或提交文档,不会触发核销。

发票的 outstanding_amount 由服务端的 reconcile 方法更新。这个方法会建立付款与发票的引用关系,并更新相应的总账和未结金额,无法用某个字段或 flag 代替。

具体可以按付款状态选择处理方式:

  1. Payment Entry 尚未提交时,直接在它的 references 子表中分配发票,然后提交 Payment Entry。
  2. Payment Entry 已提交但没有指定发票时,调用 Payment Reconciliation 的 get_unreconciled_entries、allocate_entries 和 reconcile 等文档方法。生产环境中,建议在 ERPNext 自定义应用里封装白名单方法,再由外部系统调用。

通常无须额外创建 Journal Entry,也不要直接修改 GL Entry 或手工更新发票状态。

为什么当前代码返回成功却没有核销

下面两次请求只是在创建和提交文档:

POST /api/resource/Payment Reconciliation
POST /api/resource/Payment Reconciliation/{name}/submit

标准资源 API 负责创建、修改和提交文档,不会自动执行 Payment Reconciliation 页面上的“Get Unreconciled Entries”“Allocate”和“Reconcile”操作。

Payment Reconciliation 页面调用的是一组文档方法,其中真正执行核销的是 reconcile。HTTP 200 只表示当前文档操作成功,不能说明核销已经完成。

另外,下面两个字段名更像子 Doctype 名称,未必是当前 ERPNext 版本中父文档的实际 Table 字段名:

"payment_reconciliation_invoice"
"payment_reconciliation_payment"

常见版本中的父文档字段是:

invoices
payments
allocation

实际名称应以当前站点的 DocType metadata 和对应的 ERPNext 源码为准。即使子表字段填写正确,保存和提交文档也无法代替 reconcile 方法。

首选方案:在 Payment Entry 提交前分配发票

如果付款由接口创建,可以在创建 Payment Entry 时直接填写 references:

from decimal import Decimal
import requests

allocated_amount = min(
    Decimal(str(inv_data["outstanding_amount"])),
    Decimal(str(payment_amount))
)

payload = {
    "doctype": "Payment Entry",
    "payment_type": "Receive",
    "company": inv_data["company"],
    "party_type": "Customer",
    "party": inv_data["customer"],
    "paid_from": inv_data["debit_to"],
    "paid_to": bank_account,
    "paid_amount": float(allocated_amount),
    "received_amount": float(allocated_amount),
    "reference_no": external_payment_reference,
    "reference_date": posting_date,
    "references": [
        {
            "reference_doctype": "Sales Invoice",
            "reference_name": invoice,
            "allocated_amount": float(allocated_amount)
        }
    ]
}

create_res = requests.post(
    f"{erpnext_url}/api/resource/Payment Entry",
    headers=headers,
    json=payload,
    timeout=30
)
create_res.raise_for_status()

payment_entry = create_res.json()["data"]["name"]

submit_res = requests.post(
    f"{erpnext_url}/api/resource/Payment Entry/{payment_entry}/submit",
    headers=headers,
    timeout=30
)
submit_res.raise_for_status()

提交成功后,重新读取发票,检查核销结果:

invoice_res = requests.get(
    f"{erpnext_url}/api/resource/Sales Invoice/{invoice}",
    headers=headers,
    params={"fields": '["name","status","outstanding_amount"]'},
    timeout=30
)
invoice_res.raise_for_status()

print(invoice_res.json()["data"])

采用这种方式后,无须再创建 Payment Reconciliation 文档。

如何核销已提交的 Payment Entry

对于已经提交但尚未分配的 Payment Entry,需要执行 Payment Reconciliation 的服务端流程:

  1. 设置 company、party_type、party 和 receivable_payable_account。
  2. 调用 get_unreconciled_entries,由 ERPNext 查询可核销的发票和付款。
  3. 从查询结果中选择目标 Sales Invoice 和 Payment Entry。
  4. 生成 allocation 数据,部分版本使用 allocate_entries 生成。
  5. 调用 reconcile 并传入 allocation 数据。
  6. 重新读取 Sales Invoice,检查 outstanding_amount 是否已变化。

Frappe 的传统 REST 接口可以通过 run_doc_method 调用文档方法,大致写法如下:

import json
import requests

def run_doc_method(doc, method, args=None):
    response = requests.post(
        f"{erpnext_url}/api/method/run_doc_method",
        headers=headers,
        data={
            "docs": json.dumps(doc),
            "method": method,
            "args": json.dumps(args or {})
        },
        timeout=30
    )
    response.raise_for_status()
    return response.json()

调用 reconcile 时,结构类似:

result = run_doc_method(
    reconciliation_doc,
    "reconcile",
    {
        "allocations": allocation_rows
    }
)

reconciliation_doc 不能只包含最初手工拼出的几个字段,还需要带上 get_unreconciled_entries 返回的付款、发票和相关数据。allocation_rows 也应按照当前版本中 allocate_entries 返回的结构生成,不要根据子 DocType 名称猜测字段。

不同 Frappe 版本在文档方法路由、参数序列化方式和 Payment Reconciliation 子表结构上可能存在差异。如果当前版本不支持 /api/method/run_doc_method,可以查看该站点 Payment Reconciliation 页面在浏览器开发者工具中发出的请求,或者在自定义应用中封装调用。

在服务端封装 ERPNext 接口

让外部系统直接维护 Payment Reconciliation 的完整文档状态容易受版本变化影响。ERPNext 升级后,子表字段或方法参数可能会改变。

可以在 ERPNext 自定义应用中提供一个白名单方法,由服务端完成核销流程:

@frappe.whitelist()
def reconcile_payment(invoice_name, payment_entry_name, allocated_amount=None):
    # 1. 读取并锁定 Sales Invoice 和 Payment Entry
    # 2. 验证 docstatus、company、party、account 和 currency
    # 3. 创建 Payment Reconciliation 内存文档
    # 4. 调用 get_unreconciled_entries
    # 5. 从返回结果中定位指定 invoice 和 payment
    # 6. 生成当前 ERPNext 版本要求的 allocation
    # 7. 调用 reconcile
    # 8. 返回更新后的 outstanding_amount
    pass

外部 Python 程序只需调用这个方法:

response = requests.post(
    f"{erpnext_url}/api/method/my_app.api.reconcile_payment",
    headers=headers,
    data={
        "invoice_name": invoice,
        "payment_entry_name": payment_entry,
        "allocated_amount": str(allocated_amount)
    },
    timeout=30
)
response.raise_for_status()

服务端实现应参考当前部署版本中 Payment Reconciliation.reconcile 和 allocate_entries 的方法签名。不同 ERPNext 版本的 allocation 字段不一定相同。

核销前需要检查的条件

发票和付款除了属于同一 Customer,还要满足以下条件:

  • Sales Invoice 和 Payment Entry 均已提交,即 docstatus == 1。
  • Customer、Company 和应收账户一致。
  • Customer 收款通常要求 payment_type == "Receive"。
  • paid_from 应是发票使用的应收账户,paid_to 通常是银行或现金账户。
  • Payment Entry 仍有未分配金额,不能直接把 paid_amount 当作全部可用金额。
  • 发票的 outstanding_amount 仍大于零。
  • allocation 不能超过付款的未分配金额或发票的未结金额。
  • 多币种付款还要检查 paid_from_account_currency、paid_to_account_currency、汇率和差额处理。
  • 财务金额不要使用二进制 float 计算。应优先使用 Decimal,最终精度由 ERPNext 的币种精度规则处理。

当前代码使用的是:

allocated_amount = min(
    float(inv_data["outstanding_amount"]),
    float(pay_data["paid_amount"])
)

建议改为:

from decimal import Decimal

invoice_outstanding = Decimal(str(inv_data["outstanding_amount"]))
payment_unallocated = Decimal(str(pay_data["unallocated_amount"]))

allocated_amount = min(invoice_outstanding, payment_unallocated)

因此,读取 Payment Entry 时还应请求 docstatus、party_type、company、unallocated_amount 和币种相关字段,不能只读取 paid_amount。

是否需要 Journal Entry 或手工更新 ledger

正常情况下不需要。Payment Entry 已经生成了总账记录,Payment Reconciliation 会把现有付款与发票关联起来,并通过 ERPNext 内置的会计逻辑更新未结金额。再次创建 Journal Entry 可能导致重复入账。

也不要通过 REST API 直接修改以下数据:

Sales Invoice.outstanding_amount
Sales Invoice.status
GL Entry
Payment Ledger Entry

这些值应由 ERPNext 的会计流程计算和维护。手工修改可能导致发票、总账和 Payment Ledger 数据不一致。

如何确认核销结果

HTTP 200 只能证明接口请求成功。核销完成后,还应重新读取并核对:

Sales Invoice.outstanding_amount
Sales Invoice.status
Payment Entry.unallocated_amount
Payment Entry.references
Payment Ledger Entry

如果调用 reconcile 后数据仍未更新,应记录完整的响应体和服务器日志,重点检查权限、账户、币种、已分配金额及目标单据的 docstatus。

备注:内容仅供参考。