Merge branch 'develop' into bank-trans-party-automatch
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 2996836..05f1169 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -62,7 +62,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": [
{
@@ -390,6 +393,26 @@
"fieldname": "auto_reconcile_payments",
"fieldtype": "Check",
"label": "Auto Reconcile Payments"
+ },
+ {
+ "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",
@@ -397,7 +420,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-04-21 13:11:37.130743",
+ "modified": "2023-05-17 12:20:04.107641",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/bank_party_mapper/__init__.py b/erpnext/accounts/doctype/bank_party_mapper/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_party_mapper/__init__.py
diff --git a/erpnext/accounts/doctype/bank_party_mapper/bank_party_mapper.js b/erpnext/accounts/doctype/bank_party_mapper/bank_party_mapper.js
new file mode 100644
index 0000000..b13e46a
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_party_mapper/bank_party_mapper.js
@@ -0,0 +1,10 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Bank Party Mapper", {
+ refresh(frm) {
+ if (!frm.is_new()) {
+ frm.set_intro(__("Please avoid editing unless you are absolutely certain."));
+ }
+ },
+});
diff --git a/erpnext/accounts/doctype/bank_party_mapper/bank_party_mapper.json b/erpnext/accounts/doctype/bank_party_mapper/bank_party_mapper.json
new file mode 100644
index 0000000..ddc79f2
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_party_mapper/bank_party_mapper.json
@@ -0,0 +1,80 @@
+{
+ "actions": [],
+ "creation": "2023-03-31 10:48:20.249481",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "party_type",
+ "party",
+ "column_break_wbna",
+ "bank_party_name",
+ "bank_party_account_number",
+ "bank_party_iban"
+ ],
+ "fields": [
+ {
+ "fieldname": "bank_party_account_number",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Party Account No. (Bank Statement)"
+ },
+ {
+ "fieldname": "bank_party_iban",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Party IBAN (Bank Statement)"
+ },
+ {
+ "fieldname": "column_break_wbna",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "party_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Party Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "party",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Party",
+ "options": "party_type"
+ },
+ {
+ "fieldname": "bank_party_name",
+ "fieldtype": "Data",
+ "label": "Party Name (Bank Statement)"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2023-04-04 14:27:23.450456",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Bank Party Mapper",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_party_mapper/bank_party_mapper.py b/erpnext/accounts/doctype/bank_party_mapper/bank_party_mapper.py
new file mode 100644
index 0000000..02e36b0
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_party_mapper/bank_party_mapper.py
@@ -0,0 +1,24 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+
+
+class BankPartyMapper(Document):
+ def on_update(self):
+ self.update_party_in_linked_transactions()
+
+ def update_party_in_linked_transactions(self):
+ if self.is_new():
+ return
+
+ # Set updated party values in other linked bank transactions
+ bank_transaction = frappe.qb.DocType("Bank Transaction")
+
+ frappe.qb.update(bank_transaction).set("party_type", self.party_type).set(
+ "party", self.party
+ ).where(
+ (bank_transaction.bank_party_mapper == self.name)
+ & ((bank_transaction.party_type != self.party_type) | (bank_transaction.party != self.party))
+ ).run()
diff --git a/erpnext/accounts/doctype/bank_party_mapper/test_bank_party_mapper.py b/erpnext/accounts/doctype/bank_party_mapper/test_bank_party_mapper.py
new file mode 100644
index 0000000..c05b23f
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_party_mapper/test_bank_party_mapper.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBankPartyMapper(FrappeTestCase):
+ pass
diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
new file mode 100644
index 0000000..753f0c1
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
@@ -0,0 +1,251 @@
+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, Mapper,)
+
+ Mapper(if present) is one of the forms:
+ 1. {"mapper_name": <docname>}: Indicates that an existing Bank Party Mapper matched against
+ the transaction and the same must be linked in the Bank Transaction.
+
+ 2. {"bank_party_account_number": <ACC No.>, "bank_party_iban": <IBAN>} : Indicates that a match was
+ found in Customer/Supplier/Employee by account details. A Bank Party Mapper is now created
+ mapping the Party to the Account No./IBAN
+
+ 3. {"bank_party_name": <Counter Party Name>}: Indicates that a match was found in
+ Customer/Supplier/Employee by party name. A Bank Party Mapper is now created mapping the Party
+ to the Party Name (Bank Statement). If matched by Description, no mapper is created as
+ description is not a static key.
+
+ Mapper data is used either to create a new Bank Party Mapper or link an existing mapper to a transaction.
+ """
+
+ 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 = AutoMatchbyPartyDescription(
+ 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_bank_party_mapper()
+ if not result:
+ result = self.match_account_in_party()
+
+ return result
+
+ def match_account_in_bank_party_mapper(self) -> Union[Tuple, None]:
+ """Check for a IBAN/Account No. match in Bank Party Mapper"""
+ result = None
+ or_filters = {}
+ if self.bank_party_account_number:
+ or_filters["bank_party_account_number"] = self.bank_party_account_number
+
+ if self.bank_party_iban:
+ or_filters["bank_party_iban"] = self.bank_party_iban
+
+ mapper = frappe.db.get_all(
+ "Bank Party Mapper",
+ or_filters=or_filters,
+ fields=["party_type", "party", "name"],
+ limit_page_length=1,
+ )
+ if mapper:
+ data = mapper[0]
+ return (data["party_type"], data["party"], {"mapper_name": data["name"]})
+
+ 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 = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
+ if flt(self.deposit) > 0:
+ parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
+
+ for party in parties:
+ 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
+
+ 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],
+ {
+ "bank_party_account_number": self.get("bank_party_account_number"),
+ "bank_party_iban": self.get("bank_party_iban"),
+ },
+ )
+ break
+
+ return result
+
+
+class AutoMatchbyPartyDescription:
+ 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]:
+ # Match by Customer, Supplier or Employee Name
+ # search bank party mapper by party
+ # fuzzy search by customer/supplier & employee
+ if not (self.bank_party_name or self.description):
+ return None
+
+ result = self.match_party_name_in_bank_party_mapper()
+
+ if not result:
+ result = self.match_party_name_desc_in_party()
+
+ return result
+
+ def match_party_name_in_bank_party_mapper(self) -> Union[Tuple, None]:
+ """Check if match exists for party name in Bank Party Mapper"""
+ result = None
+ if not self.bank_party_name:
+ return
+
+ mapper_res = frappe.get_all(
+ "Bank Party Mapper",
+ filters={"bank_party_name": self.bank_party_name},
+ fields=["party_type", "party", "name"],
+ limit_page_length=1,
+ )
+ if mapper_res:
+ mapper_res = mapper_res[0]
+ return (
+ mapper_res["party_type"],
+ mapper_res["party"],
+ {"mapper_name": mapper_res["name"]},
+ )
+
+ 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 = ["Supplier", "Employee", "Customer"] # most-least likely to receive
+ if flt(self.deposit) > 0.0:
+ parties = ["Customer", "Supplier", "Employee"] # most-least likely to pay
+
+ for party in parties:
+ name_field = party.lower() + "_name"
+ filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
+ names = frappe.get_all(party, filters=filters, pluck=name_field)
+
+ 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:
+ # We skip if:
+ # 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
+
+ # Dont set description as a key in Bank Party Mapper due to its volatility
+ mapper = {"bank_party_name": self.get(field)} if field == "bank_party_name" else None
+ return (
+ party,
+ party_name,
+ mapper,
+ ), 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 continue 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 (result[0][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 closes to distinguish between
+ if first_result[SCORE] == second_result[SCORE]:
+ return None, True
+
+ return first_result[PARTY], True
+ else:
+ return None, False
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index 768d2f0..e7de71a 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -33,7 +33,12 @@
"unallocated_amount",
"party_section",
"party_type",
- "party"
+ "party",
+ "column_break_3czf",
+ "bank_party_name",
+ "bank_party_account_number",
+ "bank_party_iban",
+ "bank_party_mapper"
],
"fields": [
{
@@ -63,7 +68,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
- "options": "\nPending\nSettled\nUnreconciled\nReconciled"
+ "options": "\nPending\nSettled\nUnreconciled\nReconciled\nCancelled"
},
{
"fieldname": "bank_account",
@@ -202,11 +207,38 @@
"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)"
+ },
+ {
+ "fieldname": "bank_party_mapper",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Bank Party Mapper",
+ "options": "Bank Party Mapper",
+ "read_only": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2022-05-29 18:36:50.475964",
+ "modified": "2023-05-17 14:56:10.547480",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
@@ -260,4 +292,4 @@
"states": [],
"title_field": "bank_account",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index b441af9..04c3013 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -15,6 +15,9 @@
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
@@ -26,6 +29,9 @@
self.update_allocations()
self._saving_flag = False
+ if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
+ self.update_automatch_bank_party_mapper()
+
def on_cancel(self):
self.clear_linked_payment_entries(for_cancel=True)
self.set_status(update=True)
@@ -146,6 +152,49 @@
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, mapper = result
+ to_update = {"party_type": party_type, "party": party}
+
+ if mapper and mapper.get("mapper_name"):
+ # Transaction matched with an existing Bank party Mapper record
+ to_update["bank_party_mapper"] = mapper.get("mapper_name")
+ elif mapper:
+ # Make new Mapper record to remember match
+ mapper_doc = frappe.get_doc(
+ {"doctype": "Bank Party Mapper", "party_type": party_type, "party": party}
+ )
+ mapper_doc.update(mapper)
+ mapper_doc.insert()
+ to_update["bank_party_mapper"] = mapper_doc.name
+
+ frappe.db.set_value("Bank Transaction", self.name, field=to_update)
+
+ def update_automatch_bank_party_mapper(self):
+ """Update Bank Party Mapper if Party Type & Party are manually changed after submit."""
+ doc_before_update = self.get_doc_before_save()
+ party_type_changed = self.party_type and (doc_before_update.party_type != self.party_type)
+ party_changed = self.party and (doc_before_update.party != self.party)
+
+ if (party_type_changed or party_changed) and self.bank_party_mapper:
+ mapper_doc = frappe.get_doc("Bank Party Mapper", self.bank_party_mapper)
+ mapper_doc.update({"party_type": self.party_type, "party": self.party})
+ mapper_doc.save()
+
@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():
diff --git a/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py
new file mode 100644
index 0000000..8c6dc9d
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py
@@ -0,0 +1,239 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import nowdate
+
+from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
+
+
+class TestAutoMatchParty(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ create_bank_account()
+ frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1)
+ frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1)
+ return super().setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 0)
+ frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
+
+ def test_match_by_account_number(self):
+ """Test if transaction matches with existing (Bank Party Mapper) or new match."""
+ create_supplier_for_match(account_no="000000003716541159")
+ doc = create_bank_transaction(
+ withdrawal=1200,
+ transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
+ account_no="000000003716541159",
+ iban="DE02000000003716541159",
+ )
+
+ self.assertEqual(doc.party_type, "Supplier")
+ self.assertEqual(doc.party, "John Doe & Co.")
+ self.assertTrue(doc.bank_party_mapper)
+
+ # Check if Bank Party Mapper is created to remember mapping
+ bank_party_mapper = frappe.get_doc("Bank Party Mapper", doc.bank_party_mapper)
+ self.assertEqual(bank_party_mapper.party, "John Doe & Co.")
+ self.assertEqual(bank_party_mapper.bank_party_account_number, "000000003716541159")
+ self.assertEqual(bank_party_mapper.bank_party_iban, "DE02000000003716541159")
+
+ # Check if created mapping is used for quick match
+ doc_2 = create_bank_transaction(
+ withdrawal=500,
+ transaction_id="602413b8ji8bf838fub8f2c6a39bah7y",
+ account_no="000000003716541159",
+ )
+ self.assertEqual(doc_2.party, "John Doe & Co.")
+ self.assertEqual(doc_2.bank_party_mapper, bank_party_mapper.name)
+
+ def test_match_by_iban(self):
+ create_supplier_for_match(iban="DE02000000003716541159")
+ doc = create_bank_transaction(
+ withdrawal=1200,
+ transaction_id="c5455a224602afaa51592a9d9250600d",
+ account_no="000000003716541159",
+ iban="DE02000000003716541159",
+ )
+
+ self.assertEqual(doc.party_type, "Supplier")
+ self.assertEqual(doc.party, "John Doe & Co.")
+ self.assertTrue(doc.bank_party_mapper)
+
+ bank_party_mapper = frappe.get_doc("Bank Party Mapper", doc.bank_party_mapper)
+ self.assertEqual(bank_party_mapper.party, "John Doe & Co.")
+ self.assertEqual(bank_party_mapper.bank_party_account_number, "000000003716541159")
+ self.assertEqual(bank_party_mapper.bank_party_iban, "DE02000000003716541159")
+
+ def test_match_by_party_name(self):
+ create_supplier_for_match(supplier_name="Jackson Ella W.")
+ doc = create_bank_transaction(
+ withdrawal=1200,
+ transaction_id="1f6f661f347ff7b1ea588665f473adb1",
+ party_name="Ella Jackson",
+ iban="DE04000000003716545346",
+ )
+ self.assertEqual(doc.party_type, "Supplier")
+ self.assertEqual(doc.party, "Jackson Ella W.")
+ self.assertTrue(doc.bank_party_mapper)
+
+ bank_party_mapper = frappe.get_doc("Bank Party Mapper", doc.bank_party_mapper)
+ self.assertEqual(bank_party_mapper.party, "Jackson Ella W.")
+ self.assertEqual(bank_party_mapper.bank_party_name, "Ella Jackson")
+ self.assertEqual(bank_party_mapper.bank_party_iban, None)
+
+ # Check if created mapping is used for quick match
+ doc_2 = create_bank_transaction(
+ withdrawal=500,
+ transaction_id="578313b8ji8bf838fub8f2c6a39bah7y",
+ party_name="Ella Jackson",
+ account_no="000000004316531152",
+ )
+ self.assertEqual(doc_2.party, "Jackson Ella W.")
+ self.assertEqual(doc_2.bank_party_mapper, bank_party_mapper.name)
+
+ def test_match_by_description(self):
+ create_supplier_for_match(supplier_name="Microsoft")
+ doc = create_bank_transaction(
+ description="Auftraggeber: microsoft payments Buchungstext: msft ..e3006b5hdy. ref. j375979555927627/5536",
+ withdrawal=1200,
+ transaction_id="8df880a2d09c3bed3fea358ca5168c5a",
+ party_name="",
+ )
+ self.assertEqual(doc.party_type, "Supplier")
+ self.assertEqual(doc.party, "Microsoft")
+ self.assertFalse(doc.bank_party_mapper)
+
+ def test_correct_match_after_submit(self):
+ """Correct wrong mapping after submit. Test impact."""
+ # Similar named suppliers
+ create_supplier_for_match(supplier_name="Amazon")
+ create_supplier_for_match(supplier_name="Amazing Co.")
+
+ # Bank Transactions actually from "Amazon" that match better with "Amazing Co."
+ doc = create_bank_transaction(
+ description="visa06323202 amzn.com/bill 7,88eur1,5324711959 90.22. 1,62 87861003",
+ withdrawal=24.85,
+ transaction_id="3a1da4ee2dc5a980138d36ef5297cbd9",
+ party_name="Amazn Co.",
+ )
+ doc_2 = create_bank_transaction(
+ description="visa61268005 amzn.com/bill 22,345eur1,7654711959 89.23. 1,64 61268005",
+ withdrawal=80,
+ transaction_id="584314e459b00f792bfd569267efba6e",
+ party_name="Amazn Co.",
+ )
+
+ self.assertEqual(doc.party_type, "Supplier")
+ self.assertEqual(doc.party, "Amazing Co.")
+ self.assertTrue(doc.bank_party_mapper)
+ self.assertTrue(doc_2.bank_party_mapper, doc.bank_party_mapper)
+
+ bank_party_mapper = frappe.get_doc("Bank Party Mapper", doc.bank_party_mapper)
+ self.assertEqual(bank_party_mapper.party, "Amazing Co.")
+ self.assertEqual(bank_party_mapper.bank_party_name, "Amazn Co.")
+
+ # User corrects the value after submit to "Amazon"
+ doc.party = "Amazon"
+ doc.save()
+ bank_party_mapper.reload()
+ doc_2.reload()
+
+ # Mapping is edited and all transactions with this mapping are updated
+ self.assertEqual(bank_party_mapper.party, "Amazon")
+ self.assertEqual(bank_party_mapper.bank_party_name, "Amazn Co.")
+ self.assertEqual(doc_2.party, "Amazon")
+
+ def test_skip_match_if_multiple_close_results(self):
+ create_supplier_for_match(supplier_name="Adithya Medical & General Stores")
+ create_supplier_for_match(supplier_name="Adithya Medical And General Stores")
+
+ doc = create_bank_transaction(
+ description="Paracetamol Consignment, SINV-0009",
+ withdrawal=24.85,
+ transaction_id="3a1da4ee2dc5a980138d56ef3460cbd9",
+ party_name="Adithya Medical & General",
+ )
+
+ # Mapping is skipped as both Supplier names have the same match score
+ self.assertEqual(doc.party_type, None)
+ self.assertEqual(doc.party, None)
+ self.assertFalse(doc.bank_party_mapper)
+
+
+def create_supplier_for_match(supplier_name="John Doe & Co.", iban=None, account_no=None):
+ if frappe.db.exists("Supplier", {"supplier_name": supplier_name}):
+ # Update related Bank Account details
+ if not (iban or account_no):
+ return
+
+ frappe.db.set_value(
+ dt="Bank Account",
+ dn={"party": supplier_name},
+ field={"iban": iban, "bank_account_no": account_no},
+ )
+ return
+
+ # Create Supplier and Bank Account for the same
+ supplier = frappe.get_doc(
+ {
+ "doctype": "Supplier",
+ "supplier_name": supplier_name,
+ "supplier_group": "Services",
+ "supplier_type": "Company",
+ }
+ ).insert()
+
+ if not frappe.db.exists("Bank", "TestBank"):
+ frappe.get_doc(
+ {
+ "doctype": "Bank",
+ "bank_name": "TestBank",
+ }
+ ).insert(ignore_if_duplicate=True)
+
+ if not frappe.db.exists("Bank Account", supplier.name + " - " + "TestBank"):
+ frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": supplier.name,
+ "bank": "TestBank",
+ "iban": iban,
+ "bank_account_no": account_no,
+ "party_type": "Supplier",
+ "party": supplier.name,
+ }
+ ).insert()
+
+
+def create_bank_transaction(
+ description=None,
+ withdrawal=0,
+ deposit=0,
+ transaction_id=None,
+ party_name=None,
+ account_no=None,
+ iban=None,
+):
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": description or "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
+ "date": nowdate(),
+ "withdrawal": withdrawal,
+ "deposit": deposit,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ "transaction_id": transaction_id,
+ "bank_party_name": party_name,
+ "bank_party_account_number": account_no,
+ "bank_party_iban": iban,
+ }
+ ).insert()
+ doc.submit()
+ doc.reload()
+
+ return doc
diff --git a/erpnext/accounts/doctype/shareholder/shareholder.json b/erpnext/accounts/doctype/shareholder/shareholder.json
index e94aea9..e80b057 100644
--- a/erpnext/accounts/doctype/shareholder/shareholder.json
+++ b/erpnext/accounts/doctype/shareholder/shareholder.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2017-12-25 16:50:53.878430",
"doctype": "DocType",
@@ -111,11 +112,12 @@
"read_only": 1
}
],
- "modified": "2019-11-17 23:24:11.395882",
+ "links": [],
+ "modified": "2023-04-10 22:02:20.406087",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Shareholder",
- "name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -158,6 +160,7 @@
"search_fields": "folio_no",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 1bf7f58..f009789 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -457,7 +457,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-02-18 11:05:50.592270",
+ "modified": "2023-05-09 15:34:13.408932",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index c133cd3..72a1594 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -568,7 +568,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-02-18 11:04:46.343527",
+ "modified": "2023-05-09 15:38:40.255193",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json
index 99693d9..6cb4292 100644
--- a/erpnext/setup/doctype/employee/employee.json
+++ b/erpnext/setup/doctype/employee/employee.json
@@ -78,7 +78,9 @@
"salary_mode",
"bank_details_section",
"bank_name",
+ "column_break_heye",
"bank_ac_no",
+ "iban",
"personal_details",
"marital_status",
"family_background",
@@ -804,17 +806,26 @@
{
"fieldname": "column_break_104",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_heye",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.salary_mode == 'Bank'",
+ "fieldname": "iban",
+ "fieldtype": "Data",
+ "label": "IBAN"
}
],
"icon": "fa fa-user",
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2022-09-13 10:27:14.579197",
+ "modified": "2023-03-30 15:57:05.174592",
"modified_by": "Administrator",
"module": "Setup",
"name": "Employee",
- "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
diff --git a/pyproject.toml b/pyproject.toml
index 0718e5b..e5bc884 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,6 +12,7 @@
"pycountry~=20.7.3",
"Unidecode~=1.2.0",
"barcodenumber~=0.5.0",
+ "rapidfuzz~=2.15.0",
# integration dependencies
"gocardless-pro~=1.22.0",