PAYATHON 2026

通过 REST API 在 ERPNext 中执行 Payment Reconciliation

支付小周

结论

Payment Reconciliation 是正确的业务入口,但仅靠创建并提交记录并不能完成核销。实际更新应收账款明细和发票 outstanding_amount 的,是该 DocType 控制器中的 reconcile() 方法。

因此,当前请求成功只能说明文档已经创建或提交,不能说明核销逻辑已经执行。一般也不需要手工创建 Journal Entry、修改 GL Entry,或者直接更新发票状态。

通常有两种处理方式:

  1. 创建 Payment Entry 时,在 references 子表中直接关联 Sales Invoice,然后提交 Payment Entry。
  2. 对已经提交但尚未分配的付款,通过 Payment Reconciliation 加载未核销项目,填写分配金额,再显式执行 reconcile()。

推荐方案:在 Payment Entry 中直接分配发票

如果付款还没有创建,最简单可靠的做法是在创建 Payment Entry 时填写 references:

{
  "doctype": "Payment Entry",
  "payment_type": "Receive",
  "company": "Your Company",
  "party_type": "Customer",
  "party": "CUST-0001",
  "paid_from": "Debtors - YC",
  "paid_to": "Bank - YC",
  "paid_amount": 1000,
  "received_amount": 1000,
  "references": [
    {
      "reference_doctype": "Sales Invoice",
      "reference_name": "ACC-SINV-2026-00001",
      "total_amount": 1000,
      "outstanding_amount": 1000,
      "allocated_amount": 1000
    }
  ]
}

创建后提交:

import requests

payment = {
    "doctype": "Payment Entry",
    "payment_type": "Receive",
    "company": company,
    "party_type": "Customer",
    "party": customer,
    "paid_from": receivable_account,
    "paid_to": bank_account,
    "paid_amount": allocated_amount,
    "received_amount": allocated_amount,
    "references": [
        {
            "reference_doctype": "Sales Invoice",
            "reference_name": invoice,
            "allocated_amount": allocated_amount
        }
    ]
}

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

payment_doc = create_res.json()["data"]

submit_res = requests.post(
    f"{erpnext_url}/api/method/frappe.client.submit",
    headers=headers,
    json={"doc": payment_doc},
)
submit_res.raise_for_status()

提交 Payment Entry 后,ERPNext 会生成相应的总账记录,并根据 references 更新发票的未结金额。不要通过 REST API 直接修改 Sales Invoice.outstanding_amount。

已有 Payment Entry 时如何核销

已有付款需要满足以下条件:

  • Payment Entry.docstatus == 1
  • Sales Invoice.docstatus == 1
  • 付款和发票属于同一 company
  • party_type 和 party 相同
  • 应收账户一致
  • 付款仍有 unallocated_amount
  • 发票仍有 outstanding_amount
  • 付款没有被取消
  • 币种、汇率和账户维度符合当前账套要求

一个常见错误是使用 paid_amount 计算可分配金额。对于已经提交的付款,应优先使用 unallocated_amount:

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

查询付款时,建议至少包含以下字段:

params={
    "fields": (
        '["name","docstatus","company","party_type","party",'
        '"payment_type","paid_from","paid_to","paid_amount",'
        '"received_amount","unallocated_amount","posting_date"]'
    )
}

客户收款,即 payment_type == "Receive" 时,应收账户通常是 paid_from。供应商付款,即 payment_type == "Pay" 时,应付账户通常是 paid_to。不能对所有付款类型都固定比较 paid_from。

为什么当前 payload 没有生效

主要有三个风险点。

1. 只提交文档,没有调用 reconcile()

Payment Reconciliation 真正执行核销的是业务方法 reconcile()。创建资源并调用类似下面的接口:

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

并不等同于点击界面中的 “Reconcile” 按钮。

此外,/api/resource/{doctype}/{name}/submit 不是所有 Frappe/ERPNext 版本都支持的标准提交方式。通常应该使用:

/api/method/frappe.client.submit

即使文档提交成功,也不代表 Payment Reconciliation.reconcile() 已经执行。

2. 子表字段名可能不正确

代码中使用了:

"payment_reconciliation_invoice": [...]
"payment_reconciliation_payment": [...]

这些名称可能是子 DocType 的名称,而不是 Payment Reconciliation 主文档中的实际字段名。不同 ERPNext 版本的字段结构可能不同。常见字段名包括 invoices、payments,有些版本还会使用单独的分配明细结构。

具体字段必须以当前站点的元数据为准,可以查询:

