diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.json b/crm/fcrm/doctype/crm_deal/crm_deal.json
index 4cf453ea..64b8829c 100644
--- a/crm/fcrm/doctype/crm_deal/crm_deal.json
+++ b/crm/fcrm/doctype/crm_deal/crm_deal.json
@@ -24,7 +24,17 @@
"deal_owner",
"section_break_eepu",
"lead",
- "column_break_bqvs"
+ "column_break_bqvs",
+ "sla_tab",
+ "sla",
+ "sla_creation",
+ "column_break_pfvq",
+ "sla_status",
+ "response_details_section",
+ "response_by",
+ "column_break_hpvj",
+ "first_response_time",
+ "first_responded_on"
],
"fields": [
{
@@ -134,11 +144,62 @@
"fieldname": "organization_tab",
"fieldtype": "Tab Break",
"label": "Organization"
+ },
+ {
+ "fieldname": "sla_tab",
+ "fieldtype": "Tab Break",
+ "label": "SLA",
+ "read_only": 1
+ },
+ {
+ "fieldname": "sla",
+ "fieldtype": "Link",
+ "label": "SLA",
+ "options": "CRM Service Level Agreement"
+ },
+ {
+ "fieldname": "response_by",
+ "fieldtype": "Datetime",
+ "label": "Response By"
+ },
+ {
+ "fieldname": "column_break_pfvq",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "sla_status",
+ "fieldtype": "Select",
+ "label": "SLA Status",
+ "options": "\nFirst Response Due\nFailed\nFulfilled"
+ },
+ {
+ "fieldname": "sla_creation",
+ "fieldtype": "Datetime",
+ "label": "SLA Creation"
+ },
+ {
+ "fieldname": "response_details_section",
+ "fieldtype": "Section Break",
+ "label": "Response Details"
+ },
+ {
+ "fieldname": "column_break_hpvj",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "first_response_time",
+ "fieldtype": "Duration",
+ "label": "First Response Time"
+ },
+ {
+ "fieldname": "first_responded_on",
+ "fieldtype": "Datetime",
+ "label": "First Responded On"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-11-29 11:31:46.968519",
+ "modified": "2023-12-11 12:37:51.198228",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Deal",
diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.py b/crm/fcrm/doctype/crm_deal/crm_deal.py
index 78032c55..87cdfb72 100644
--- a/crm/fcrm/doctype/crm_deal/crm_deal.py
+++ b/crm/fcrm/doctype/crm_deal/crm_deal.py
@@ -7,10 +7,16 @@ from frappe.model.document import Document
class CRMDeal(Document):
+ def before_validate(self):
+ self.set_sla()
+
def validate(self):
self.set_primary_contact()
self.set_primary_email_mobile_no()
+ def before_save(self):
+ self.apply_sla()
+
def set_primary_contact(self, contact=None):
if not self.contacts:
return
@@ -45,6 +51,22 @@ class CRMDeal(Document):
self.email = ""
self.mobile_no = ""
+ def set_sla(self):
+ """
+ Find an SLA to apply to the deal.
+ """
+ if sla := get_sla("CRM Deal"):
+ if not sla:
+ return
+ self.sla = sla.name
+
+ def apply_sla(self):
+ """
+ Apply SLA if set.
+ """
+ if sla := frappe.get_last_doc("CRM Service Level Agreement", {"name": self.sla}):
+ sla.apply(self)
+
@staticmethod
def sort_options():
return [
@@ -113,6 +135,9 @@ class CRMDeal(Document):
"email",
"mobile_no",
"deal_owner",
+ "sla_status",
+ "first_response_time",
+ "first_responded_on",
"modified",
]
return {'columns': columns, 'rows': rows}
@@ -145,4 +170,10 @@ def set_primary_contact(deal, contact):
deal = frappe.get_cached_doc("CRM Deal", deal)
deal.set_primary_contact(contact)
deal.save()
- return True
\ No newline at end of file
+ return True
+
+def get_sla(doctype):
+ sla = frappe.db.exists("CRM Service Level Agreement", {"apply_on": doctype, "enabled": 1})
+ if not sla:
+ return None
+ return frappe.get_cached_doc("CRM Service Level Agreement", sla)
diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.json b/crm/fcrm/doctype/crm_lead/crm_lead.json
index 300f9901..c37c8835 100644
--- a/crm/fcrm/doctype/crm_lead/crm_lead.json
+++ b/crm/fcrm/doctype/crm_lead/crm_lead.json
@@ -34,7 +34,17 @@
"no_of_employees",
"annual_revenue",
"image",
- "converted"
+ "converted",
+ "sla_tab",
+ "sla",
+ "sla_creation",
+ "column_break_ffnp",
+ "sla_status",
+ "response_details_section",
+ "response_by",
+ "column_break_pweh",
+ "first_response_time",
+ "first_responded_on"
],
"fields": [
{
@@ -196,12 +206,63 @@
"fieldname": "details",
"fieldtype": "Tab Break",
"label": "Details"
+ },
+ {
+ "fieldname": "sla_tab",
+ "fieldtype": "Tab Break",
+ "label": "SLA",
+ "read_only": 1
+ },
+ {
+ "fieldname": "sla",
+ "fieldtype": "Link",
+ "label": "SLA",
+ "options": "CRM Service Level Agreement"
+ },
+ {
+ "fieldname": "sla_creation",
+ "fieldtype": "Datetime",
+ "label": "SLA Creation"
+ },
+ {
+ "fieldname": "column_break_ffnp",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "sla_status",
+ "fieldtype": "Select",
+ "label": "SLA Status",
+ "options": "\nFirst Response Due\nFailed\nFulfilled"
+ },
+ {
+ "fieldname": "response_details_section",
+ "fieldtype": "Section Break",
+ "label": "Response Details"
+ },
+ {
+ "fieldname": "response_by",
+ "fieldtype": "Datetime",
+ "label": "Response By"
+ },
+ {
+ "fieldname": "column_break_pweh",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "first_response_time",
+ "fieldtype": "Duration",
+ "label": "First Response Time"
+ },
+ {
+ "fieldname": "first_responded_on",
+ "fieldtype": "Datetime",
+ "label": "First Responded On"
}
],
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-11-29 11:31:08.555096",
+ "modified": "2023-12-10 13:54:53.630114",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Lead",
diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.py b/crm/fcrm/doctype/crm_lead/crm_lead.py
index d41cfd3f..e9e20487 100644
--- a/crm/fcrm/doctype/crm_lead/crm_lead.py
+++ b/crm/fcrm/doctype/crm_lead/crm_lead.py
@@ -9,12 +9,18 @@ from frappe.utils import has_gravatar, validate_email_address
class CRMLead(Document):
+ def before_validate(self):
+ self.set_sla()
+
def validate(self):
self.set_full_name()
self.set_lead_name()
self.set_title()
self.validate_email()
+ def before_save(self):
+ self.apply_sla()
+
def set_full_name(self):
if self.first_name:
self.lead_name = " ".join(
@@ -121,6 +127,22 @@ class CRMLead(Document):
deal.insert(ignore_permissions=True)
return deal.name
+ def set_sla(self):
+ """
+ Find an SLA to apply to the lead.
+ """
+ if sla := get_sla("CRM Lead"):
+ if not sla:
+ return
+ self.sla = sla.name
+
+ def apply_sla(self):
+ """
+ Apply SLA if set.
+ """
+ if sla := frappe.get_last_doc("CRM Service Level Agreement", {"name": self.sla}):
+ sla.apply(self)
+
@staticmethod
def sort_options():
return [
@@ -193,6 +215,9 @@ class CRMLead(Document):
"mobile_no",
"lead_owner",
"first_name",
+ "sla_status",
+ "first_response_time",
+ "first_responded_on",
"modified",
"image",
]
@@ -209,4 +234,10 @@ def convert_to_deal(lead):
contact = lead.create_contact(False)
deal = lead.create_deal(contact)
lead.save()
- return deal
\ No newline at end of file
+ return deal
+
+def get_sla(doctype):
+ sla = frappe.db.exists("CRM Service Level Agreement", {"apply_on": doctype, "enabled": 1})
+ if not sla:
+ return None
+ return frappe.get_cached_doc("CRM Service Level Agreement", sla)
diff --git a/crm/fcrm/doctype/crm_service_day/__init__.py b/crm/fcrm/doctype/crm_service_day/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/crm/fcrm/doctype/crm_service_day/crm_service_day.json b/crm/fcrm/doctype/crm_service_day/crm_service_day.json
new file mode 100644
index 00000000..ddca9883
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_day/crm_service_day.json
@@ -0,0 +1,59 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-12-04 16:07:20.400084",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "workday",
+ "section_break_uegc",
+ "start_time",
+ "column_break_maie",
+ "end_time"
+ ],
+ "fields": [
+ {
+ "fieldname": "workday",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Workday",
+ "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_uegc",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "start_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "Start Time",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_maie",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "end_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "End Time",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-12-04 16:09:22.928308",
+ "modified_by": "Administrator",
+ "module": "FCRM",
+ "name": "CRM Service Day",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/crm/fcrm/doctype/crm_service_day/crm_service_day.py b/crm/fcrm/doctype/crm_service_day/crm_service_day.py
new file mode 100644
index 00000000..724a720d
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_day/crm_service_day.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CRMServiceDay(Document):
+ pass
diff --git a/crm/fcrm/doctype/crm_service_level_agreement/__init__.py b/crm/fcrm/doctype/crm_service_level_agreement/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.js b/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.js
new file mode 100644
index 00000000..d1c3549c
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.js
@@ -0,0 +1,21 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("CRM Service Level Agreement", {
+// refresh(frm) {
+
+// },
+// });
+
+frappe.ui.form.on("CRM Service Level Priority", {
+ priorities_add: function (frm, cdt, cdn) {
+ if (frm.doc.apply_on == "CRM Deal") {
+ frappe.model.set_value(
+ cdt,
+ cdn,
+ "reference_doctype",
+ "CRM Deal Status"
+ );
+ }
+ },
+});
diff --git a/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.json b/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.json
new file mode 100644
index 00000000..59603dd6
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.json
@@ -0,0 +1,109 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:sla_name",
+ "creation": "2023-12-04 13:07:18.426211",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "sla_name",
+ "apply_on",
+ "enabled",
+ "default",
+ "column_break_uxua",
+ "condition",
+ "section_break_ufaf",
+ "priorities",
+ "section_break_rmgo",
+ "working_hours"
+ ],
+ "fields": [
+ {
+ "fieldname": "sla_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "SLA Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "label": "Enabled"
+ },
+ {
+ "default": "0",
+ "fieldname": "default",
+ "fieldtype": "Check",
+ "label": "Default"
+ },
+ {
+ "fieldname": "column_break_uxua",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "Simple Python Expression, Example: doc.status == 'Open' and doc.lead_source == 'Ads'",
+ "fieldname": "condition",
+ "fieldtype": "Code",
+ "label": "Condition",
+ "options": "Python"
+ },
+ {
+ "fieldname": "apply_on",
+ "fieldtype": "Link",
+ "label": "Apply On",
+ "link_filters": "[[{\"fieldname\":\"apply_on\",\"field_option\":\"DocType\"},\"name\",\"in\",[\"CRM Lead\",\"CRM Deal\"]]]",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_ufaf",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "priorities",
+ "fieldtype": "Table",
+ "label": "Priorities",
+ "options": "CRM Service Level Priority",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_rmgo",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "working_hours",
+ "fieldtype": "Table",
+ "label": "Working Hours",
+ "options": "CRM Service Day",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2023-12-04 16:13:24.638239",
+ "modified_by": "Administrator",
+ "module": "FCRM",
+ "name": "CRM Service Level Agreement",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.py b/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.py
new file mode 100644
index 00000000..2debf7f1
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.py
@@ -0,0 +1,188 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from datetime import timedelta
+from frappe.model.document import Document
+from frappe.utils import (
+ add_to_date,
+ get_datetime,
+ get_weekdays,
+ getdate,
+ now_datetime,
+ time_diff_in_seconds,
+)
+
+
+class CRMServiceLevelAgreement(Document):
+ def apply(self, doc: Document):
+ self.handle_new(doc)
+ self.handle_status(doc)
+ self.handle_targets(doc)
+ self.handle_sla_status(doc)
+
+ def handle_new(self, doc: Document):
+ if not doc.is_new():
+ return
+ creation = doc.sla_creation or now_datetime()
+ doc.sla_creation = creation
+
+ def handle_status(self, doc: Document):
+ if doc.is_new() or not doc.has_value_changed("status"):
+ return
+ self.set_first_responded_on(doc)
+ self.set_first_response_time(doc)
+
+ def set_first_responded_on(self, doc: Document):
+ if doc.status != self.get_default_priority():
+ doc.first_responded_on = (
+ doc.first_responded_on or now_datetime()
+ )
+
+ def set_first_response_time(self, doc: Document):
+ start_at = doc.sla_creation
+ end_at = doc.first_responded_on
+ if not start_at or not end_at:
+ return
+ doc.first_response_time = self.calc_elapsed_time(start_at, end_at)
+
+ def handle_targets(self, doc: Document):
+ self.set_response_by(doc)
+
+ def set_response_by(self, doc: Document):
+ start_time = doc.sla_creation
+ status = doc.status
+
+ priorities = self.get_priorities()
+ priority = priorities.get(status)
+ if not priority or doc.response_by:
+ return
+
+ first_response_time = priority.get("first_response_time", 0)
+ end_time = self.calc_time(start_time, first_response_time)
+ if end_time:
+ doc.response_by = end_time
+
+ def handle_sla_status(self, doc: Document):
+ is_failed = self.is_first_response_failed(doc)
+ options = {
+ "Fulfilled": True,
+ "First Response Due": not doc.first_responded_on,
+ "Failed": is_failed,
+ }
+ for status in options:
+ if options[status]:
+ doc.sla_status = status
+
+ def is_first_response_failed(self, doc: Document):
+ if not doc.first_responded_on:
+ return get_datetime(doc.response_by) < now_datetime()
+ return get_datetime(doc.response_by) < get_datetime(doc.first_responded_on)
+
+ def calc_time(
+ self,
+ start_at: str,
+ duration_seconds: int,
+ ):
+ res = get_datetime(start_at)
+ time_needed = duration_seconds
+ holidays = []
+ weekdays = get_weekdays()
+ workdays = self.get_workdays()
+ while time_needed:
+ today = res
+ today_day = getdate(today)
+ today_weekday = weekdays[today.weekday()]
+ is_workday = today_weekday in workdays
+ is_holiday = today_day in holidays
+ if is_holiday or not is_workday:
+ res = add_to_date(res, days=1, as_datetime=True)
+ continue
+ today_workday = workdays[today_weekday]
+ now_in_seconds = time_diff_in_seconds(today, today_day)
+ start_time = max(today_workday.start_time.total_seconds(), now_in_seconds)
+ till_start_time = max(start_time - now_in_seconds, 0)
+ end_time = max(today_workday.end_time.total_seconds(), now_in_seconds)
+ time_left = max(end_time - start_time, 0)
+ if not time_left:
+ res = getdate(add_to_date(res, days=1, as_datetime=True))
+ continue
+ time_taken = min(time_needed, time_left)
+ time_needed -= time_taken
+ time_required = till_start_time + time_taken
+ res = add_to_date(res, seconds=time_required, as_datetime=True)
+ return res
+
+ def calc_elapsed_time(self, start_time, end_time) -> float:
+ """
+ Get took from start to end, excluding non-working hours
+
+ :param start_at: Date at which calculation starts
+ :param end_at: Date at which calculation ends
+ :return: Number of seconds
+ """
+ start_time = get_datetime(start_time)
+ end_time = get_datetime(end_time)
+ holiday_list = []
+ working_day_list = self.get_working_days()
+ working_hours = self.get_working_hours()
+
+ total_seconds = 0
+ current_time = start_time
+
+ while current_time < end_time:
+ in_holiday_list = current_time.date() in holiday_list
+ not_in_working_day_list = get_weekdays()[current_time.weekday()] not in working_day_list
+ if in_holiday_list or not_in_working_day_list or not self.is_working_time(current_time, working_hours):
+ current_time += timedelta(seconds=1)
+ continue
+ total_seconds += 1
+ current_time += timedelta(seconds=1)
+
+ return total_seconds
+
+ def get_priorities(self):
+ """
+ Return priorities related info as a dict. With `priority` as key
+ """
+ res = {}
+ for row in self.priorities:
+ res[row.priority] = row
+ return res
+
+ def get_default_priority(self):
+ """
+ Return default priority
+ """
+ for row in self.priorities:
+ if row.default_priority:
+ return row.priority
+
+ return self.priorities[0].priority
+
+ def get_workdays(self) -> dict[str, dict]:
+ """
+ Return workdays related info as a dict. With `workday` as key
+ """
+ res = {}
+ for row in self.working_hours:
+ res[row.workday] = row
+ return res
+
+ def get_working_days(self) -> dict[str, dict]:
+ workdays = []
+ for row in self.working_hours:
+ workdays.append(row.workday)
+ return workdays
+
+ def get_working_hours(self) -> dict[str, dict]:
+ res = {}
+ for row in self.working_hours:
+ res[row.workday] = (row.start_time, row.end_time)
+ return res
+
+ def is_working_time(self, date_time, working_hours):
+ day_of_week = get_weekdays()[date_time.weekday()]
+ start_time, end_time = working_hours.get(day_of_week, (0, 0))
+ date_time = timedelta(hours=date_time.hour, minutes=date_time.minute, seconds=date_time.second)
+ return start_time <= date_time < end_time
diff --git a/crm/fcrm/doctype/crm_service_level_agreement/test_crm_service_level_agreement.py b/crm/fcrm/doctype/crm_service_level_agreement/test_crm_service_level_agreement.py
new file mode 100644
index 00000000..40e2555e
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_level_agreement/test_crm_service_level_agreement.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestCRMServiceLevelAgreement(FrappeTestCase):
+ pass
diff --git a/crm/fcrm/doctype/crm_service_level_priority/__init__.py b/crm/fcrm/doctype/crm_service_level_priority/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.js b/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.js
new file mode 100644
index 00000000..ef808235
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("CRM Service Level Priority", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.json b/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.json
new file mode 100644
index 00000000..294b94a7
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.json
@@ -0,0 +1,57 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-12-04 13:18:58.028384",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "default_priority",
+ "reference_doctype",
+ "priority",
+ "first_response_time"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "fieldname": "default_priority",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Default Priority"
+ },
+ {
+ "fieldname": "priority",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Priority",
+ "options": "reference_doctype",
+ "reqd": 1
+ },
+ {
+ "fieldname": "first_response_time",
+ "fieldtype": "Duration",
+ "in_list_view": 1,
+ "label": "First Response TIme",
+ "reqd": 1
+ },
+ {
+ "default": "CRM Lead Status",
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "label": "DocType",
+ "options": "DocType"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-12-04 14:05:42.838493",
+ "modified_by": "Administrator",
+ "module": "FCRM",
+ "name": "CRM Service Level Priority",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.py b/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.py
new file mode 100644
index 00000000..a7210a82
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CRMServiceLevelPriority(Document):
+ pass
diff --git a/crm/fcrm/doctype/crm_service_level_priority/test_crm_service_level_priority.py b/crm/fcrm/doctype/crm_service_level_priority/test_crm_service_level_priority.py
new file mode 100644
index 00000000..f1f5448b
--- /dev/null
+++ b/crm/fcrm/doctype/crm_service_level_priority/test_crm_service_level_priority.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestCRMServiceLevelPriority(FrappeTestCase):
+ pass
diff --git a/crm/fcrm/workspace/frappe_crm/frappe_crm.json b/crm/fcrm/workspace/frappe_crm/frappe_crm.json
index 6c56ef5a..164fa444 100644
--- a/crm/fcrm/workspace/frappe_crm/frappe_crm.json
+++ b/crm/fcrm/workspace/frappe_crm/frappe_crm.json
@@ -1,19 +1,19 @@
{
"charts": [],
- "content": "[{\"id\":\"1nr6UkvDiL\",\"type\":\"header\",\"data\":{\"text\":\"PORTAL\",\"col\":12}},{\"id\":\"1hyi8SysUY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"CRM Portal Page\",\"col\":3}},{\"id\":\"ktENiGaqXQ\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"VgeWLYOuAS\",\"type\":\"paragraph\",\"data\":{\"text\":\"SHORTCUTS\",\"col\":12}},{\"id\":\"A66FpG-K3T\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leads\",\"col\":3}},{\"id\":\"n9b6N5ebOj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Deals\",\"col\":3}},{\"id\":\"sGHTXrludH\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Organizations\",\"col\":3}},{\"id\":\"uXZNCdqxy0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Contacts\",\"col\":3}},{\"id\":\"TZ7cULX3Tk\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"zpySv0nGVQ\",\"type\":\"paragraph\",\"data\":{\"text\":\"META\",\"col\":12}},{\"id\":\"fa-uKzobpp\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead Statuses\",\"col\":3}},{\"id\":\"hxoZghUHP2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Deal Statuses\",\"col\":3}},{\"id\":\"HbgghUpc8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead Sources\",\"col\":3}},{\"id\":\"8cPs7Fohb4\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Industries\",\"col\":3}}]",
+ "content": "[{\"id\":\"1nr6UkvDiL\",\"type\":\"header\",\"data\":{\"text\":\"PORTAL\",\"col\":12}},{\"id\":\"1hyi8SysUY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"CRM Portal Page\",\"col\":3}},{\"id\":\"ktENiGaqXQ\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"VgeWLYOuAS\",\"type\":\"paragraph\",\"data\":{\"text\":\"SHORTCUTS\",\"col\":12}},{\"id\":\"A66FpG-K3T\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leads\",\"col\":3}},{\"id\":\"n9b6N5ebOj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Deals\",\"col\":3}},{\"id\":\"sGHTXrludH\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Organizations\",\"col\":3}},{\"id\":\"uXZNCdqxy0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Contacts\",\"col\":3}},{\"id\":\"v1kkMwlntf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"SLA\",\"col\":3}},{\"id\":\"TZ7cULX3Tk\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"zpySv0nGVQ\",\"type\":\"paragraph\",\"data\":{\"text\":\"META\",\"col\":12}},{\"id\":\"fa-uKzobpp\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead Statuses\",\"col\":3}},{\"id\":\"hxoZghUHP2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Deal Statuses\",\"col\":3}},{\"id\":\"HbgghUpc8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead Sources\",\"col\":3}},{\"id\":\"8cPs7Fohb4\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Industries\",\"col\":3}}]",
"creation": "2023-11-27 13:55:17.090361",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
- "icon": "crm",
+ "icon": "filter",
"idx": 0,
- "indicator_color": "green",
+ "indicator_color": "",
"is_hidden": 0,
"label": "Frappe CRM",
"links": [],
- "modified": "2023-11-29 13:37:56.731645",
+ "modified": "2023-12-11 13:06:13.532693",
"modified_by": "Administrator",
"module": "FCRM",
"name": "Frappe CRM",
@@ -40,6 +40,14 @@
"type": "URL",
"url": "/crm"
},
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "SLA",
+ "link_to": "CRM Service Level Agreement",
+ "stats_filter": "[]",
+ "type": "DocType"
+ },
{
"color": "Grey",
"doc_view": "List",
diff --git a/frontend/src/components/ListViews/DealsListView.vue b/frontend/src/components/ListViews/DealsListView.vue
index 1b5618b4..72efab98 100644
--- a/frontend/src/components/ListViews/DealsListView.vue
+++ b/frontend/src/components/ListViews/DealsListView.vue
@@ -44,9 +44,31 @@