diff --git a/jey_erp/bank_integration/create_reconcile.py b/jey_erp/bank_integration/create_reconcile.py index 4c3e475..f03b345 100644 --- a/jey_erp/bank_integration/create_reconcile.py +++ b/jey_erp/bank_integration/create_reconcile.py @@ -48,6 +48,40 @@ 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}, + built from the mapped rows of customer_mappings / supplier_mappings.""" + idx = {} + for row in settings.customer_mappings: + if not row.erp_customer or not row.bi_customer_name: + continue + cust_name = frappe.db.get_value("Bank Integration Customer", row.bi_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.bi_supplier_name: + continue + supp_name = frappe.db.get_value("Bank Integration Supplier", row.bi_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, bank_integration, paid_from=None, paid_to=None, document_type="Payment Entry", mode="Both"): @@ -67,6 +101,20 @@ def create_purpose_mappings(transactions, bank_integration, paid_from=None, paid settings = frappe.get_doc("Bank Integration", bank_integration) + # Bank Transactions are imported without a party (the mapping is what tells + # us who they are). 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()) @@ -457,9 +505,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) diff --git a/jey_erp/bank_integration/import_api.py b/jey_erp/bank_integration/import_api.py index eb3d091..a37d5fc 100644 --- a/jey_erp/bank_integration/import_api.py +++ b/jey_erp/bank_integration/import_api.py @@ -48,13 +48,13 @@ def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_accoun @frappe.whitelist() -def import_bulk_bt(txn_list, bank_integration, bank_account, load_registries=1): +def import_bulk_bt(txn_list, bank_integration, bank_account): """Enqueue bulk Bank Transaction creation as a background job. - load_registries: when truthy, also upsert counterparties (→ Bank Integration - Customer/Supplier by direction) and purpose keywords (→ Bank Integration - Purpose) with status=New, exactly as before. When falsy, only Bank - Transactions are created. + Creates Bank Transactions only — registries (Bank Integration Customer / + Supplier / Purpose) are populated separately via the "Load Data" button on + the Bank Integration form. Party is resolved later, at "Create & Reconcile" + time, from the mappings. """ if isinstance(txn_list, str): txn_list = json.loads(txn_list) @@ -67,7 +67,6 @@ def import_bulk_bt(txn_list, bank_integration, bank_account, load_registries=1): bank_integration=bank_integration, bank_account=bank_account, user=user, - load_registries=cint(load_registries), queue="default", timeout=600, ) @@ -75,8 +74,8 @@ def import_bulk_bt(txn_list, bank_integration, bank_account, load_registries=1): return {"success": True, "enqueued": True, "total": len(txn_list)} -def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, load_registries=1): - """Background job: create BT (+ optionally upsert registry rows) for each transaction.""" +def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user): + """Background job: create one Bank Transaction per row.""" frappe.set_user(user) bi = frappe.get_doc("Bank Integration", bank_integration) @@ -91,9 +90,6 @@ def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, load total = len(txn_list) imported_count = 0 errors = [] - new_customers = 0 - new_suppliers = 0 - new_purposes = 0 for idx, txn in enumerate(txn_list): frappe.publish_realtime( @@ -104,24 +100,8 @@ def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, load try: counterparty = (txn.get("counterparty") or "").strip() - contr_voen = (txn.get("contr_voen") or "").strip() - contr_iban = (txn.get("contr_iban") or "").strip() drcr = (txn.get("drcr") or "D").upper() purpose = (txn.get("purpose") or "").strip() - - if load_registries: - if counterparty: - if drcr == "C": - if _upsert_customer(counterparty, contr_voen, contr_iban, bank_integration): - new_customers += 1 - else: - if _upsert_supplier(counterparty, contr_voen, contr_iban, bank_integration): - new_suppliers += 1 - - if purpose: - if _upsert_purpose(purpose, drcr, bank_integration): - new_purposes += 1 - ref_no = (txn.get("ref_no") or "").strip() amount = abs(flt(txn.get("amount", 0))) @@ -163,21 +143,10 @@ def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, load frappe.publish_realtime( "bi_bt_import_complete", - { - "total": total, - "imported": imported_count, - "errors": errors, - "new_customers": new_customers, - "new_suppliers": new_suppliers, - "new_purposes": new_purposes, - }, + {"total": total, "imported": imported_count, "errors": errors}, user=user, ) - frappe.log_error( - f"BI BT import done: {imported_count}/{total}, " - f"+{new_customers} customers / +{new_suppliers} suppliers / +{new_purposes} purposes", - "BI BT Import Complete", - ) + frappe.log_error(f"BI BT import done: {imported_count}/{total}", "BI BT Import Complete") def _upsert_customer(name, voen, iban, bank_integration): diff --git a/jey_erp/public/js/bank_transaction_list.js b/jey_erp/public/js/bank_transaction_list.js index 99ed795..3481be1 100644 --- a/jey_erp/public/js/bank_transaction_list.js +++ b/jey_erp/public/js/bank_transaction_list.js @@ -84,13 +84,6 @@ const BIExcelImport = { label: __('Excel File'), reqd: 1, }, - { - fieldname: 'load_registries', - fieldtype: 'Check', - label: __('Also load counterparties & purposes'), - default: 1, - description: __('Extract unique counterparties and purpose keywords from the file into the Bank Integration registries (status: New).'), - }, ], primary_action_label: __('Parse'), primary_action(values) { @@ -221,13 +214,9 @@ const BIExcelImport = { const ok = d.imported || 0; const errCount = (d.errors || []).length; - const newC = d.new_customers || 0; - const newS = d.new_suppliers || 0; - const newP = d.new_purposes || 0; let msg = '