Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Auto set Party in Bank Transaction #34675

Merged
merged 25 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3a89828
chore: Single query with or filter to search Party Mapper by name/desc
marination Apr 3, 2023
ad31e02
feat: Store Party bank details in party records (Customer/Supplier/Em…
marination Mar 30, 2023
e774503
feat: Party auto-matcher from Bank Transaction data
marination Mar 31, 2023
37c1331
fix: Don't set description as key in Mapper doc if matched by descrip…
marination Apr 4, 2023
27ce789
feat: Manually Update/Correct Party in Bank Transaction
marination Apr 4, 2023
3360455
chore: Perform automatch on submit
marination Apr 4, 2023
aea4315
chore: Make auto matching party configurable
marination Apr 4, 2023
d7bc192
fix: Match by both Account No and IBAN & other cleanups
marination Apr 5, 2023
fcc8f9f
Merge branch 'develop' into bank-trans-party-automatch
marination Apr 5, 2023
36de35c
Merge branch 'develop' into bank-trans-party-automatch
marination Apr 10, 2023
7ed8f59
test: Match by Account No, IBAN, Party Name, Desc and match correction
marination Apr 10, 2023
430b247
fix: Remove bank details fields from Shareholder
marination Apr 10, 2023
fd38e8e
Merge branch 'develop' into bank-trans-party-automatch
marination Apr 17, 2023
88647c6
Merge branch 'develop' into bank-trans-party-automatch
marination Apr 24, 2023
dbf7a47
fix: Use existing bank fields to match by bank account no/IBAN
marination May 9, 2023
4a14e9e
fix: Tests
marination May 17, 2023
4364fb9
feat: Optional Fuzzy Matching & Skip Matches for multiple similar mat…
marination May 17, 2023
6fe5264
Merge branch 'develop' into bank-trans-party-automatch
marination May 18, 2023
0987230
Merge branch 'develop' into bank-trans-party-automatch
barredterra Jun 1, 2023
752a92b
chore: Remove Bank Party Mapper implementation
marination Jun 6, 2023
75387bb
Merge branch 'develop' into bank-trans-party-automatch
marination Jun 6, 2023
eb1db5e
chore: Remove instances of `bank_party_mapper` and use `new_doc`
marination Jun 7, 2023
51848ee
Merge branch 'develop' into bank-trans-party-automatch
barredterra Jun 15, 2023
1112652
Merge branch 'develop' into bank-trans-party-automatch
barredterra Jun 19, 2023
8ab8230
Merge branch 'develop' into bank-trans-party-automatch
marination Jun 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@
"column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
"show_balance_in_coa"
"show_balance_in_coa",
"banking_tab",
"enable_party_matching",
"enable_fuzzy_matching"
],
"fields": [
{
Expand Down Expand Up @@ -383,14 +386,34 @@
"fieldname": "show_taxes_as_table_in_print",
"fieldtype": "Check",
"label": "Show Taxes as Table in Print"
},
{
"fieldname": "banking_tab",
"fieldtype": "Tab Break",
"label": "Banking"
},
{
"default": "0",
"description": "Auto match and set the Party in Bank Transactions",
"fieldname": "enable_party_matching",
"fieldtype": "Check",
"label": "Enable Automatic Party Matching"
},
{
"default": "0",
"depends_on": "enable_party_matching",
"description": "Approximately match the description/party name against parties",
"fieldname": "enable_fuzzy_matching",
"fieldtype": "Check",
"label": "Enable Fuzzy Matching"
}
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-06-13 18:47:46.430291",
"modified": "2023-06-15 16:35:45.123456",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
Expand Down
178 changes: 178 additions & 0 deletions erpnext/accounts/doctype/bank_transaction/auto_match_party.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
from typing import Tuple, Union

import frappe
from frappe.utils import flt
from rapidfuzz import fuzz, process


class AutoMatchParty:
"""
Matches by Account/IBAN and then by Party Name/Description sequentially.
Returns when a result is obtained.

Result (if present) is of the form: (Party Type, Party,)
"""

def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)

def get(self, key):
return self.__dict__.get(key, None)

def match(self) -> Union[Tuple, None]:
result = None
result = AutoMatchbyAccountIBAN(
bank_party_account_number=self.bank_party_account_number,
bank_party_iban=self.bank_party_iban,
deposit=self.deposit,
).match()

fuzzy_matching_enabled = frappe.db.get_single_value("Accounts Settings", "enable_fuzzy_matching")
if not result and fuzzy_matching_enabled:
result = AutoMatchbyPartyNameDescription(
bank_party_name=self.bank_party_name, description=self.description, deposit=self.deposit
).match()