GET /api/resource/DocType/Payment Reconciliation

重点检查:

  • 主文档中 Table 类型字段的 fieldname
  • Table 字段对应的 options
  • 子表中的付款编号、发票编号和分配金额字段名

如果传入的键不是有效字段,Frappe 可能会忽略这些数据,最终保存出一个没有有效核销明细的文档。

3. Payment Reconciliation 需要先加载未核销数据

界面中的 Payment Reconciliation 通常不是简单地手工拼接两张子表。系统会先根据以下条件加载符合要求的未核销发票和付款:

  • company
  • party_type
  • party
  • receivable_payable_account

用户填写分配金额后,系统才会调用 reconcile()。

直接构造一份看似完整的 JSON,可能会漏掉当前版本所需的字段、汇率、未分配金额、引用类型,或者内部校验依赖的数据。

通过 API 显式调用 reconcile()

Frappe 支持调用文档上的白名单方法,但具体入口会因 Frappe/ERPNext 版本和部署配置而异。有些版本的前端通过 run_doc_method 调用文档方法,例如:

import json
import requests

doc_res = requests.get(
    f"{erpnext_url}/api/resource/Payment Reconciliation/{rec_name}",
    headers=headers,
)
doc_res.raise_for_status()
reconciliation_doc = doc_res.json()["data"]

run_res = requests.post(
    f"{erpnext_url}/api/method/run_doc_method",
    headers=headers,
    data={
        "docs": json.dumps(reconciliation_doc),
        "method": "reconcile",
        "args": "{}",
    },
)
run_res.raise_for_status()

这段调用需要在当前版本中验证。如果返回 “方法不存在”、“not permitted” 或参数格式错误,可以打开浏览器开发者工具,查看在 ERPNext 界面中点击 “Reconcile” 按钮时实际发送的请求。

更可靠的做法是提供一个很小的自定义白名单方法,由服务端直接调用当前版本的 Python API:

import frappe

@frappe.whitelist()
def reconcile_payment(invoice, payment_entry, allocated_amount):
    # 在这里读取并验证 Sales Invoice、Payment Entry,
    # 然后按当前 ERPNext 版本构造 Payment Reconciliation,
    # 最后显式调用 doc.reconcile()。
    pass

外部系统只需调用这个受控接口:

res = requests.post(
    f"{erpnext_url}/api/method/your_app.api.reconcile_payment",
    headers=headers,
    json={
        "invoice": invoice,
        "payment_entry": payment_entry,
        "allocated_amount": allocated_amount,
    },
)
res.raise_for_status()

这样可以减少外部代码对内部子表字段名的依赖,也方便加入权限、幂等和金额校验。

不要手工更新 Ledger

正常核销不需要执行以下操作:

  • 直接篡改 GL Entry
  • 直接修改 Payment Ledger Entry
  • 直接设置 Sales Invoice.status
  • 直接设置 Sales Invoice.outstanding_amount
  • 为普通的未分配收款额外创建 Journal Entry

发票状态和未结金额是账务处理的结果,不应作为核销入口。直接修改这些字段,可能导致总账、付款台账和单据状态不一致。

只有折扣、汇兑损益、坏账、舍入差额等确实需要会计分录的情况,ERPNext 核销流程才可能生成相应分录,或者要求配置相关账户。

排查清单

核销前后,建议查询以下数据:

GET /api/resource/Sales Invoice/{invoice}
GET /api/resource/Payment Entry/{payment_entry}

确认:

Sales Invoice.docstatus = 1
Payment Entry.docstatus = 1
Sales Invoice.customer = Payment Entry.party
Sales Invoice.company = Payment Entry.company
Sales Invoice.outstanding_amount > 0
Payment Entry.unallocated_amount > 0

还需要检查:

  • 客户收款的 paid_from 是否等于发票的 debit_to
  • 是否存在币种或汇率差异
  • 是否使用付款的 unallocated_amount,而不是原始 paid_amount
  • API 用户是否拥有 Payment Reconciliation、Payment Entry 和 Sales Invoice 的相应权限
  • reconcile() 请求是否确实发出
  • 响应是否只表示文档保存成功,却没有实际分配结果
  • Error Log 和应用日志中是否记录了后台校验异常
  • 核销后是否重新读取付款的 unallocated_amount 和发票的 outstanding_amount,而不是只看 HTTP 200

如果业务流程允许,优先在创建 Payment Entry 时填写 references。只有付款已经入账但没有指定发票时,再使用 Payment Reconciliation。

备注:内容仅供参考。