From 8f6969721243f6044462ae80fae7efe034c9c8fe Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 22 Jun 2026 12:47:44 +0530 Subject: [PATCH 1/2] fix(lead): don't crash deriving lead name when only ignore_mandatory is set set_lead_name fell through to email_id.split('@') when a lead had no name, company or email but ignore_mandatory was set (e.g. data import), raising AttributeError on a None email. Only derive from email when one exists; the lead name is then left blank, as intended for that path. --- erpnext/crm/doctype/lead/lead.py | 2 +- erpnext/crm/doctype/lead/test_lead.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 5c5dfb03fff..6e2e6a57bb1 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -140,7 +140,7 @@ class Lead(SellingController, CRMNote): frappe.throw(_("A Lead requires either a person's name or an organization's name")) elif self.company_name: self.lead_name = self.company_name - else: + elif self.email_id: self.lead_name = self.email_id.split("@")[0] def set_title(self): diff --git a/erpnext/crm/doctype/lead/test_lead.py b/erpnext/crm/doctype/lead/test_lead.py index fe48d651693..49a80ca7c81 100644 --- a/erpnext/crm/doctype/lead/test_lead.py +++ b/erpnext/crm/doctype/lead/test_lead.py @@ -193,6 +193,29 @@ class TestLead(ERPNextTestSuite): lead.delete() self.assertFalse(frappe.db.exists("Prospect", prospect_name)) + def test_set_lead_name_fallbacks(self): + # organization name is used when there is no person name + lead = frappe.new_doc("Lead") + lead.company_name = "_Test Org Lead" + lead.set_lead_name() + self.assertEqual(lead.lead_name, "_Test Org Lead") + + # the email local-part is used when only an email is present + lead = frappe.new_doc("Lead") + lead.email_id = "jane.doe@example.com" + lead.set_lead_name() + self.assertEqual(lead.lead_name, "jane.doe") + + # data import (ignore_mandatory) with no name/company/email must not crash + lead = frappe.new_doc("Lead") + lead.flags.ignore_mandatory = True + lead.set_lead_name() + self.assertFalse(lead.lead_name) + + # otherwise a lead with no name source is rejected + lead = frappe.new_doc("Lead") + self.assertRaises(frappe.ValidationError, lead.set_lead_name) + def create_event(subject, starts_on, reference_type, reference_name): event = frappe.new_doc("Event") From 43d2c7335de9392cd28f69bd73da2a05d9b41ef1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 22 Jun 2026 13:10:29 +0530 Subject: [PATCH 2/2] refactor(lead): name the loops in remove_link_from_prospect The outer and inner loops both used 'd'; name them linked_prospect and lead so the prospect/lead iteration reads clearly. No behaviour change. --- erpnext/crm/doctype/lead/lead.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 6e2e6a57bb1..5757d20c824 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -197,17 +197,17 @@ class Lead(SellingController, CRMNote): lead_row.db_update() def remove_link_from_prospect(self): - prospects = self.get_linked_prospects() + linked_prospects = self.get_linked_prospects() - for d in prospects: - prospect = frappe.get_doc("Prospect", d.parent) + for linked_prospect in linked_prospects: + prospect = frappe.get_doc("Prospect", linked_prospect.parent) if len(prospect.get("leads")) == 1: prospect.delete(ignore_permissions=True) else: to_remove = None - for d in prospect.get("leads"): - if d.lead == self.name: - to_remove = d + for lead in prospect.get("leads"): + if lead.lead == self.name: + to_remove = lead if to_remove: prospect.remove(to_remove)