return result


class AutoMatchbyAccountIBAN:
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)

def get(self, key):
return self.__dict__.get(key, None)

def match(self):
if not (self.bank_party_account_number or self.bank_party_iban):
return None

result = self.match_account_in_party()
return result

def match_account_in_party(self) -> Union[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()

for party in parties:
party_result = frappe.db.get_all(
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
)

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")

party_result = frappe.db.get_all(
party, or_filters=or_filters, pluck="name", limit_page_length=1
)

if party_result:
result = (
party,
party_result[0],
)
break

return result

def get_or_filters(self) -> dict:
or_filters = {}
if self.bank_party_account_number:
or_filters["bank_account_no"] = self.bank_party_account_number

if self.bank_party_iban:
or_filters["iban"] = self.bank_party_iban

return or_filters


class AutoMatchbyPartyNameDescription:
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)

def get(self, key):
return self.__dict__.get(key, None)

def match(self) -> Union[Tuple, None]:
# fuzzy search by customer/supplier & employee
if not (self.bank_party_name or self.description):
return None

result = self.match_party_name_desc_in_party()
return result

def match_party_name_desc_in_party(self) -> Union[Tuple, None]:
"""Fuzzy search party name and/or description against parties in the system"""
result = None
parties = get_parties_in_order(self.deposit)

for party in parties:
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")

for field in ["bank_party_name", "description"]:
if not self.get(field):
continue

result, skip = self.fuzzy_search_and_return_result(party, names, field)
if result or skip:
break

if result or skip:
# Skip If: It was hard to distinguish between close matches and so match is None
# OR if the right match was found
break

return result

def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
skip = False
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
party_name, skip = self.process_fuzzy_result(result)

if not party_name:
return None, skip

return (
party,
party_name,
), skip

def process_fuzzy_result(self, result: Union[list, None]):
"""
If there are multiple valid close matches return None as result may be faulty.
Return the result only if one accurate match stands out.

Returns: Result, Skip (whether or not to discontinue matching)
"""
PARTY, SCORE, CUTOFF = 0, 1, 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

second_result = result[1]
if first_result[SCORE] > CUTOFF:
# 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]:
return None, True

return first_result[PARTY], True
else:
return None, False


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
31 changes: 27 additions & 4 deletions erpnext/accounts/doctype/bank_transaction/bank_transaction.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
"unallocated_amount",
"party_section",
"party_type",
"party"
"party",
"column_break_3czf",
"bank_party_name",
"bank_party_account_number",
"bank_party_iban"
],
"fields": [
{
Expand Down Expand Up @@ -63,7 +67,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"options": "\nPending\nSettled\nUnreconciled\nReconciled"
"options": "\nPending\nSettled\nUnreconciled\nReconciled\nCancelled"
},
{
"fieldname": "bank_account",
Expand Down Expand Up @@ -202,11 +206,30 @@
"fieldtype": "Data",
"label": "Transaction Type",
"length": 50
},
{
"fieldname": "column_break_3czf",
"fieldtype": "Column Break"
},
{
"fieldname": "bank_party_name",
"fieldtype": "Data",
"label": "Party Name/Account Holder (Bank Statement)"
},
{
"fieldname": "bank_party_iban",
"fieldtype": "Data",
"label": "Party IBAN (Bank Statement)"
},
{
"fieldname": "bank_party_account_number",
"fieldtype": "Data",
"label": "Party Account No. (Bank Statement)"
}
],
"is_submittable": 1,
"links": [],
"modified": "2022-05-29 18:36:50.475964",
"modified": "2023-06-06 13:58:12.821411",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
Expand Down Expand Up @@ -260,4 +283,4 @@
"states": [],
"title_field": "bank_account",
"track_changes": 1
}
}
23 changes: 23 additions & 0 deletions erpnext/accounts/doctype/bank_transaction/bank_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ def on_submit(self):
self.clear_linked_payment_entries()
self.set_status()

if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
self.auto_set_party()

_saving_flag = False

# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
Expand Down Expand Up @@ -146,6 +149,26 @@ def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
)

def auto_set_party(self):
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty

if self.party_type and self.party:
return

result = AutoMatchParty(
bank_party_account_number=self.bank_party_account_number,
bank_party_iban=self.bank_party_iban,
bank_party_name=self.bank_party_name,
description=self.description,
deposit=self.deposit,
).match()

if result:
party_type, party = result
frappe.db.set_value(
"Bank Transaction", self.name, field={"party_type": party_type, "party": party}
)


@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():
Expand Down
Loading