diff --git a/kapital_bank/mapping.py b/kapital_bank/mapping.py index 09c697c..cf9a29f 100644 --- a/kapital_bank/mapping.py +++ b/kapital_bank/mapping.py @@ -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)