ERPNext REST API 返回成功但 Payment Reconciliation 未生效
结论
Payment Reconciliation 用于核销,但不能当作普通会计凭证处理。通过 /api/resource/Payment Reconciliation 创建记录后再调用 submit,通常只会保存或提交文档,不会触发核销。
发票的 outstanding_amount 由服务端的 reconcile 方法更新。这个方法会建立付款与发票的引用关系,并更新相应的总账和未结金额,无法用某个字段或 flag 代替。
具体可以按付款状态选择处理方式:
- Payment Entry 尚未提交时,直接在它的
references子表中分配发票,然后提交 Payment Entry。 - 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 的服务端流程:
- 设置
company、party_type、party和receivable_payable_account。 - 调用
get_unreconciled_entries,由 ERPNext 查询可核销的发票和付款。 - 从查询结果中选择目标 Sales Invoice 和 Payment Entry。
- 生成 allocation 数据,部分版本使用
allocate_entries生成。 - 调用
reconcile并传入 allocation 数据。 - 重新读取 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。
备注:内容仅供参考。
