From fc03509e46e4a7a9a6ae2adce4ead3eabc3e3838 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 30 Jun 2019 16:24:29 +0530 Subject: [PATCH 01/17] created data structure for email automation --- .../campaign_email_schedule/__init__.py | 0 .../campaign_email_schedule.json | 38 +++++++ .../campaign_email_schedule.py | 10 ++ .../crm/doctype/email_campaign/__init__.py | 0 .../doctype/email_campaign/email_campaign.js | 8 ++ .../email_campaign/email_campaign.json | 100 ++++++++++++++++++ .../doctype/email_campaign/email_campaign.py | 10 ++ .../email_campaign/test_email_campaign.py | 10 ++ 8 files changed, 176 insertions(+) create mode 100644 erpnext/crm/doctype/campaign_email_schedule/__init__.py create mode 100644 erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json create mode 100644 erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.py create mode 100644 erpnext/crm/doctype/email_campaign/__init__.py create mode 100644 erpnext/crm/doctype/email_campaign/email_campaign.js create mode 100644 erpnext/crm/doctype/email_campaign/email_campaign.json create mode 100644 erpnext/crm/doctype/email_campaign/email_campaign.py create mode 100644 erpnext/crm/doctype/email_campaign/test_email_campaign.py diff --git a/erpnext/crm/doctype/campaign_email_schedule/__init__.py b/erpnext/crm/doctype/campaign_email_schedule/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json new file mode 100644 index 00000000000..2d900940a3c --- /dev/null +++ b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json @@ -0,0 +1,38 @@ +{ + "creation": "2019-06-30 15:56:20.306901", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "send_after_days", + "email_template" + ], + "fields": [ + { + "fieldname": "send_after_days", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Send After (days)", + "reqd": 1 + }, + { + "fieldname": "email_template", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Email Template", + "options": "Email Template", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-06-30 15:56:20.306901", + "modified_by": "Administrator", + "module": "CRM", + "name": "Campaign Email Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.py b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.py new file mode 100644 index 00000000000..8445b8a397e --- /dev/null +++ b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class CampaignEmailSchedule(Document): + pass diff --git a/erpnext/crm/doctype/email_campaign/__init__.py b/erpnext/crm/doctype/email_campaign/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.js b/erpnext/crm/doctype/email_campaign/email_campaign.js new file mode 100644 index 00000000000..6020028a13f --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Email Campaign', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.json b/erpnext/crm/doctype/email_campaign/email_campaign.json new file mode 100644 index 00000000000..49b3c0643da --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign.json @@ -0,0 +1,100 @@ +{ + "autoname": "naming_series:", + "creation": "2019-06-30 16:05:30.015615", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign_section", + "campaign_name", + "lead", + "column_break_4", + "start_date", + "status", + "email_schedule_section", + "email_schedule", + "naming_series" + ], + "fields": [ + { + "fieldname": "campaign_section", + "fieldtype": "Section Break", + "label": "CAMPAIGN " + }, + { + "fieldname": "campaign_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Campaign Name", + "options": "Campaign", + "reqd": 1 + }, + { + "fieldname": "lead", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Lead", + "options": "Lead", + "reqd": 1 + }, + { + "default": "Started", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "\nStarted\nIn Progress\nCompleted" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "reqd": 1 + }, + { + "fieldname": "email_schedule_section", + "fieldtype": "Section Break", + "label": "EMAIL SCHEDULE" + }, + { + "fieldname": "email_schedule", + "fieldtype": "Table", + "label": "Email Schedule", + "options": "Campaign Email Schedule", + "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "MAIL-CAMP-.YYYY.-", + "reqd": 1 + } + ], + "modified": "2019-06-30 16:23:00.696185", + "modified_by": "Administrator", + "module": "CRM", + "name": "Email Campaign", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py new file mode 100644 index 00000000000..baa82e89690 --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class EmailCampaign(Document): + pass diff --git a/erpnext/crm/doctype/email_campaign/test_email_campaign.py b/erpnext/crm/doctype/email_campaign/test_email_campaign.py new file mode 100644 index 00000000000..f5eab483330 --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/test_email_campaign.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEmailCampaign(unittest.TestCase): + pass From dcf5fbd35dca426941342cae25a9b9e7d53eacb8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 30 Jun 2019 16:36:09 +0530 Subject: [PATCH 02/17] added some validations --- .../doctype/email_campaign/email_campaign.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index baa82e89690..c3e8f9f5593 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -3,8 +3,26 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +from frappe import _ +from frappe.utils import getdate, add_days from frappe.model.document import Document class EmailCampaign(Document): - pass + def validate(self): + self.validate_dates() + + def validate_dates(self): + campaign = frappe.get_doc("Campaign", self.campaign_name) + + #email campaign cannot start before campaign + if campaign.from_date and getdate(self.start_date) < getdate(campaign.from_date): + frappe.throw(_("Email Campaign Start Date cannot be before Campaign Start Date")) + + #check if email_schedule is exceeding the campaign end date + no_of_days = 0 + for entry in self.get("email_schedule"): + no_of_days += entry.send_after_days + email_schedule_end_date = add_days(getdate(self.start_date), no_of_days) + if campaign.to_date and getdate(email_schedule_end_date) > getdate(campaign.to_date): + frappe.throw(_("Email Schedule cannot extend Campaign End Date")) From 162f7d1b50c175e85e118f15140b55e039084210 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 1 Jul 2019 01:09:58 +0530 Subject: [PATCH 03/17] auto email and new communication linked to email campaign setup --- .../email_campaign/email_campaign.json | 17 ++++++-- .../doctype/email_campaign/email_campaign.py | 42 ++++++++++++++++++- erpnext/hooks.py | 1 + 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.json b/erpnext/crm/doctype/email_campaign/email_campaign.json index 49b3c0643da..d7113f6b4e7 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.json +++ b/erpnext/crm/doctype/email_campaign/email_campaign.json @@ -13,7 +13,8 @@ "status", "email_schedule_section", "email_schedule", - "naming_series" + "naming_series", + "amended_from" ], "fields": [ { @@ -42,7 +43,7 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "\nStarted\nIn Progress\nCompleted" + "options": "\nDraft\nSubmitted\nStarted\nIn Progress\nCompleted" }, { "fieldname": "column_break_4", @@ -72,9 +73,19 @@ "label": "Naming Series", "options": "MAIL-CAMP-.YYYY.-", "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Email Campaign", + "print_hide": 1, + "read_only": 1 } ], - "modified": "2019-06-30 16:23:00.696185", + "is_submittable": 1, + "modified": "2019-06-30 23:00:24.765312", "modified_by": "Administrator", "module": "CRM", "name": "Email Campaign", diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index c3e8f9f5593..82ee6a6cb4a 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -5,12 +5,14 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import getdate, add_days +from frappe.utils import getdate, add_days, nowdate from frappe.model.document import Document +from frappe.email.inbox import link_communication_to_document class EmailCampaign(Document): def validate(self): self.validate_dates() + self.validate_lead() def validate_dates(self): campaign = frappe.get_doc("Campaign", self.campaign_name) @@ -25,4 +27,40 @@ class EmailCampaign(Document): no_of_days += entry.send_after_days email_schedule_end_date = add_days(getdate(self.start_date), no_of_days) if campaign.to_date and getdate(email_schedule_end_date) > getdate(campaign.to_date): - frappe.throw(_("Email Schedule cannot extend Campaign End Date")) + frappe.throw(_("Email Schedule cannot extend Campaign End Date")) + + def validate_lead(self): + lead = frappe.get_doc("Lead", self.lead) + if not lead.get("email_id"): + frappe.throw(_("Please set email id for lead communication")) + + def send(self): + lead = frappe.get_doc("Lead", self.get("lead")) + email_schedule = frappe.get_doc("Campaign Email Schedule", self.get("email_schedule")) + email_template = frappe.get_doc("Email Template", email_schedule.name) + frappe.sendmail( + recipients = lead.get("email_id"), + sender = lead.get("lead_owner"), + subject = email_template.get("subject"), + message = email_template.get("response"), + reference_doctype = self.doctype, + reference_name = self.name + ) + + def on_submit(self): + """Create a new communication linked to the campaign if not created""" + if not frappe.db.sql("select subject from tabCommunication where reference_name = %s", self.name): + doc = frappe.new_doc("Communication") + doc.subject = "Email Campaign Communication: " + self.name + link_communication_to_document(doc, "Email Campaign", self.name, ignore_communication_links = False) + +@frappe.whitelist() +def send_email_to_leads(): + email_campaigns = frappe.get_all("Email Campaign", filters = { 'start_date': ("<=", nowdate()) }) + for campaign in email_campaigns: + email_campaign = frappe.get_doc("Email Campaign", campaign.name) + for entry in email_campaign.get("email_schedule"): + scheduled_date = add_days(email_campaign.get('start_date'), entry.get('send_after_days')) + if(scheduled_date == nowdate()): + email_campaign.send() +# send_email_to_leads() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6ce75bbac36..1466d243ad6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -266,6 +266,7 @@ scheduler_events = { "erpnext.projects.doctype.project.project.send_project_status_email_to_users", "erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", + "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads" ], "daily_long": [ "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" From 36963a8e0408af1171ad0ee170ac0224ebeebc5b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 9 Jul 2019 15:14:13 +0530 Subject: [PATCH 04/17] feat: Email Campaign --- .../email_campaign/email_campaign.json | 66 +++++---- .../doctype/email_campaign/email_campaign.py | 128 ++++++++++++++---- .../email_campaign/email_campaign_list.js | 11 ++ erpnext/hooks.py | 3 +- 4 files changed, 155 insertions(+), 53 deletions(-) create mode 100644 erpnext/crm/doctype/email_campaign/email_campaign_list.js diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.json b/erpnext/crm/doctype/email_campaign/email_campaign.json index d7113f6b4e7..66b35467cfd 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.json +++ b/erpnext/crm/doctype/email_campaign/email_campaign.json @@ -7,20 +7,23 @@ "field_order": [ "campaign_section", "campaign_name", - "lead", - "column_break_4", + "email_campaign_for", "start_date", + "column_break_4", + "sender", + "recipient", + "end_date", "status", "email_schedule_section", "email_schedule", - "naming_series", - "amended_from" + "unsubscribed", + "naming_series" ], "fields": [ { "fieldname": "campaign_section", "fieldtype": "Section Break", - "label": "CAMPAIGN " + "label": "Campaign" }, { "fieldname": "campaign_name", @@ -31,19 +34,10 @@ "reqd": 1 }, { - "fieldname": "lead", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Lead", - "options": "Lead", - "reqd": 1 - }, - { - "default": "Started", "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "\nDraft\nSubmitted\nStarted\nIn Progress\nCompleted" + "options": "\nScheduled\nIn Progress\nCompleted\nUnsubscribed" }, { "fieldname": "column_break_4", @@ -58,7 +52,7 @@ { "fieldname": "email_schedule_section", "fieldtype": "Section Break", - "label": "EMAIL SCHEDULE" + "label": "Email Schedule" }, { "fieldname": "email_schedule", @@ -75,17 +69,41 @@ "reqd": 1 }, { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Email Campaign", - "print_hide": 1, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", "read_only": 1 + }, + { + "default": "Lead", + "fieldname": "email_campaign_for", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Email Campaign For ", + "options": "\nLead\nContact" + }, + { + "fieldname": "recipient", + "fieldtype": "Dynamic Link", + "label": "Recipient", + "options": "email_campaign_for", + "reqd": 1 + }, + { + "default": "__user", + "fieldname": "sender", + "fieldtype": "Link", + "label": "Sender", + "options": "User" + }, + { + "default": "0", + "fieldname": "unsubscribed", + "fieldtype": "Check", + "label": "Unsubscribed" } ], - "is_submittable": 1, - "modified": "2019-06-30 23:00:24.765312", + "modified": "2019-07-09 15:07:03.328591", "modified_by": "Administrator", "module": "CRM", "name": "Email Campaign", diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 82ee6a6cb4a..1132226b90d 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -5,14 +5,18 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import getdate, add_days, nowdate +from frappe.utils import getdate, add_days, today, nowdate, cstr from frappe.model.document import Document -from frappe.email.inbox import link_communication_to_document +from frappe.core.doctype.communication.email import make class EmailCampaign(Document): def validate(self): self.validate_dates() - self.validate_lead() + #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.set_end_date() + self.update_status() def validate_dates(self): campaign = frappe.get_doc("Campaign", self.campaign_name) @@ -30,37 +34,105 @@ class EmailCampaign(Document): frappe.throw(_("Email Schedule cannot extend Campaign End Date")) def validate_lead(self): - lead = frappe.get_doc("Lead", self.lead) + lead = frappe.get_doc("Lead", self.recipient) if not lead.get("email_id"): - frappe.throw(_("Please set email id for lead communication")) + frappe.throw(_("Please set an email id for lead communication")) - def send(self): - lead = frappe.get_doc("Lead", self.get("lead")) - email_schedule = frappe.get_doc("Campaign Email Schedule", self.get("email_schedule")) - email_template = frappe.get_doc("Email Template", email_schedule.name) - frappe.sendmail( - recipients = lead.get("email_id"), - sender = lead.get("lead_owner"), - subject = email_template.get("subject"), - message = email_template.get("response"), - reference_doctype = self.doctype, - reference_name = self.name - ) + def set_end_date(self): + #set the end date as start date + max(send after days) in email schedule + send_after_days = [] + for entry in self.get("email_schedule"): + send_after_days.append(entry.send_after_days) + self.end_date = add_days(getdate(self.start_date), max(send_after_days)) - def on_submit(self): - """Create a new communication linked to the campaign if not created""" - if not frappe.db.sql("select subject from tabCommunication where reference_name = %s", self.name): - doc = frappe.new_doc("Communication") - doc.subject = "Email Campaign Communication: " + self.name - link_communication_to_document(doc, "Email Campaign", self.name, ignore_communication_links = False) + def update_status(self): + start_date = getdate(self.start_date) + end_date = getdate(self.end_date) + today_date = getdate(today()) + if self.unsubscribed: + self.status = "Unsubscribed" + else: + if start_date > today_date: + self.status = "Scheduled" + elif end_date >= today_date: + self.status = "In Progress" + elif end_date < today_date: + self.status = "Completed" -@frappe.whitelist() +#called through hooks to send campaign mails to leads def send_email_to_leads(): - email_campaigns = frappe.get_all("Email Campaign", filters = { 'start_date': ("<=", nowdate()) }) + email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('not in', ['Unsubscribed', 'Completed', 'Scheduled']), 'unsubscribed': 0 }) for campaign in email_campaigns: email_campaign = frappe.get_doc("Email Campaign", campaign.name) for entry in email_campaign.get("email_schedule"): scheduled_date = add_days(email_campaign.get('start_date'), entry.get('send_after_days')) - if(scheduled_date == nowdate()): - email_campaign.send() -# send_email_to_leads() + if scheduled_date == getdate(today()): + send_mail(entry, email_campaign) + +def send_mail(entry, email_campaign): + if email_campaign.email_campaign_for == "Lead": + lead = frappe.get_doc("Lead", email_campaign.get("recipient")) + recipient_email = lead.email_id + elif email_campaign.email_campaign_for == "Contact": + recipient = frappe.get_doc("Contact", email_campaign.get("recipient")) + recipient_email = recipient.email_id + email_template = frappe.get_doc("Email Template", entry.get("email_template")) + sender = frappe.get_doc("User", email_campaign.get("sender")) + sender_email = sender.email + # send mail and link communication to document + comm = make( + doctype = "Email Campaign", + name = email_campaign.name, + subject = email_template.get("subject"), + content = email_template.get("response"), + sender = sender_email, + recipients = recipient_email, + communication_medium = "Email", + sent_or_received = "Sent", + send_email = False, + email_template = email_template.name + ) + frappe.sendmail( + recipients = recipient_email, + sender = sender_email, + subject = email_template.get("subject"), + content = email_template.get("response"), + reference_doctype = "Email Campaign", + reference_name = email_campaign.name, + unsubscribe_method = "/api/method/erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient", + unsubscribe_params = {"name": email_campaign.name, "email": recipient_email}, + unsubscribe_message = "Stop Getting Email Campaign Mails", + communication = comm.get("name") + ) + +@frappe.whitelist(allow_guest=True) +def unsubscribe_recipient(name, email): + # unsubsribe from comments and communications + try: + frappe.get_doc({ + "doctype": "Email Unsubscribe", + "email": email, + "reference_doctype": "Email Campaign", + "reference_name": name + }).insert(ignore_permissions=True) + + except frappe.DuplicateEntryError: + frappe.db.rollback() + + else: + frappe.db.commit() + frappe.db.set_value("Email Campaign", name, "unsubscribed", 1) + frappe.db.set_value("Email Campaign", name, "status", "Unsubscribed") + frappe.db.commit() + return_unsubscribed_page(email, name) + +def return_unsubscribed_page(email, name): + frappe.respond_as_web_page(_("Unsubscribed"), + _("{0} has left the Email Campaign {1}").format(email, name), + indicator_color='green') + +#called through hooks to update email campaign status daily +def set_email_campaign_status(): + email_campaigns = frappe.get_all("Email Campaign") + for email_campaign in email_campaigns: + email_campaign.update_status() diff --git a/erpnext/crm/doctype/email_campaign/email_campaign_list.js b/erpnext/crm/doctype/email_campaign/email_campaign_list.js new file mode 100644 index 00000000000..d1bfdd31ee7 --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign_list.js @@ -0,0 +1,11 @@ +frappe.listview_settings['Email Campaign'] = { + get_indicator: function(doc) { + var colors = { + "Unsubscribed": "red", + "Scheduled": "blue", + "In Progress": "orange", + "Completed": "green" + } + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + } +}; diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 1466d243ad6..1b34a59f55b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -266,7 +266,8 @@ scheduler_events = { "erpnext.projects.doctype.project.project.send_project_status_email_to_users", "erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", - "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads" + "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads", + "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status" ], "daily_long": [ "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" From 9e35bff55c2fc64bc269582fc0f3c8c2a51823bd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 12 Jul 2019 13:56:36 +0530 Subject: [PATCH 05/17] feat: Email Campaign --- .../campaign_email_schedule.json | 6 +- .../doctype/email_campaign/email_campaign.js | 3 + .../email_campaign/email_campaign.json | 52 ++------ .../doctype/email_campaign/email_campaign.py | 114 ++++++------------ erpnext/hooks.py | 3 + .../selling/doctype/campaign/campaign.json | 31 ++++- .../doctype/campaign/campaign_dashboard.py | 13 ++ 7 files changed, 93 insertions(+), 129 deletions(-) create mode 100644 erpnext/selling/doctype/campaign/campaign_dashboard.py diff --git a/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json index 2d900940a3c..1481a32d5b0 100644 --- a/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json +++ b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json @@ -4,8 +4,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "send_after_days", - "email_template" + "email_template", + "send_after_days" ], "fields": [ { @@ -25,7 +25,7 @@ } ], "istable": 1, - "modified": "2019-06-30 15:56:20.306901", + "modified": "2019-07-12 11:46:43.184123", "modified_by": "Administrator", "module": "CRM", "name": "Campaign Email Schedule", diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.js b/erpnext/crm/doctype/email_campaign/email_campaign.js index 6020028a13f..09ed84882d9 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.js +++ b/erpnext/crm/doctype/email_campaign/email_campaign.js @@ -5,4 +5,7 @@ frappe.ui.form.on('Email Campaign', { // refresh: function(frm) { // } + email_campaign_for: function(frm) { + frm.set_value('recipient', ''); + } }); diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.json b/erpnext/crm/doctype/email_campaign/email_campaign.json index 66b35467cfd..32591362753 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.json +++ b/erpnext/crm/doctype/email_campaign/email_campaign.json @@ -1,35 +1,25 @@ { - "autoname": "naming_series:", + "autoname": "format:MAIL-CAMP-{YYYY}-{#####}", "creation": "2019-06-30 16:05:30.015615", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "campaign_section", "campaign_name", "email_campaign_for", - "start_date", - "column_break_4", - "sender", "recipient", + "sender", + "column_break_4", + "start_date", "end_date", - "status", - "email_schedule_section", - "email_schedule", - "unsubscribed", - "naming_series" + "status" ], "fields": [ - { - "fieldname": "campaign_section", - "fieldtype": "Section Break", - "label": "Campaign" - }, { "fieldname": "campaign_name", "fieldtype": "Link", "in_list_view": 1, - "label": "Campaign Name", + "label": "Campaign", "options": "Campaign", "reqd": 1 }, @@ -37,7 +27,8 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "\nScheduled\nIn Progress\nCompleted\nUnsubscribed" + "options": "\nScheduled\nIn Progress\nCompleted\nUnsubscribed", + "read_only": 1 }, { "fieldname": "column_break_4", @@ -49,25 +40,6 @@ "label": "Start Date", "reqd": 1 }, - { - "fieldname": "email_schedule_section", - "fieldtype": "Section Break", - "label": "Email Schedule" - }, - { - "fieldname": "email_schedule", - "fieldtype": "Table", - "label": "Email Schedule", - "options": "Campaign Email Schedule", - "reqd": 1 - }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Naming Series", - "options": "MAIL-CAMP-.YYYY.-", - "reqd": 1 - }, { "fieldname": "end_date", "fieldtype": "Date", @@ -95,15 +67,9 @@ "fieldtype": "Link", "label": "Sender", "options": "User" - }, - { - "default": "0", - "fieldname": "unsubscribed", - "fieldtype": "Check", - "label": "Unsubscribed" } ], - "modified": "2019-07-09 15:07:03.328591", + "modified": "2019-07-12 13:47:37.261213", "modified_by": "Administrator", "module": "CRM", "name": "Email Campaign", diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 1132226b90d..005c2b81850 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -15,7 +15,6 @@ class EmailCampaign(Document): #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.set_end_date() self.update_status() def validate_dates(self): @@ -25,114 +24,73 @@ class EmailCampaign(Document): if campaign.from_date and getdate(self.start_date) < getdate(campaign.from_date): frappe.throw(_("Email Campaign Start Date cannot be before Campaign Start Date")) - #check if email_schedule is exceeding the campaign end date - no_of_days = 0 - for entry in self.get("email_schedule"): - no_of_days += entry.send_after_days - email_schedule_end_date = add_days(getdate(self.start_date), no_of_days) - if campaign.to_date and getdate(email_schedule_end_date) > getdate(campaign.to_date): + #set the end date as start date + max(send after days) in campaign schedule + send_after_days = [] + for entry in campaign.get("campaign_schedule"): + send_after_days.append(entry.send_after_days) + end_date = add_days(getdate(self.start_date), max(send_after_days)) + + if campaign.to_date and getdate(end_date) > getdate(campaign.to_date): frappe.throw(_("Email Schedule cannot extend Campaign End Date")) + else: + self.end_date = end_date def validate_lead(self): - lead = frappe.get_doc("Lead", self.recipient) - if not lead.get("email_id"): + lead_email_id = frappe.db.get_value("Lead", self.recipient, 'email_id') + if not lead_email_id: frappe.throw(_("Please set an email id for lead communication")) - def set_end_date(self): - #set the end date as start date + max(send after days) in email schedule - send_after_days = [] - for entry in self.get("email_schedule"): - send_after_days.append(entry.send_after_days) - self.end_date = add_days(getdate(self.start_date), max(send_after_days)) - def update_status(self): start_date = getdate(self.start_date) end_date = getdate(self.end_date) today_date = getdate(today()) - if self.unsubscribed: - self.status = "Unsubscribed" - else: - if start_date > today_date: - self.status = "Scheduled" - elif end_date >= today_date: - self.status = "In Progress" - elif end_date < today_date: - self.status = "Completed" + if start_date > today_date: + self.status = "Scheduled" + elif end_date >= today_date: + self.status = "In Progress" + elif end_date < today_date: + self.status = "Completed" #called through hooks to send campaign mails to leads def send_email_to_leads(): - email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('not in', ['Unsubscribed', 'Completed', 'Scheduled']), 'unsubscribed': 0 }) - for campaign in email_campaigns: - email_campaign = frappe.get_doc("Email Campaign", campaign.name) - for entry in email_campaign.get("email_schedule"): + email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('not in', ['Unsubscribed', 'Completed', 'Scheduled']) }) + for camp in email_campaigns: + email_campaign = frappe.get_doc("Email Campaign", camp.name) + campaign = frappe.get_doc("Campaign", email_campaign.campaign_name) + for entry in campaign.get("campaign_schedule"): scheduled_date = add_days(email_campaign.get('start_date'), entry.get('send_after_days')) if scheduled_date == getdate(today()): send_mail(entry, email_campaign) def send_mail(entry, email_campaign): - if email_campaign.email_campaign_for == "Lead": - lead = frappe.get_doc("Lead", email_campaign.get("recipient")) - recipient_email = lead.email_id - elif email_campaign.email_campaign_for == "Contact": - recipient = frappe.get_doc("Contact", email_campaign.get("recipient")) - recipient_email = recipient.email_id + recipient = frappe.db.get_value(email_campaign.email_campaign_for, email_campaign.get("recipient"), 'email_id') + email_template = frappe.get_doc("Email Template", entry.get("email_template")) - sender = frappe.get_doc("User", email_campaign.get("sender")) - sender_email = sender.email + sender = frappe.db.get_value("User", email_campaign.get("sender"), 'email') + # send mail and link communication to document comm = make( doctype = "Email Campaign", name = email_campaign.name, subject = email_template.get("subject"), content = email_template.get("response"), - sender = sender_email, - recipients = recipient_email, + sender = sender, + recipients = recipient, communication_medium = "Email", sent_or_received = "Sent", - send_email = False, + send_email = True, email_template = email_template.name ) - frappe.sendmail( - recipients = recipient_email, - sender = sender_email, - subject = email_template.get("subject"), - content = email_template.get("response"), - reference_doctype = "Email Campaign", - reference_name = email_campaign.name, - unsubscribe_method = "/api/method/erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient", - unsubscribe_params = {"name": email_campaign.name, "email": recipient_email}, - unsubscribe_message = "Stop Getting Email Campaign Mails", - communication = comm.get("name") - ) @frappe.whitelist(allow_guest=True) -def unsubscribe_recipient(name, email): - # unsubsribe from comments and communications - try: - frappe.get_doc({ - "doctype": "Email Unsubscribe", - "email": email, - "reference_doctype": "Email Campaign", - "reference_name": name - }).insert(ignore_permissions=True) - - except frappe.DuplicateEntryError: - frappe.db.rollback() - - else: - frappe.db.commit() - frappe.db.set_value("Email Campaign", name, "unsubscribed", 1) - frappe.db.set_value("Email Campaign", name, "status", "Unsubscribed") - frappe.db.commit() - return_unsubscribed_page(email, name) - -def return_unsubscribed_page(email, name): - frappe.respond_as_web_page(_("Unsubscribed"), - _("{0} has left the Email Campaign {1}").format(email, name), - indicator_color='green') +#called from hooks on doc_event Email Unsubscribe +def unsubscribe_recipient(unsubscribe, method): + if unsubscribe.reference_doctype == 'Email Campaign': + frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed") #called through hooks to update email campaign status daily def set_email_campaign_status(): - email_campaigns = frappe.get_all("Email Campaign") - for email_campaign in email_campaigns: + email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('!=', 'Unsubscribed')}) + for entry in email_campaigns: + email_campaign = frappe.get_doc("Email Campaign", entry.name) email_campaign.update_status() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index e7a4bc4d14e..48d133fdff3 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -233,6 +233,9 @@ doc_events = { }, "Contact":{ "on_trash": "erpnext.support.doctype.issue.issue.update_issue" + }, + "Email Unsubscribe": { + "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" } } diff --git a/erpnext/selling/doctype/campaign/campaign.json b/erpnext/selling/doctype/campaign/campaign.json index d12069959cc..371a9d580dd 100644 --- a/erpnext/selling/doctype/campaign/campaign.json +++ b/erpnext/selling/doctype/campaign/campaign.json @@ -6,6 +6,7 @@ "description": "Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ", "doctype": "DocType", "document_type": "Setup", + "engine": "InnoDB", "field_order": [ "campaign", "campaign_name", @@ -18,6 +19,9 @@ "currency", "column_break2", "budget", + "schedule_section", + "campaign_schedule_section", + "campaign_schedule", "description_section", "description" ], @@ -53,13 +57,13 @@ "width": "300px" }, { + "default": "Planned", "fieldname": "status", "fieldtype": "Select", "in_list_view": 1, "label": "Status", "options": "\nPlanned\nIn Progress\nCompleted\nCancelled", - "reqd": 1, - "default": "Planned" + "reqd": 1 }, { "fieldname": "from_date", @@ -98,11 +102,26 @@ "fieldname": "budget_section", "fieldtype": "Section Break", "label": "BUDGET" + }, + { + "fieldname": "campaign_schedule_section", + "fieldtype": "Section Break", + "label": "Campaign Schedule" + }, + { + "fieldname": "campaign_schedule", + "fieldtype": "Table", + "label": "Campaign Schedule", + "options": "Campaign Email Schedule" + }, + { + "fieldname": "schedule_section", + "fieldtype": "Section Break" } ], "icon": "fa fa-bullhorn", "idx": 1, - "modified": "2019-04-29 22:09:39.251884", + "modified": "2019-07-12 11:52:47.196736", "modified_by": "Administrator", "module": "Selling", "name": "Campaign", @@ -140,5 +159,7 @@ "write": 1 } ], - "quick_entry": 1 -} + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py new file mode 100644 index 00000000000..a9d8eca38c2 --- /dev/null +++ b/erpnext/selling/doctype/campaign/campaign_dashboard.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'campaign_name', + 'transactions': [ + { + 'label': _('Email Campaigns'), + 'items': ['Email Campaign'] + } + ], + } From 2b7064a348cb865c9f9da35afd90592c7ced02d8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 12 Jul 2019 14:34:43 +0530 Subject: [PATCH 06/17] codacy fixes --- erpnext/crm/doctype/email_campaign/email_campaign.js | 1 - erpnext/crm/doctype/email_campaign/email_campaign_list.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.js b/erpnext/crm/doctype/email_campaign/email_campaign.js index 09ed84882d9..7549dbf7ef9 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.js +++ b/erpnext/crm/doctype/email_campaign/email_campaign.js @@ -3,7 +3,6 @@ frappe.ui.form.on('Email Campaign', { // refresh: function(frm) { - // } email_campaign_for: function(frm) { frm.set_value('recipient', ''); diff --git a/erpnext/crm/doctype/email_campaign/email_campaign_list.js b/erpnext/crm/doctype/email_campaign/email_campaign_list.js index d1bfdd31ee7..adc399da0f0 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign_list.js +++ b/erpnext/crm/doctype/email_campaign/email_campaign_list.js @@ -3,9 +3,9 @@ frappe.listview_settings['Email Campaign'] = { var colors = { "Unsubscribed": "red", "Scheduled": "blue", - "In Progress": "orange", - "Completed": "green" - } + "In Progress": "orange", + "Completed": "green" + }; return [__(doc.status), colors[doc.status], "status,=," + doc.status]; } }; From 7396bb893340098b3c4a71c551f7cdee490eb369 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 12 Jul 2019 14:47:02 +0530 Subject: [PATCH 07/17] codacy fixes --- erpnext/crm/doctype/email_campaign/email_campaign.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.js b/erpnext/crm/doctype/email_campaign/email_campaign.js index 7549dbf7ef9..87624422610 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.js +++ b/erpnext/crm/doctype/email_campaign/email_campaign.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Email Campaign', { // refresh: function(frm) { // } - email_campaign_for: function(frm) { - frm.set_value('recipient', ''); - } + email_campaign_for: function(frm) { + frm.set_value('recipient', ''); + } }); From c35a9a4888be66d49acdf0b63923adb56cce915f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 12 Jul 2019 15:49:05 +0530 Subject: [PATCH 08/17] codacy fixes --- erpnext/crm/doctype/email_campaign/email_campaign.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.js b/erpnext/crm/doctype/email_campaign/email_campaign.js index 87624422610..7549dbf7ef9 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.js +++ b/erpnext/crm/doctype/email_campaign/email_campaign.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Email Campaign', { // refresh: function(frm) { // } - email_campaign_for: function(frm) { - frm.set_value('recipient', ''); - } + email_campaign_for: function(frm) { + frm.set_value('recipient', ''); + } }); From 4879e00d646daeed206a390ae6656a291c55f909 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 12 Jul 2019 16:09:32 +0530 Subject: [PATCH 09/17] codacy fixes --- erpnext/crm/doctype/email_campaign/email_campaign.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.js b/erpnext/crm/doctype/email_campaign/email_campaign.js index 7549dbf7ef9..a5137c8b60b 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.js +++ b/erpnext/crm/doctype/email_campaign/email_campaign.js @@ -2,8 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('Email Campaign', { - // refresh: function(frm) { - // } email_campaign_for: function(frm) { frm.set_value('recipient', ''); } From 0fe37fda0913b7c849787216ee529202973d6aa7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 12 Jul 2019 17:47:32 +0530 Subject: [PATCH 10/17] codacy fixes --- erpnext/crm/doctype/email_campaign/email_campaign.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.js b/erpnext/crm/doctype/email_campaign/email_campaign.js index a5137c8b60b..b0e93536094 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.js +++ b/erpnext/crm/doctype/email_campaign/email_campaign.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Email Campaign', { - email_campaign_for: function(frm) { - frm.set_value('recipient', ''); - } + email_campaign_for: function(frm) { + frm.set_value('recipient', ''); + } }); From bbb22ad082641285f748282ce4932b61851bcc12 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 15 Jul 2019 17:52:50 +0530 Subject: [PATCH 11/17] fix: child table naming --- .../doctype/email_campaign/email_campaign.py | 7 +++---- .../selling/doctype/campaign/campaign.json | 21 +++++++------------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 005c2b81850..25d54af333b 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -26,7 +26,7 @@ class EmailCampaign(Document): #set the end date as start date + max(send after days) in campaign schedule send_after_days = [] - for entry in campaign.get("campaign_schedule"): + for entry in campaign.get("campaign_schedules"): send_after_days.append(entry.send_after_days) end_date = add_days(getdate(self.start_date), max(send_after_days)) @@ -56,8 +56,8 @@ def send_email_to_leads(): email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('not in', ['Unsubscribed', 'Completed', 'Scheduled']) }) for camp in email_campaigns: email_campaign = frappe.get_doc("Email Campaign", camp.name) - campaign = frappe.get_doc("Campaign", email_campaign.campaign_name) - for entry in campaign.get("campaign_schedule"): + campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name) + for entry in campaign.get("campaign_schedules"): scheduled_date = add_days(email_campaign.get('start_date'), entry.get('send_after_days')) if scheduled_date == getdate(today()): send_mail(entry, email_campaign) @@ -82,7 +82,6 @@ def send_mail(entry, email_campaign): email_template = email_template.name ) -@frappe.whitelist(allow_guest=True) #called from hooks on doc_event Email Unsubscribe def unsubscribe_recipient(unsubscribe, method): if unsubscribe.reference_doctype == 'Email Campaign': diff --git a/erpnext/selling/doctype/campaign/campaign.json b/erpnext/selling/doctype/campaign/campaign.json index 371a9d580dd..ee2714f1086 100644 --- a/erpnext/selling/doctype/campaign/campaign.json +++ b/erpnext/selling/doctype/campaign/campaign.json @@ -19,9 +19,8 @@ "currency", "column_break2", "budget", - "schedule_section", - "campaign_schedule_section", - "campaign_schedule", + "campaign_schedules_section", + "campaign_schedules", "description_section", "description" ], @@ -104,24 +103,20 @@ "label": "BUDGET" }, { - "fieldname": "campaign_schedule_section", - "fieldtype": "Section Break", - "label": "Campaign Schedule" - }, - { - "fieldname": "campaign_schedule", + "fieldname": "campaign_schedules", "fieldtype": "Table", - "label": "Campaign Schedule", + "label": "Campaign Schedules", "options": "Campaign Email Schedule" }, { - "fieldname": "schedule_section", - "fieldtype": "Section Break" + "fieldname": "campaign_schedules_section", + "fieldtype": "Section Break", + "label": "Campaign Schedules" } ], "icon": "fa fa-bullhorn", "idx": 1, - "modified": "2019-07-12 11:52:47.196736", + "modified": "2019-07-15 17:45:06.168107", "modified_by": "Administrator", "module": "Selling", "name": "Campaign", From b54459e5883c578cfb66fedfcbe027900e01f9a0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 22 Jul 2019 03:23:40 +0530 Subject: [PATCH 12/17] dash: added Email Campaign to CRM dashboard --- erpnext/config/crm.py | 5 +++++ erpnext/crm/doctype/email_campaign/email_campaign.py | 8 +++++++- erpnext/hooks.py | 2 +- erpnext/www/lms/macros/__init__.py | 0 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 erpnext/www/lms/macros/__init__.py diff --git a/erpnext/config/crm.py b/erpnext/config/crm.py index e49fc60f63a..70784f3d5f7 100644 --- a/erpnext/config/crm.py +++ b/erpnext/config/crm.py @@ -141,6 +141,11 @@ def get_data(): "name": "Campaign", "description": _("Sales campaigns."), }, + { + "type": "doctype", + "name": "Email Campaign", + "description": _("Sends Mails to lead or contact based on a Campaign schedule"), + }, { "type": "doctype", "name": "SMS Center", diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 25d54af333b..fa4a4ed67fc 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -15,6 +15,7 @@ class EmailCampaign(Document): #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_email_campaign_already_exists() self.update_status() def validate_dates(self): @@ -40,6 +41,10 @@ class EmailCampaign(Document): if not lead_email_id: frappe.throw(_("Please set an email id for lead communication")) + def validate_email_campaign_already_exists(self): + if frappe.db.get_value("Email Campaign", {"campaign_name": self.campaign_name, "recipient": self.recipient, "status": "Active"}): + frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) + def update_status(self): start_date = getdate(self.start_date) end_date = getdate(self.end_date) @@ -52,7 +57,7 @@ class EmailCampaign(Document): self.status = "Completed" #called through hooks to send campaign mails to leads -def send_email_to_leads(): +def send_email_to_leads_or_contacts(): email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('not in', ['Unsubscribed', 'Completed', 'Scheduled']) }) for camp in email_campaigns: email_campaign = frappe.get_doc("Email Campaign", camp.name) @@ -81,6 +86,7 @@ def send_mail(entry, email_campaign): send_email = True, email_template = email_template.name ) + return comm #called from hooks on doc_event Email Unsubscribe def unsubscribe_recipient(unsubscribe, method): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 48d133fdff3..47d1a68efcb 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -275,7 +275,7 @@ scheduler_events = { "erpnext.projects.doctype.project.project.send_project_status_email_to_users", "erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", - "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads", + "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts", "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status" ], "daily_long": [ diff --git a/erpnext/www/lms/macros/__init__.py b/erpnext/www/lms/macros/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 8d994cb1f223856557a6f13e122b908a465650e0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 22 Jul 2019 03:26:40 +0530 Subject: [PATCH 13/17] fix: allow only 1 active Email Campaign for a lead/contact at a time --- erpnext/crm/doctype/email_campaign/email_campaign.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index fa4a4ed67fc..8821fd59e22 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -42,7 +42,7 @@ class EmailCampaign(Document): frappe.throw(_("Please set an email id for lead communication")) def validate_email_campaign_already_exists(self): - if frappe.db.get_value("Email Campaign", {"campaign_name": self.campaign_name, "recipient": self.recipient, "status": "Active"}): + if frappe.db.get_value("Email Campaign", {"campaign_name": self.campaign_name, "recipient": self.recipient, "status": "In Progress"}): frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) def update_status(self): From d23a95c06e0b9c666609649204411bfd745b134b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 22 Jul 2019 12:26:52 +0530 Subject: [PATCH 14/17] fix: removed unnecessary fields from Campaign DocType and other fixes in Email Campaign --- .../doctype/email_campaign/email_campaign.py | 28 +++++----- .../selling/doctype/campaign/campaign.json | 53 +------------------ 2 files changed, 13 insertions(+), 68 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 8821fd59e22..719c0d078e4 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -11,38 +11,34 @@ from frappe.core.doctype.communication.email import make class EmailCampaign(Document): def validate(self): - self.validate_dates() + 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_email_campaign_already_exists() self.update_status() - def validate_dates(self): - campaign = frappe.get_doc("Campaign", self.campaign_name) - - #email campaign cannot start before campaign - if campaign.from_date and getdate(self.start_date) < getdate(campaign.from_date): - frappe.throw(_("Email Campaign Start Date cannot be before Campaign Start Date")) - + def set_date(self): + if getdate(self.start_date) < getdate(today()): + frappe.throw(_("Start Date cannot be before the current date")) #set the end date as start date + max(send after days) in campaign schedule send_after_days = [] + campaign = frappe.get_doc("Campaign", self.campaign_name) for entry in campaign.get("campaign_schedules"): send_after_days.append(entry.send_after_days) - end_date = add_days(getdate(self.start_date), max(send_after_days)) - - if campaign.to_date and getdate(end_date) > getdate(campaign.to_date): - frappe.throw(_("Email Schedule cannot extend Campaign End Date")) - else: - self.end_date = end_date + try: + end_date = add_days(getdate(self.start_date), max(send_after_days)) + except ValueError: + frappe.throw(_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name)) def validate_lead(self): lead_email_id = frappe.db.get_value("Lead", self.recipient, 'email_id') if not lead_email_id: - frappe.throw(_("Please set an email id for lead communication")) + 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_email_campaign_already_exists(self): - if frappe.db.get_value("Email Campaign", {"campaign_name": self.campaign_name, "recipient": self.recipient, "status": "In Progress"}): + if frappe.db.get_value("Email Campaign", {"campaign_name": self.campaign_name, "recipient": self.recipient, "status": ("in", ["In Progress", "Scheduled"])}): frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) def update_status(self): diff --git a/erpnext/selling/doctype/campaign/campaign.json b/erpnext/selling/doctype/campaign/campaign.json index ee2714f1086..986ac1306cd 100644 --- a/erpnext/selling/doctype/campaign/campaign.json +++ b/erpnext/selling/doctype/campaign/campaign.json @@ -11,14 +11,6 @@ "campaign", "campaign_name", "naming_series", - "from_date", - "column_break1", - "status", - "to_date", - "budget_section", - "currency", - "column_break2", - "budget", "campaign_schedules_section", "campaign_schedules", "description_section", @@ -55,53 +47,10 @@ "oldfieldtype": "Text", "width": "300px" }, - { - "default": "Planned", - "fieldname": "status", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Status", - "options": "\nPlanned\nIn Progress\nCompleted\nCancelled", - "reqd": 1 - }, - { - "fieldname": "from_date", - "fieldtype": "Date", - "label": "From Date" - }, - { - "fieldname": "to_date", - "fieldtype": "Date", - "label": "To Date" - }, - { - "fieldname": "column_break1", - "fieldtype": "Column Break" - }, - { - "fieldname": "budget", - "fieldtype": "Currency", - "label": "Budget" - }, { "fieldname": "description_section", "fieldtype": "Section Break" }, - { - "fieldname": "currency", - "fieldtype": "Link", - "label": "Currency", - "options": "Currency" - }, - { - "fieldname": "column_break2", - "fieldtype": "Column Break" - }, - { - "fieldname": "budget_section", - "fieldtype": "Section Break", - "label": "BUDGET" - }, { "fieldname": "campaign_schedules", "fieldtype": "Table", @@ -116,7 +65,7 @@ ], "icon": "fa fa-bullhorn", "idx": 1, - "modified": "2019-07-15 17:45:06.168107", + "modified": "2019-07-22 12:03:39.832342", "modified_by": "Administrator", "module": "Selling", "name": "Campaign", From 62242665129e06f8b27a360a9bf4042d26da0dce Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 22 Jul 2019 13:04:43 +0530 Subject: [PATCH 15/17] fix: replaced frappe.db.get_value with frappe.db.exists --- erpnext/crm/doctype/email_campaign/email_campaign.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 719c0d078e4..1a0bb2c2596 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -38,7 +38,12 @@ class EmailCampaign(Document): frappe.throw(_("Please set an email id for the Lead {0}").format(lead_name)) def validate_email_campaign_already_exists(self): - if frappe.db.get_value("Email Campaign", {"campaign_name": self.campaign_name, "recipient": self.recipient, "status": ("in", ["In Progress", "Scheduled"])}): + email_campaign_exists = frappe.db.exists("Email Campaign", { + "campaign_name": self.campaign_name, + "recipient": self.recipient, + "status": ("in", ["In Progress", "Scheduled"]) + }) + if email_campaign_exists: frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) def update_status(self): From 433e587a956426c79c3c1fa2c9648d3ab850c63b Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Mon, 22 Jul 2019 13:21:35 +0530 Subject: [PATCH 16/17] style: Add tabs --- erpnext/crm/doctype/email_campaign/email_campaign.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 1a0bb2c2596..98e4927beb6 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -39,9 +39,9 @@ class EmailCampaign(Document): def validate_email_campaign_already_exists(self): email_campaign_exists = frappe.db.exists("Email Campaign", { - "campaign_name": self.campaign_name, - "recipient": self.recipient, - "status": ("in", ["In Progress", "Scheduled"]) + "campaign_name": self.campaign_name, + "recipient": self.recipient, + "status": ("in", ["In Progress", "Scheduled"]) }) if email_campaign_exists: frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) From 3f8326358b1b287adb0a778df20364c5ccac892f Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 22 Jul 2019 16:24:56 +0530 Subject: [PATCH 17/17] fix: remove wrong status update for order type maintenance (#18444) --- erpnext/controllers/status_updater.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index d8c50b2622f..b2057ca40f9 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -40,7 +40,6 @@ status_map = { ["To Bill", "eval:self.per_delivered == 100 and self.per_billed < 100 and self.docstatus == 1"], ["To Deliver", "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_delivered == 100 and self.per_billed == 100 and self.docstatus == 1"], - ["Completed", "eval:self.order_type == 'Maintenance' and self.per_billed == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed'"], ["On Hold", "eval:self.status=='On Hold'"],