From 86f4bf6e0166f3049a21d5b23765aab82e562cbb Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 29 Sep 2023 15:19:09 +0530 Subject: [PATCH 1/3] fix: Set right party name in bank transaction - If party name and docname are different, set the docname in Bank Transaction --- .../doctype/bank_transaction/auto_match_party.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index a1271b9c32a..997aded5465 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -1,4 +1,5 @@ import frappe +from frappe.core.utils import find from frappe.utils import flt from rapidfuzz import fuzz, process @@ -113,7 +114,8 @@ class AutoMatchbyPartyNameDescription: for party in parties: filters = {"status": "Active"} if party == "Employee" else {"disabled": 0} - names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name") + field = party.lower() + "_name" + names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"]) for field in ["bank_party_name", "description"]: if not self.get(field): @@ -132,12 +134,18 @@ class AutoMatchbyPartyNameDescription: def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None: skip = False - result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio) + result = process.extract( + query=self.get(field), + choices=[name.get("party_name") for name in names], + scorer=fuzz.token_set_ratio, + ) party_name, skip = self.process_fuzzy_result(result) if not party_name: return None, skip + # Get Party Docname from the list of dicts + party_name = find(names, lambda x: x["party_name"] == party_name).get("name") return ( party, party_name, From 153e961df75a9766512c1b9fc70c502be0df6986 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 31 Oct 2023 16:53:08 +0100 Subject: [PATCH 2/3] fix: Use `process.extract` to get the corresponding party doc name of the result - rapidfuzz accepts an iterable or a dict. dict input gives the dict key and value in the result --- .../doctype/bank_transaction/auto_match_party.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index 997aded5465..8aa870f6dce 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -1,5 +1,4 @@ import frappe -from frappe.core.utils import find from frappe.utils import flt from rapidfuzz import fuzz, process @@ -136,7 +135,7 @@ class AutoMatchbyPartyNameDescription: skip = False result = process.extract( query=self.get(field), - choices=[name.get("party_name") for name in names], + choices={row.get("name"): row.get("party_name") for row in names}, scorer=fuzz.token_set_ratio, ) party_name, skip = self.process_fuzzy_result(result) @@ -144,8 +143,6 @@ class AutoMatchbyPartyNameDescription: if not party_name: return None, skip - # Get Party Docname from the list of dicts - party_name = find(names, lambda x: x["party_name"] == party_name).get("name") return ( party, party_name, @@ -158,14 +155,14 @@ class AutoMatchbyPartyNameDescription: Returns: Result, Skip (whether or not to discontinue matching) """ - PARTY, SCORE, CUTOFF = 0, 1, 80 + SCORE, PARTY_ID, CUTOFF = 1, 2, 80 if not result or not len(result): return None, False first_result = result[0] if len(result) == 1: - return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True + return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True second_result = result[1] if first_result[SCORE] > CUTOFF: @@ -174,7 +171,7 @@ class AutoMatchbyPartyNameDescription: if first_result[SCORE] == second_result[SCORE]: return None, True - return first_result[PARTY], True + return first_result[PARTY_ID], True else: return None, False From 60feb7cbd42090235bb3f1ad61aa86eeddcc97eb Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 7 Jan 2025 13:21:30 +0530 Subject: [PATCH 3/3] fix: Wrong `bank_ac_no` filter + simplify convoluted logic --- .../bank_transaction/auto_match_party.py | 85 ++++++++----------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index 8aa870f6dce..66aab9d62dd 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -45,45 +45,41 @@ class AutoMatchbyAccountIBAN: if not (self.bank_party_account_number or self.bank_party_iban): return None - result = self.match_account_in_party() - return result + return self.match_account_in_party() def match_account_in_party(self) -> tuple | None: - """Check if there is a IBAN/Account No. match in Customer/Supplier/Employee""" - result = None - parties = get_parties_in_order(self.deposit) - or_filters = self.get_or_filters() + """ + Returns (Party Type, Party) if a matching account is found in Bank Account or Employee: + 1. Get party from a matching (iban/account no) Bank Account + 2. If not found, get party from Employee with matching bank account details (iban/account no) + """ + if not (self.bank_party_account_number or self.bank_party_iban): + # Nothing to match + return None - for party in parties: - party_result = frappe.db.get_all( - "Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1 - ) + # Search for a matching Bank Account that has party set + party_result = frappe.db.get_all( + "Bank Account", + or_filters=self.get_or_filters(), + filters={"party_type": ("is", "set"), "party": ("is", "set")}, + fields=["party", "party_type"], + limit_page_length=1, + ) + if result := party_result[0] if party_result else None: + return (result["party_type"], result["party"]) - if party == "Employee" and not party_result: - # Search in Bank Accounts first for Employee, and then Employee record - if "bank_account_no" in or_filters: - or_filters["bank_ac_no"] = or_filters.pop("bank_account_no") + # If no party is found, search in Employee (since it has bank account details) + if employee_result := frappe.db.get_all( + "Employee", or_filters=self.get_or_filters("Employee"), pluck="name", limit_page_length=1 + ): + return ("Employee", employee_result[0]) - party_result = frappe.db.get_all( - party, or_filters=or_filters, pluck="name", limit_page_length=1 - ) - - if "bank_ac_no" in or_filters: - or_filters["bank_account_no"] = or_filters.pop("bank_ac_no") - - if party_result: - result = ( - party, - party_result[0], - ) - break - - return result - - def get_or_filters(self) -> dict: + def get_or_filters(self, party: str | None = None) -> dict: + """Return OR filters for Bank Account and IBAN""" or_filters = {} if self.bank_party_account_number: - or_filters["bank_account_no"] = self.bank_party_account_number + bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no" + or_filters[bank_ac_field] = self.bank_party_account_number if self.bank_party_iban: or_filters["iban"] = self.bank_party_iban @@ -103,8 +99,7 @@ class AutoMatchbyPartyNameDescription: if not (self.bank_party_name or self.description): return None - result = self.match_party_name_desc_in_party() - return result + return self.match_party_name_desc_in_party() def match_party_name_desc_in_party(self) -> tuple | None: """Fuzzy search party name and/or description against parties in the system""" @@ -113,7 +108,7 @@ class AutoMatchbyPartyNameDescription: for party in parties: filters = {"status": "Active"} if party == "Employee" else {"disabled": 0} - field = party.lower() + "_name" + field = f"{party.lower()}_name" names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"]) for field in ["bank_party_name", "description"]: @@ -140,13 +135,7 @@ class AutoMatchbyPartyNameDescription: ) party_name, skip = self.process_fuzzy_result(result) - if not party_name: - return None, skip - - return ( - party, - party_name, - ), skip + return ((party, party_name), skip) if party_name else (None, skip) def process_fuzzy_result(self, result: list | None): """ @@ -164,8 +153,8 @@ class AutoMatchbyPartyNameDescription: if len(result) == 1: return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True - second_result = result[1] if first_result[SCORE] > CUTOFF: + second_result = result[1] # If multiple matches with the same score, return None but discontinue matching # Matches were found but were too close to distinguish between if first_result[SCORE] == second_result[SCORE]: @@ -177,8 +166,8 @@ class AutoMatchbyPartyNameDescription: def get_parties_in_order(deposit: float) -> list: - parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive - if flt(deposit) > 0: - parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay - - return parties + return ( + ["Customer", "Supplier", "Employee"] # most -> least likely to pay us + if flt(deposit) > 0 + else ["Supplier", "Employee", "Customer"] # most -> least likely to receive from us + )