feat(bank-integration): case-insensitive party matching, with per-row override

Party-name lookup at Create & Reconcile previously used a strict (strip-only)
key, so a Bank Transaction whose bank_party_name differed in casing from the
registry record would never resolve a party. Now:

- 'Ignore Case in Party Matching' Check on Bank Integration (default ON) sets
  the global behaviour: party names are indexed under both their strict and
  lowercase forms, and the lookup tries both.
- 'Case Mode' Select on each Customer / Supplier Mapping row overrides the
  global for that specific party — blank inherits, "Ignore Case" / "Case
  Sensitive" forces the mode regardless of the global setting.

Honours the established "row > global" priority pattern used elsewhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-05-13 16:25:46 +00:00
parent 44c3281cb9
commit e8e9359cb5
4 changed files with 65 additions and 6 deletions

View File

@ -48,29 +48,64 @@ def _extract_messages(message_log):
return " | ".join(p for p in parts if p)
def _effective_case_insensitive(row, global_default):
"""Per-row Case Mode overrides the global Ignore Case flag.
Returns True if the party name for this row should be indexed/looked-up
case-insensitively, False otherwise.
"""
mode = (getattr(row, "case_mode", None) or "").strip()
if mode == "Ignore Case":
return True
if mode == "Case Sensitive":
return False
return global_default
def _build_name_to_party(settings):
"""Counterparty text -> {"Customer": erp_customer} / {"Supplier": erp_supplier},
built from the mapped rows of customer_mappings / supplier_mappings."""
built from the mapped rows of customer_mappings / supplier_mappings.
A row may be indexed under its strict key only, its lowercased key only, or
both depending on the row's Case Mode (which overrides the global
`case_insensitive_party_match` setting).
"""
global_ci = bool(getattr(settings, "case_insensitive_party_match", 1))
idx = {}
def _add(name, party_type, erp_party, ci):
key = (name or "").strip()
if not key:
return
idx.setdefault(key, {})[party_type] = erp_party
if ci:
idx.setdefault(key.lower(), {})[party_type] = erp_party
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
_add(cust_name, "Customer", row.erp_customer, _effective_case_insensitive(row, global_ci))
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
_add(supp_name, "Supplier", row.erp_supplier, _effective_case_insensitive(row, global_ci))
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())
"""(party_type, erp_party) for `name`, or (None, None). Tries an exact (strip-only)
lookup first; falls back to lowercased lookup so rows indexed case-insensitively
can be reached from arbitrary Bank Transaction bank_party_name casing.
Prefers Customer for incoming (Receive) and Supplier for outgoing (Pay),
falling back to the other."""
key = (name or "").strip()
if not key:
return None, None
cands = name_to_party.get(key) or name_to_party.get(key.lower())
if not cands:
return None, None
preferred = "Customer" if payment_type == "Receive" else "Supplier"

View File

@ -14,6 +14,7 @@
"similarity_threshold_suppliers",
"similarity_threshold_purpose",
"consider_azeri_chars",
"case_insensitive_party_match",
"default_party_settings_section",
"default_supplier_group",
"default_customer_group",
@ -104,6 +105,13 @@
"fieldtype": "Check",
"label": "Consider Azerbaijani Characters"
},
{
"default": "1",
"description": "When resolving a Bank Transaction's counterparty against the mappings, ignore case differences (e.g. 'Acme MMC' matches 'acme mmc'). Each row in Customer/Supplier Mappings can override this via 'Case Mode'.",
"fieldname": "case_insensitive_party_match",
"fieldtype": "Check",
"label": "Ignore Case in Party Matching"
},
{
"fieldname": "default_party_settings_section",
"fieldtype": "Section Break",

View File

@ -10,6 +10,7 @@
"customer_group",
"territory",
"payment_terms",
"case_mode",
"mapping_type"
],
"fields": [
@ -55,6 +56,13 @@
"label": "Payment Terms",
"options": "Payment Terms Template"
},
{
"description": "Override the global 'Ignore Case in Party Matching' for this row. Blank = use the global setting.",
"fieldname": "case_mode",
"fieldtype": "Select",
"label": "Case Mode",
"options": "\nIgnore Case\nCase Sensitive"
},
{
"default": "Manual",
"fieldname": "mapping_type",

View File

@ -9,6 +9,7 @@
"erp_supplier",
"supplier_group",
"payment_terms",
"case_mode",
"mapping_type"
],
"fields": [
@ -47,6 +48,13 @@
"label": "Payment Terms",
"options": "Payment Terms Template"
},
{
"description": "Override the global 'Ignore Case in Party Matching' for this row. Blank = use the global setting.",
"fieldname": "case_mode",
"fieldtype": "Select",
"label": "Case Mode",
"options": "\nIgnore Case\nCase Sensitive"
},
{
"default": "Manual",
"fieldname": "mapping_type",