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/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'"], 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..1481a32d5b0 --- /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": [ + "email_template", + "send_after_days" + ], + "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-07-12 11:46:43.184123", + "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..b0e93536094 --- /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', { + 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 new file mode 100644 index 00000000000..32591362753 --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign.json @@ -0,0 +1,95 @@ +{ + "autoname": "format:MAIL-CAMP-{YYYY}-{#####}", + "creation": "2019-06-30 16:05:30.015615", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign_name", + "email_campaign_for", + "recipient", + "sender", + "column_break_4", + "start_date", + "end_date", + "status" + ], + "fields": [ + { + "fieldname": "campaign_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Campaign", + "options": "Campaign", + "reqd": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "\nScheduled\nIn Progress\nCompleted\nUnsubscribed", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "reqd": 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" + } + ], + "modified": "2019-07-12 13:47:37.261213", + "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..98e4927beb6 --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -0,0 +1,102 @@ +# -*- 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 import _ +from frappe.utils import getdate, add_days, today, nowdate, cstr +from frappe.model.document import Document +from frappe.core.doctype.communication.email import make + +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_email_campaign_already_exists() + self.update_status() + + 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) + 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: + 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): + 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): + start_date = getdate(self.start_date) + end_date = getdate(self.end_date) + today_date = getdate(today()) + 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_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) + 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) + +def send_mail(entry, email_campaign): + 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.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, + recipients = recipient, + communication_medium = "Email", + sent_or_received = "Sent", + send_email = True, + email_template = email_template.name + ) + return comm + +#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", 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/crm/doctype/email_campaign/email_campaign_list.js b/erpnext/crm/doctype/email_campaign/email_campaign_list.js new file mode 100644 index 00000000000..adc399da0f0 --- /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/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 diff --git a/erpnext/hooks.py b/erpnext/hooks.py index d814700a458..47d1a68efcb 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" } } @@ -272,6 +275,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_or_contacts", + "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" diff --git a/erpnext/selling/doctype/campaign/campaign.json b/erpnext/selling/doctype/campaign/campaign.json index d12069959cc..986ac1306cd 100644 --- a/erpnext/selling/doctype/campaign/campaign.json +++ b/erpnext/selling/doctype/campaign/campaign.json @@ -6,18 +6,13 @@ "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", "naming_series", - "from_date", - "column_break1", - "status", - "to_date", - "budget_section", - "currency", - "column_break2", - "budget", + "campaign_schedules_section", + "campaign_schedules", "description_section", "description" ], @@ -52,57 +47,25 @@ "oldfieldtype": "Text", "width": "300px" }, - { - "fieldname": "status", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Status", - "options": "\nPlanned\nIn Progress\nCompleted\nCancelled", - "reqd": 1, - "default": "Planned" - }, - { - "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": "campaign_schedules", + "fieldtype": "Table", + "label": "Campaign Schedules", + "options": "Campaign Email Schedule" }, { - "fieldname": "column_break2", - "fieldtype": "Column Break" - }, - { - "fieldname": "budget_section", + "fieldname": "campaign_schedules_section", "fieldtype": "Section Break", - "label": "BUDGET" + "label": "Campaign Schedules" } ], "icon": "fa fa-bullhorn", "idx": 1, - "modified": "2019-04-29 22:09:39.251884", + "modified": "2019-07-22 12:03:39.832342", "modified_by": "Administrator", "module": "Selling", "name": "Campaign", @@ -140,5 +103,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'] + } + ], + }