通过 REST API 在 ERPNext 中执行 Payment Reconciliation
结论
Payment Reconciliation 是正确的业务入口,但仅靠创建并提交记录并不能完成核销。实际更新应收账款明细和发票 outstanding_amount 的,是该 DocType 控制器中的 reconcile() 方法。
因此,当前请求成功只能说明文档已经创建或提交,不能说明核销逻辑已经执行。一般也不需要手工创建 Journal Entry、修改 GL Entry,或者直接更新发票状态。
通常有两种处理方式:
- 创建
Payment Entry时,在references子表中直接关联Sales Invoice,然后提交Payment Entry。 - 对已经提交但尚未分配的付款,通过
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 == 1Sales 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 通常不是简单地手工拼接两张子表。系统会先根据以下条件加载符合要求的未核销发票和付款:
companyparty_typepartyreceivable_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。
备注:内容仅供参考。
