From 53491e20088c1a444936b1821aedf9fe6cd54c19 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:17:22 +0530 Subject: [PATCH] fix(selling): match MariaDB's customer-name suffix extraction on Postgres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_customer_name's Postgres branch extracted the PURE TRAILING digits of the name (regexp '^.*?(\d*)$'), while the MariaDB branch uses CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED) — the LEADING digits of the last whitespace token. For a scanned name like " - 3a" MariaDB yields 3 but Postgres yielded NULL→0, so the next de-duplicated number (and thus the generated Customer name) diverged between engines. Make the Postgres branch take the last whitespace token then its leading digits, mirroring MariaDB exactly ("X - 3a"->3, "X - 1.5"->1, "X - Foo"->0). Add a regression test with a " - 3a" name asserting the next name is " - 4" on both engines (it produced " - 1" on the old Postgres regex). --- erpnext/selling/doctype/customer/customer.py | 16 ++++++++------ .../selling/doctype/customer/test_customer.py | 22 ++++++++++++++++--- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 776df5cd5cb..6a95a580d4c 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -128,15 +128,17 @@ class Customer(TransactionBase): Customer = frappe.qb.DocType("Customer") if frappe.db.db_type == "postgres": - # Postgres: extract the TRAILING digits (e.g. "Customer - 3" -> "3") and cast to int. - # A non-numeric trailing token (e.g. "Customer - Foo") strips to an empty string, which - # NULLIF turns into NULL: MAX() then skips it and COALESCE floors to 0, matching - # MariaDB's CAST(... AS UNSIGNED) -> 0. (pypika's Substring is start/length, not a - # regex, so it can't be used here; UNSIGNED also doesn't exist on postgres, and a raw - # CAST of a non-numeric token to INTEGER would raise instead of yielding NULL.) + # Mirror MariaDB's CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED): take the last + # whitespace-delimited token, then its LEADING digits, and cast to int. So "X - 3" -> 3, + # "X - 3a" -> 3, "X - 1.5" -> 1, matching MariaDB exactly. A non-numeric token (e.g. + # "X - Foo") strips to '' which NULLIF turns into NULL: MAX() skips it and COALESCE floors + # to 0, matching MariaDB's CAST(... AS UNSIGNED) -> 0. (pypika's Substring is start/length, + # not a regex; UNSIGNED doesn't exist on postgres, and a raw CAST of a non-numeric token to + # INTEGER would raise instead of yielding NULL.) regexp_replace = CustomFunction("regexp_replace", ["source", "pattern", "replacement"]) nullif = CustomFunction("NULLIF", ["expr", "value"]) - extracted_part = nullif(regexp_replace(Customer.name, r"^.*?(\d*)$", r"\1"), "") + last_token = regexp_replace(Customer.name, r"^.*\s", "") + extracted_part = nullif(regexp_replace(last_token, r"^(\d*).*$", r"\1"), "") casted_part = Cast(extracted_part, "INTEGER") else: # MariaDB/MySQL: keep existing behavior. diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index ed0b435ffc7..e4b2ccae4d6 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -21,9 +21,9 @@ from erpnext.tests.utils import ERPNextTestSuite class TestCustomer(ERPNextTestSuite): def test_get_customer_name_dedupes_with_numeric_suffix(self): - # When a customer name already exists, get_customer_name appends "- ". - # The Postgres branch extracts the trailing digits with regexp_replace/NULLIF/CAST (pypika's - # Substring cannot do regex extraction); this exercises that path on both engines. + # When a customer name already exists, get_customer_name appends "- ". The + # Postgres branch extracts the suffix with regexp_replace/NULLIF/CAST (pypika's Substring cannot + # do regex extraction); this exercises that path on both engines. base = "_Test PG Dedup Customer" for nm in (base, f"{base} - 3"): if not frappe.db.exists("Customer", nm): @@ -35,6 +35,22 @@ class TestCustomer(ERPNextTestSuite): doc = frappe.get_doc({"doctype": "Customer", "customer_name": base, "customer_type": "Individual"}) self.assertEqual(doc.get_customer_name(), f"{base} - 4") + def test_get_customer_name_dedupe_handles_mixed_suffix(self): + # The suffix extractor must read the LEADING digits of the last whitespace-token, like MariaDB's + # CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED): " - 3a" -> 3, so the next name is + # " - 4". The earlier Postgres regex read pure-trailing digits, yielding 0 for "3a" and + # diverging from MariaDB (which would have produced " - 1"). Asserts engine parity. + base = "_Test PG Dedup Mixed" + for nm in (base, f"{base} - 3a"): + if not frappe.db.exists("Customer", nm): + frappe.get_doc( + {"doctype": "Customer", "customer_name": nm, "customer_type": "Individual"} + ).insert() + self.addCleanup(frappe.delete_doc, "Customer", nm, force=1) + + doc = frappe.get_doc({"doctype": "Customer", "customer_name": base, "customer_type": "Individual"}) + self.assertEqual(doc.get_customer_name(), f"{base} - 4") + def test_get_customer_group_details(self): doc = frappe.new_doc("Customer Group") doc.customer_group_name = "_Testing Customer Group"