From f22f84a58af430768c0e480c921ebed7517fe441 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:42:32 +0530 Subject: [PATCH] fix(crm): validate contact email before saving an email campaign (backport #58667) (#58672) Co-authored-by: kaulith <64089478+kaulith@users.noreply.github.com> --- .../doctype/email_campaign/email_campaign.py | 20 ++++- .../email_campaign/test_email_campaign.py | 80 ++++++++++++++++++- 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index dbc4382a041..3dadc830d0a 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -29,12 +29,19 @@ class EmailCampaign(Document): def validate(self): self.set_date() - # checking if email is set for lead. Not checking for contact as email is a mandatory field for contact. - if self.email_campaign_for == "Lead": - self.validate_lead() + self.validate_recipient_email() self.validate_email_campaign_already_exists() self.update_status() + def validate_recipient_email(self): + if not self.recipient: + return + + if self.email_campaign_for == "Lead": + self.validate_lead() + elif self.email_campaign_for == "Contact": + self.validate_contact() + def set_date(self): if getdate(self.start_date) < getdate(today()): frappe.throw(_("Start Date cannot be before the current date")) @@ -56,6 +63,13 @@ class EmailCampaign(Document): lead_name = frappe.db.get_value("Lead", self.recipient, "lead_name") frappe.throw(_("Please set an email id for the Lead {0}").format(lead_name)) + def validate_contact(self): + contact = frappe.db.get_value("Contact", self.recipient, ["email_id", "full_name"], as_dict=True) + if contact and not contact.email_id: + frappe.throw( + _("Please set a primary email ID for the Contact {0}").format(frappe.bold(contact.full_name)) + ) + def validate_email_campaign_already_exists(self): email_campaign_exists = frappe.db.exists( "Email Campaign", diff --git a/erpnext/crm/doctype/email_campaign/test_email_campaign.py b/erpnext/crm/doctype/email_campaign/test_email_campaign.py index 7d7d445b4be..f1a7aa286ac 100644 --- a/erpnext/crm/doctype/email_campaign/test_email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/test_email_campaign.py @@ -1,10 +1,84 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe -import unittest + +import frappe +from frappe.utils import add_days, getdate, today from erpnext.tests.utils import ERPNextTestSuite class TestEmailCampaign(ERPNextTestSuite): - pass + """Email Campaign derives its window from the linked Campaign schedule and + guards the start date and the recipient's email.""" + + def setUp(self): + frappe.set_user("Administrator") + + def make_email_template(self): + name = "_Test EC Email Template" + if not frappe.db.exists("Email Template", name): + frappe.get_doc( + {"doctype": "Email Template", "name": name, "subject": "Test", "response": "Hello"} + ).insert() + return name + + def make_campaign(self, schedules): + campaign = frappe.new_doc("Campaign") + campaign.campaign_name = f"_Test EC Campaign {frappe.generate_hash(length=6)}" + for days in schedules: + campaign.append( + "campaign_schedules", + {"send_after_days": days, "email_template": self.make_email_template()}, + ) + return campaign.insert() + + def make_email_campaign(self, campaign_name, start_date=None): + doc = frappe.new_doc("Email Campaign") + doc.campaign_name = campaign_name + doc.start_date = start_date or today() + return doc + + def test_start_date_cannot_be_in_the_past(self): + doc = self.make_email_campaign("irrelevant", start_date=add_days(today(), -1)) + self.assertRaises(frappe.ValidationError, doc.set_date) + + def test_end_date_is_start_plus_max_send_after_days(self): + campaign = self.make_campaign(schedules=[0, 5]) + doc = self.make_email_campaign(campaign.name) + doc.set_date() + self.assertEqual(getdate(doc.end_date), add_days(getdate(today()), 5)) + + def test_campaign_without_a_schedule_is_rejected(self): + campaign = self.make_campaign(schedules=[]) + doc = self.make_email_campaign(campaign.name) + self.assertRaises(frappe.ValidationError, doc.set_date) + + def test_lead_without_an_email_is_rejected(self): + lead = frappe.get_doc({"doctype": "Lead", "lead_name": "_Test Lead No Email"}).insert() + doc = frappe.new_doc("Email Campaign") + doc.email_campaign_for = "Lead" + doc.recipient = lead.name + self.assertRaises(frappe.ValidationError, doc.validate_lead) + + def test_contact_without_an_email_is_rejected(self): + contact = frappe.get_doc({"doctype": "Contact", "first_name": "_Test Contact No Email"}).insert() + campaign = self.make_campaign(schedules=[0]) + doc = self.make_email_campaign(campaign.name) + doc.email_campaign_for = "Contact" + doc.recipient = contact.name + self.assertRaisesRegex(frappe.ValidationError, "primary email ID", doc.insert) + + def test_contact_with_an_email_is_accepted(self): + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": "_Test Contact With Email", + "email_ids": [{"email_id": "_test_email_campaign@example.com", "is_primary": 1}], + } + ).insert() + campaign = self.make_campaign(schedules=[0]) + doc = self.make_email_campaign(campaign.name) + doc.email_campaign_for = "Contact" + doc.recipient = contact.name + doc.insert() + self.assertEqual(doc.status, "In Progress")