From 04bea0733b9ba211c412d90883d07c2a1f672a2c Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Sun, 9 Aug 2026 21:53:14 +0530 Subject: [PATCH] feat: implement auto-demotion of other primary contacts in Supplier and Customer import providers --- .../supplier/supplier_import_provider.py | 19 +++++++++++++++++++ .../customer/customer_import_provider.py | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/erpnext/buying/doctype/supplier/supplier_import_provider.py b/erpnext/buying/doctype/supplier/supplier_import_provider.py index fd03c4814ac..ce622e55d0e 100644 --- a/erpnext/buying/doctype/supplier/supplier_import_provider.py +++ b/erpnext/buying/doctype/supplier/supplier_import_provider.py @@ -126,6 +126,11 @@ class SupplierImportProvider(ImportProvider): if flagged or primary is None: primary = contact if primary: + # Contact has no cross-contact auto-demotion (unlike Address's + # validate_preferred_address), so explicitly demote any other primary Contact on + # this party first — otherwise get_default_contact may return a Contact other + # than supplier_primary_contact. + _demote_other_primary_contacts("Supplier", supplier.name, primary.name) frappe.db.set_value("Contact", primary.name, "is_primary_contact", 1) supplier.db_set("supplier_primary_contact", primary.name) supplier.db_set("mobile_no", primary.mobile_no) @@ -184,6 +189,20 @@ class SupplierImportProvider(ImportProvider): supplier.db_set("primary_address", get_address_display(primary.name)) +def _demote_other_primary_contacts(link_doctype: str, link_name: str, keep: str) -> None: + """Clear ``is_primary_contact`` on the party's other Contacts (keeps ``keep``).""" + linked = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": link_doctype, "link_name": link_name, "parenttype": "Contact"}, + pluck="parent", + ) + for other in frappe.get_all( + "Contact", filters={"name": ["in", linked or [""]], "is_primary_contact": 1}, pluck="name" + ): + if other != keep: + frappe.db.set_value("Contact", other, "is_primary_contact", 0) + + def _doctype_docfields(doctype: str, prefer_plain_label: bool = False) -> list[dict]: from frappe.model import display_fieldtypes, no_value_fields diff --git a/erpnext/selling/doctype/customer/customer_import_provider.py b/erpnext/selling/doctype/customer/customer_import_provider.py index cc5c7bd230f..7253e3e27fd 100644 --- a/erpnext/selling/doctype/customer/customer_import_provider.py +++ b/erpnext/selling/doctype/customer/customer_import_provider.py @@ -133,6 +133,11 @@ class CustomerImportProvider(ImportProvider): if flagged or primary is None: primary = contact if primary: + # Contact has no cross-contact auto-demotion (unlike Address's + # validate_preferred_address), so explicitly demote any other primary Contact on + # this party first — otherwise get_default_contact may return a Contact other + # than customer_primary_contact. + _demote_other_primary_contacts("Customer", customer.name, primary.name) frappe.db.set_value("Contact", primary.name, "is_primary_contact", 1) customer.db_set("customer_primary_contact", primary.name) customer.db_set("mobile_no", primary.mobile_no) @@ -192,6 +197,20 @@ class CustomerImportProvider(ImportProvider): customer.db_set("primary_address", get_address_display(primary.name)) +def _demote_other_primary_contacts(link_doctype: str, link_name: str, keep: str) -> None: + """Clear ``is_primary_contact`` on the party's other Contacts (keeps ``keep``).""" + linked = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": link_doctype, "link_name": link_name, "parenttype": "Contact"}, + pluck="parent", + ) + for other in frappe.get_all( + "Contact", filters={"name": ["in", linked or [""]], "is_primary_contact": 1}, pluck="name" + ): + if other != keep: + frappe.db.set_value("Contact", other, "is_primary_contact", 0) + + def _doctype_docfields(doctype: str, prefer_plain_label: bool = False) -> list[dict]: """Non-table importable fields of ``doctype`` as complete docfield dicts.""" from frappe.model import display_fieldtypes, no_value_fields