feat: resolve party at Create & Reconcile time from the mappings

A Bank Transaction created before its counterparty was mapped carries no
party. When reconciling, look up the BT's bank_party_name in the
customer/supplier mappings — if that counterparty is mapped to an
ERPNext party, the txn gets it, so counterparty-based rules match and
the created PE/JE carries the party. Also: only attach a party to a
Payment Entry when one of the GL accounts is Receivable/Payable
(otherwise ERPNext rejects it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-05-12 13:01:33 +00:00
parent e589dcdaf4
commit 39c8733099
1 changed files with 54 additions and 2 deletions

View File

@ -37,6 +37,39 @@ def _extract_messages(message_log):
return " | ".join(p for p in parts if p)
def _build_name_to_party(settings):
"""Counterparty text -> {"Customer": erp_customer} / {"Supplier": erp_supplier}."""
idx = {}
for row in settings.customer_mappings:
if not row.erp_customer or not row.kb_customer_name:
continue
cust_name = frappe.db.get_value("Kapital Bank Customer", row.kb_customer_name, "customer_name")
if cust_name:
idx.setdefault(cust_name.strip(), {})["Customer"] = row.erp_customer
for row in settings.supplier_mappings:
if not row.erp_supplier or not row.kb_supplier_name:
continue
supp_name = frappe.db.get_value("Kapital Bank Supplier", row.kb_supplier_name, "supplier_name")
if supp_name:
idx.setdefault(supp_name.strip(), {})["Supplier"] = row.erp_supplier
return idx
def _resolve_party_from_name(name, name_to_party, payment_type):
"""(party_type, erp_party) for `name`, or (None, None). Prefers Customer for
incoming (Receive) and Supplier for outgoing (Pay), falling back to the other."""
cands = name_to_party.get((name or "").strip())
if not cands:
return None, None
preferred = "Customer" if payment_type == "Receive" else "Supplier"
other = "Supplier" if preferred == "Customer" else "Customer"
if preferred in cands:
return preferred, cands[preferred]
if other in cands:
return other, cands[other]
return None, None
@frappe.whitelist()
def create_purpose_mappings(transactions, paid_from=None, paid_to=None, document_type="Payment Entry", mode="Both"):
"""Bulk-create transaction mappings and/or documents from selected BRT transactions."""
@ -57,6 +90,20 @@ def create_purpose_mappings(transactions, paid_from=None, paid_to=None, document
if needs_settings:
settings = frappe.get_single("Kapital Bank Settings")
# A Bank Transaction created before its counterparty was mapped has no
# party. Resolve it now, from the customer/supplier mappings, so both
# counterparty-based rule matching and the created PE/JE pick it up.
name_to_party = _build_name_to_party(settings)
for txn in txn_list:
if txn.get("party") or not txn.get("bank_transaction_name"):
continue
bp_name = frappe.db.get_value("Bank Transaction", txn["bank_transaction_name"], "bank_party_name")
payment_type = "Pay" if txn.get("drcr") == "D" else "Receive"
pt, ep = _resolve_party_from_name(bp_name, name_to_party, payment_type)
if pt and ep:
txn["party_type"] = pt
txn["party"] = ep
if mode in ("Both", "Mappings Only"):
existing_purpose = {
(row.purpose_keyword, row.payment_type, (row.currency or "").upper())
@ -459,9 +506,14 @@ def _create_payment_entry_for_brt(txn, paid_from, paid_to, bank_txn):
pe.reference_no = bank_txn.reference_number or txn.get("reference_number") or ""
pe.reference_date = posting_date
pe.remarks = bank_txn.description or txn.get("purpose") or ""
# Only attach a party if one of the GL accounts is a party account —
# otherwise ERPNext rejects the PE ("Party Account ... is required").
if party_type and party:
pe.party_type = party_type
pe.party = party
pf_type = frappe.get_cached_value("Account", paid_from, "account_type") or ""
pt_type = frappe.get_cached_value("Account", paid_to, "account_type") or ""
if pf_type in ("Receivable", "Payable") or pt_type in ("Receivable", "Payable"):
pe.party_type = party_type
pe.party = party
frappe.local.message_log = []
pe.insert(ignore_permissions=True)