Merge branch 'develop' into sla-enhancements
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a0707b7..db9610b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -697,3 +697,4 @@
erpnext.patches.v12_0.update_uom_conversion_factor
erpnext.patches.v13_0.delete_old_purchase_reports
erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions
+erpnext.patches.v13_0.update_sla_enhancements
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py
new file mode 100644
index 0000000..3eb0411
--- /dev/null
+++ b/erpnext/patches/v13_0/update_sla_enhancements.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2018, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+
+def execute():
+ # add holiday list and employee group fields in SLA
+ # change response and resolution time in priorities child table
+ if frappe.db.exists('DocType', 'Service Level Agreement'):
+ sla_details = frappe.db.get_all('Service Level Agreement', fields=['name', 'service_level'])
+ priorities = frappe.db.get_all('Service Level Priority', fields=['*'], filters={
+ 'parenttype': ('in', ['Service Level Agreement', 'Service Level'])
+ })
+
+ frappe.reload_doc('support', 'doctype', 'service_level_agreement')
+ frappe.reload_doc('support', 'doctype', 'service_level_priority')
+
+ for entry in sla_details:
+ values = frappe.db.get_value('Service Level', entry.service_level, ['holiday_list', 'employee_group'])
+ if values:
+ holiday_list = values[0]
+ employee_group = values[1]
+ frappe.db.set_value('Service Level Agreement', entry.name, {
+ 'holiday_list': holiday_list,
+ 'employee_group': employee_group
+ })
+
+ priority_dict = {}
+
+ for priority in priorities:
+ if priority.parenttype == 'Service Level Agreement':
+ response_time = convert_to_seconds(priority.response_time, priority.response_time_period)
+ resolution_time = convert_to_seconds(priority.resolution_time, priority.resolution_time_period)
+ frappe.db.set_value('Service Level Priority', priority.name, {
+ 'response_time': response_time,
+ 'resolution_time': resolution_time
+ })
+ if priority.parenttype == 'Service Level':
+ if not priority.parent in priority_dict:
+ priority_dict[priority.parent] = []
+ priority_dict[priority.parent].append(priority)
+
+
+ # copy Service Levels to Service Level Agreements
+ sl = [entry.service_level for entry in sla_details]
+ if frappe.db.exists('DocType', 'Service Level'):
+ service_levels = frappe.db.get_all('Service Level', filters={'service_level': ('not in', sl)}, fields=['*'])
+ for entry in service_levels:
+ sla = frappe.new_doc('Service Level Agreement')
+ sla.service_level = entry.service_level
+ sla.holiday_list = entry.holiday_list
+ sla.employee_group = entry.employee_group
+ sla.flags.ignore_validate = True
+ sla = sla.insert(ignore_mandatory=True)
+
+ frappe.db.sql("""
+ UPDATE
+ `tabService Day`
+ SET
+ parent = %(new_parent)s , parentfield = 'support_and_resolution', parenttype = 'Service Level Agreement'
+ WHERE
+ parent = %(old_parent)s
+ """, {'new_parent': sla.name, 'old_parent': entry.name}, as_dict = 1)
+
+ priority_list = priority_dict.get(entry.name)
+ if priority_list:
+ sla = frappe.get_doc('Service Level Agreement', sla.name)
+ for priority in priority_list:
+ row = sla.append('priorities', {
+ 'priority': priority.priority,
+ 'default_priority': priority.default_priority,
+ 'response_time': convert_to_seconds(priority.response_time, priority.response_time_period),
+ 'resolution_time': convert_to_seconds(priority.resolution_time, priority.resolution_time_period)
+ })
+ row.db_update()
+ sla.db_update()
+
+ frappe.delete_doc_if_exists('DocType', 'Service Level')
+
+
+def convert_to_seconds(value, unit):
+ seconds = 0
+ if unit == "Hour":
+ seconds = value * 3600
+ if unit == "Day":
+ seconds = value * 3600 * 24
+ if unit == "Week":
+ seconds = value * 3600 * 24 * 7
+ return seconds
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index e666a41..74ff0ec 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -105,3 +105,4 @@
"ref_doctype": "Company"
})
settings.save()
+
diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json
index a3fe72d..b1ad7c8 100644
--- a/erpnext/support/desk_page/support/support.json
+++ b/erpnext/support/desk_page/support/support.json
@@ -13,7 +13,7 @@
{
"hidden": 0,
"label": "Service Level Agreement",
- "links": "[\n {\n \"description\": \"Service Level.\",\n \"label\": \"Service Level\",\n \"name\": \"Service Level\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]"
+ "links": "[\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@@ -43,7 +43,7 @@
"idx": 0,
"is_standard": 1,
"label": "Support",
- "modified": "2020-05-28 13:51:23.869954",
+ "modified": "2020-06-04 11:54:56.124219",
"modified_by": "Administrator",
"module": "Support",
"name": "Support",
@@ -65,8 +65,8 @@
"type": "DocType"
},
{
- "label": "Service Level",
- "link_to": "Service Level",
+ "label": "Service Level Agreement",
+ "link_to": "Service Level Agreement",
"type": "DocType"
}
]
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index bad40cc..e7e5bd3 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -38,10 +38,35 @@
},
refresh: function (frm) {
-
if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") {
if (frm.doc.service_level_agreement) {
- set_time_to_resolve_and_response(frm);
+ frappe.call({
+ 'method': 'frappe.client.get',
+ args: {
+ doctype: 'Service Level Agreement',
+ name: frm.doc.service_level_agreement
+ },
+ callback: function(data) {
+ let statuses = data.message.pause_sla_on;
+ const hold_statuses = [];
+ $.each(statuses, (_i, entry) => {
+ hold_statuses.push(entry.status);
+ });
+ if (hold_statuses.includes(frm.doc.status)) {
+ frm.dashboard.clear_headline();
+ let message = {"indicator": "orange", "msg": __("SLA is on hold since {0}", [moment(frm.doc.on_hold_since).fromNow(true)])};
+ frm.dashboard.set_headline_alert(
+ '<div class="row">' +
+ '<div class="col-xs-12">' +
+ '<span class="indicator whitespace-nowrap '+ message.indicator +'"><span>'+ message.msg +'</span></span> ' +
+ '</div>' +
+ '</div>'
+ );
+ } else {
+ set_time_to_resolve_and_response(frm);
+ }
+ }
+ });
}
frm.add_custom_button(__("Close"), function () {
@@ -55,6 +80,7 @@
frm: frm
});
}, __("Make"));
+
} else {
if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();
diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json
index c12cef4..6525ab2 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -31,9 +31,13 @@
"resolution_by",
"resolution_by_variance",
"service_level_agreement_creation",
+ "on_hold_since",
+ "total_hold_time",
"response",
"mins_to_first_response",
"first_responded_on",
+ "column_break_26",
+ "avg_response_time",
"additional_info",
"lead",
"contact",
@@ -50,7 +54,9 @@
"resolution_date",
"content_type",
"attachment",
- "via_customer_portal"
+ "via_customer_portal",
+ "resolution_time",
+ "user_resolution_time"
],
"fields": [
{
@@ -114,7 +120,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "Open\nReplied\nHold\nClosed",
+ "options": "Open\nReplied\nHold\nResolved\nClosed",
"search_index": 1
},
{
@@ -161,6 +167,7 @@
"options": "Service Level Agreement"
},
{
+ "depends_on": "eval: doc.status != 'Replied';",
"fieldname": "response_by",
"fieldtype": "Datetime",
"label": "Response By",
@@ -174,6 +181,7 @@
"read_only": 1
},
{
+ "depends_on": "eval: doc.status != 'Replied';",
"fieldname": "resolution_by",
"fieldtype": "Datetime",
"label": "Resolution By",
@@ -328,7 +336,7 @@
"read_only": 1
},
{
- "depends_on": "eval: doc.service_level_agreement",
+ "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
"description": "in hours",
"fieldname": "response_by_variance",
"fieldtype": "Float",
@@ -336,7 +344,7 @@
"read_only": 1
},
{
- "depends_on": "eval: doc.service_level_agreement",
+ "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
"description": "in hours",
"fieldname": "resolution_by_variance",
"fieldtype": "Float",
@@ -362,12 +370,48 @@
"label": "Issue Split From",
"options": "Issue",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
+ },
+ {
+ "bold": 1,
+ "fieldname": "avg_response_time",
+ "fieldtype": "Duration",
+ "label": "Average Response Time",
+ "read_only": 1
+ },
+ {
+ "fieldname": "resolution_time",
+ "fieldtype": "Duration",
+ "label": "Resolution Time",
+ "read_only": 1
+ },
+ {
+ "fieldname": "user_resolution_time",
+ "fieldtype": "Duration",
+ "label": "User Resolution Time",
+ "read_only": 1
+ },
+ {
+ "fieldname": "on_hold_since",
+ "fieldtype": "Datetime",
+ "hidden": 1,
+ "label": "On Hold Since",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_hold_time",
+ "fieldtype": "Duration",
+ "label": "Total Hold Time",
+ "read_only": 1
}
],
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
- "modified": "2020-03-13 02:19:49.477928",
+ "modified": "2020-06-10 12:47:37.146914",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",
@@ -395,4 +439,4 @@
"title_field": "subject",
"track_changes": 1,
"track_seen": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 117267f..a23fe05 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -7,7 +7,7 @@
from frappe import _
from frappe import utils
from frappe.model.document import Document
-from frappe.utils import now, time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime
+from frappe.utils import now, time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime, time_diff_in_seconds, time_diff
from datetime import datetime, timedelta
from frappe.model.mapper import get_mapped_doc
from frappe.utils.user import is_website_user
@@ -47,8 +47,8 @@
self.contact = frappe.db.get_value("Contact", {"email_id": email_id})
if self.contact:
- contact = frappe.get_doc('Contact', self.contact)
- self.customer = contact.get_link_for('Customer')
+ contact = frappe.get_doc("Contact", self.contact)
+ self.customer = contact.get_link_for("Customer")
if not self.company:
self.company = frappe.db.get_value("Lead", self.lead, "company") or \
@@ -56,18 +56,70 @@
def update_status(self):
status = frappe.db.get_value("Issue", self.name, "status")
- if self.status!="Open" and status =="Open" and not self.first_responded_on:
+ if self.status != "Open" and status == "Open" and not self.first_responded_on:
self.first_responded_on = frappe.flags.current_time or now_datetime()
- if self.status=="Closed" and status !="Closed":
+ if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
self.resolution_date = frappe.flags.current_time or now_datetime()
if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing":
set_service_level_agreement_variance(issue=self.name)
self.update_agreement_status()
+ set_resolution_time(issue=self)
+ set_user_resolution_time(issue=self)
- if self.status=="Open" and status !="Open":
+ if self.status == "Open" and status != "Open":
# if no date, it should be set as None and not a blank string "", as per mysql strict config
self.resolution_date = None
+ self.reset_issue_metrics()
+ # enable SLA and variance on Reopen
+ self.agreement_fulfilled = "Ongoing"
+ set_service_level_agreement_variance(issue=self.name)
+
+ self.handle_hold_time(status)
+
+ def handle_hold_time(self, status):
+ if self.service_level_agreement:
+ # set response and resolution variance as None as the issue is on Hold for status as Replied
+ pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"],
+ filters={"parent": self.service_level_agreement})
+ hold_statuses = [entry.status for entry in pause_sla_on]
+ update_values = {}
+
+ if self.status in hold_statuses and status not in hold_statuses:
+ update_values['on_hold_since'] = frappe.flags.current_time or now_datetime()
+ if not self.first_responded_on:
+ update_values['response_by'] = None
+ update_values['response_by_variance'] = 0
+ update_values['resolution_by'] = None
+ update_values['resolution_by_variance'] = 0
+
+ # calculate hold time when status is changed from Replied to any other status
+ if self.status not in hold_statuses and status in hold_statuses:
+ hold_time = self.total_hold_time if self.total_hold_time else 0
+ now_time = frappe.flags.current_time or now_datetime()
+ update_values['total_hold_time'] = hold_time + time_diff_in_seconds(now_time, self.on_hold_since)
+
+ # re-calculate SLA variables after issue changes from Replied to Open
+ # add hold time to SLA variables
+ if self.status == "Open" and status in hold_statuses:
+ start_date_time = get_datetime(self.service_level_agreement_creation)
+ priority = get_priority(self)
+ now_time = frappe.flags.current_time or now_datetime()
+ hold_time = time_diff_in_seconds(now_time, self.on_hold_since)
+
+ if not self.first_responded_on:
+ response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
+ update_values['response_by'] = add_to_date(response_by, seconds=round(hold_time))
+ response_by_variance = round(time_diff_in_hours(self.response_by, now_time))
+ update_values['response_by_variance'] = response_by_variance + (hold_time // 3600)
+
+ resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
+ update_values['resolution_by'] = add_to_date(resolution_by, seconds=round(hold_time))
+ resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_time))
+ update_values['resolution_by_variance'] = resolution_by_variance + (hold_time // 3600)
+ update_values['on_hold_since'] = None
+
+ self.db_set(update_values)
def update_agreement_status(self):
if self.service_level_agreement and self.agreement_fulfilled == "Ongoing":
@@ -128,6 +180,7 @@
replicated_issue.response_by_variance = None
replicated_issue.resolution_by = None
replicated_issue.resolution_by_variance = None
+ replicated_issue.reset_issue_metrics()
frappe.get_doc(replicated_issue).insert()
@@ -137,7 +190,7 @@
communications = frappe.get_all("Communication",
filters={"reference_doctype": "Issue",
"reference_name": comm_to_split_from.reference_name,
- "creation": ('>=', comm_to_split_from.creation)})
+ "creation": (">=", comm_to_split_from.creation)})
for communication in communications:
doc = frappe.get_doc("Communication", communication.name)
@@ -173,20 +226,15 @@
self.service_level_agreement = service_level_agreement.name
self.priority = service_level_agreement.default_priority if not priority else priority
- service_level_agreement = frappe.get_doc("Service Level Agreement", service_level_agreement.name)
- priority = service_level_agreement.get_service_level_agreement_priority(self.priority)
- priority.update({
- "support_and_resolution": service_level_agreement.support_and_resolution,
- "holiday_list": service_level_agreement.holiday_list
- })
+ priority = get_priority(self)
if not self.creation:
self.creation = now_datetime()
self.service_level_agreement_creation = now_datetime()
start_date_time = get_datetime(self.service_level_agreement_creation)
- self.response_by = get_expected_time_for(parameter='response', service_level=priority, start_date_time=start_date_time)
- self.resolution_by = get_expected_time_for(parameter='resolution', service_level=priority, start_date_time=start_date_time)
+ self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
+ self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime()))
self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()))
@@ -221,36 +269,41 @@
self.agreement_fulfilled = "Ongoing"
self.save()
+ def reset_issue_metrics(self):
+ self.db_set("resolution_time", None)
+ self.db_set("user_resolution_time", None)
+
+
+def get_priority(issue):
+ service_level_agreement = frappe.get_doc("Service Level Agreement", issue.service_level_agreement)
+ priority = service_level_agreement.get_service_level_agreement_priority(issue.priority)
+ priority.update({
+ "support_and_resolution": service_level_agreement.support_and_resolution,
+ "holiday_list": service_level_agreement.holiday_list
+ })
+ return priority
+
+
def get_expected_time_for(parameter, service_level, start_date_time):
current_date_time = start_date_time
expected_time = current_date_time
start_time = None
end_time = None
- # lets assume response time is in days by default
- if parameter == 'response':
- allotted_days = service_level.get("response_time")
- time_period = service_level.get("response_time_period")
- elif parameter == 'resolution':
- allotted_days = service_level.get("resolution_time")
- time_period = service_level.get("resolution_time_period")
+ if parameter == "response":
+ allotted_seconds = service_level.get("response_time")
+ elif parameter == "resolution":
+ allotted_seconds = service_level.get("resolution_time")
else:
frappe.throw(_("{0} parameter is invalid").format(parameter))
- allotted_hours = 0
- if time_period == 'Hour':
- allotted_hours = allotted_days
- allotted_days = 0
- elif time_period == 'Week':
- allotted_days *= 7
-
- expected_time_is_set = 1 if allotted_days == 0 and time_period in ['Day', 'Week'] else 0
+ expected_time_is_set = 0
support_days = {}
for service in service_level.get("support_and_resolution"):
support_days[service.workday] = frappe._dict({
- 'start_time': service.start_time,
- 'end_time': service.end_time,
+ "start_time": service.start_time,
+ "end_time": service.end_time,
})
holidays = get_holidays(service_level.get("holiday_list"))
@@ -264,25 +317,22 @@
if getdate(current_date_time) == getdate(start_date_time) and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time \
else support_days[current_weekday].start_time
end_time = support_days[current_weekday].end_time
- time_left_today = time_diff_in_hours(end_time, start_time)
+ time_left_today = time_diff_in_seconds(end_time, start_time)
# no time left for support today
- if time_left_today < 0: pass
- elif time_period == 'Hour':
- if time_left_today >= allotted_hours:
+ if time_left_today <= 0: pass
+ elif allotted_seconds:
+ if time_left_today >= allotted_seconds:
expected_time = datetime.combine(getdate(current_date_time), get_time(start_time))
- expected_time = add_to_date(expected_time, hours=allotted_hours)
+ expected_time = add_to_date(expected_time, seconds=allotted_seconds)
expected_time_is_set = 1
else:
- allotted_hours = allotted_hours - time_left_today
- else:
- allotted_days -= 1
- expected_time_is_set = allotted_days <= 0
+ allotted_seconds = allotted_seconds - time_left_today
if not expected_time_is_set:
current_date_time = add_to_date(current_date_time, days=1)
- if end_time and time_period != 'Hour':
+ if end_time and allotted_seconds >= 86400:
current_date_time = datetime.combine(getdate(current_date_time), get_time(end_time))
else:
current_date_time = expected_time
@@ -311,6 +361,36 @@
if variance < 0:
frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
+
+def set_resolution_time(issue):
+ # total time taken from issue creation to closing
+ resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation)
+ issue.db_set("resolution_time", resolution_time)
+
+
+def set_user_resolution_time(issue):
+ # total time taken by a user to close the issue apart from wait_time
+ communications = frappe.get_list("Communication", filters={
+ "reference_doctype": issue.doctype,
+ "reference_name": issue.name
+ },
+ fields=["sent_or_received", "name", "creation"],
+ order_by="creation"
+ )
+
+ pending_time = []
+ for i in range(len(communications)):
+ if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent":
+ wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation)
+ if wait_time > 0:
+ pending_time.append(wait_time)
+
+ total_pending_time = sum(pending_time)
+ resolution_time_in_secs = time_diff_in_seconds(issue.resolution_date, issue.creation)
+ user_resolution_time = resolution_time_in_secs - total_pending_time
+ issue.db_set("user_resolution_time", user_resolution_time)
+
+
def get_list_context(context=None):
return {
"title": _("Issues"),
@@ -318,7 +398,7 @@
"row_template": "templates/includes/issue_row.html",
"show_sidebar": True,
"show_search": True,
- 'no_breadcrumbs': True
+ "no_breadcrumbs": True
}
@@ -326,12 +406,12 @@
from frappe.www.list import get_list
user = frappe.session.user
- contact = frappe.db.get_value('Contact', {'user': user}, 'name')
+ contact = frappe.db.get_value("Contact", {"user": user}, "name")
customer = None
if contact:
- contact_doc = frappe.get_doc('Contact', contact)
- customer = contact_doc.get_link_for('Customer')
+ contact_doc = frappe.get_doc("Contact", contact)
+ customer = contact_doc.get_link_for("Customer")
ignore_permissions = False
if is_website_user():
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index 7a5e3e3..a004843 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -10,10 +10,13 @@
from datetime import timedelta
class TestIssue(unittest.TestCase):
- def test_response_time_and_resolution_time_based_on_different_sla(self):
+ def setUp(self):
+ frappe.db.sql("delete from `tabService Level Agreement`")
+ frappe.db.sql("delete from `tabEmployee`")
frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
create_service_level_agreements_for_issues()
+ def test_response_time_and_resolution_time_based_on_different_sla(self):
creation = datetime.datetime(2019, 3, 4, 12, 0)
# make issue with customer specific SLA
@@ -72,8 +75,67 @@
self.assertEqual(issue.agreement_fulfilled, 'Fulfilled')
-def make_issue(creation=None, customer=None, index=0):
+ def test_issue_metrics(self):
+ creation = datetime.datetime(2020, 3, 4, 4, 0)
+ issue = make_issue(creation, index=1)
+ create_communication(issue.name, "test@example.com", "Received", creation)
+
+ creation = datetime.datetime(2020, 3, 4, 4, 15)
+ create_communication(issue.name, "test@admin.com", "Sent", creation)
+
+ creation = datetime.datetime(2020, 3, 4, 5, 0)
+ create_communication(issue.name, "test@example.com", "Received", creation)
+
+ creation = datetime.datetime(2020, 3, 4, 5, 5)
+ create_communication(issue.name, "test@admin.com", "Sent", creation)
+
+ frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5)
+ issue.reload()
+ issue.status = 'Closed'
+ issue.save()
+
+ self.assertEqual(issue.avg_response_time, 600)
+ self.assertEqual(issue.resolution_time, 3900)
+ self.assertEqual(issue.user_resolution_time, 1200)
+
+ def test_hold_time_on_replied(self):
+ creation = datetime.datetime(2020, 3, 4, 4, 0)
+
+ issue = make_issue(creation, index=1)
+ create_communication(issue.name, "test@example.com", "Received", creation)
+
+ creation = datetime.datetime(2020, 3, 4, 4, 15)
+ create_communication(issue.name, "test@admin.com", "Sent", creation)
+
+ frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15)
+ issue.reload()
+ issue.status = 'Replied'
+ issue.save()
+
+ self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
+
+ creation = datetime.datetime(2020, 3, 4, 5, 0)
+ frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 0)
+ create_communication(issue.name, "test@example.com", "Received", creation)
+
+ issue.reload()
+ self.assertEqual(issue.total_hold_time, 2700)
+ self.assertEqual(issue.resolution_by, datetime.datetime(2020, 3, 4, 16, 45))
+
+ creation = datetime.datetime(2020, 3, 4, 5, 5)
+ create_communication(issue.name, "test@admin.com", "Sent", creation)
+
+ frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5)
+ issue.reload()
+ issue.status = 'Closed'
+ issue.save()
+
+ issue.reload()
+ self.assertEqual(issue.total_hold_time, 2700)
+
+
+def make_issue(creation=None, customer=None, index=0):
issue = frappe.get_doc({
"doctype": "Issue",
"subject": "Service Level Agreement Issue {0}".format(index),
@@ -86,6 +148,7 @@
return issue
+
def create_customer(name, customer_group, territory):
create_customer_group(customer_group)
@@ -99,6 +162,7 @@
"territory": territory
}).insert(ignore_permissions=True)
+
def create_customer_group(customer_group):
if not frappe.db.exists("Customer Group", {"customer_group_name": customer_group}):
@@ -107,6 +171,7 @@
"customer_group_name": customer_group
}).insert(ignore_permissions=True)
+
def create_territory(territory):
if not frappe.db.exists("Territory", {"territory_name": territory}):
@@ -114,3 +179,21 @@
"doctype": "Territory",
"territory_name": territory,
}).insert(ignore_permissions=True)
+
+
+def create_communication(reference_name, sender, sent_or_received, creation):
+ issue = frappe.get_doc({
+ "doctype": "Communication",
+ "communication_type": "Communication",
+ "communication_medium": "Email",
+ "sent_or_received": sent_or_received,
+ "email_status": "Open",
+ "subject": "Test Issue",
+ "sender": sender,
+ "content": "Test",
+ "status": "Linked",
+ "reference_doctype": "Issue",
+ "creation": creation,
+ "reference_name": reference_name
+ })
+ issue.save()
diff --git a/erpnext/support/doctype/service_level/__init__.py b/erpnext/support/doctype/pause_sla_on_status/__init__.py
similarity index 100%
rename from erpnext/support/doctype/service_level/__init__.py
rename to erpnext/support/doctype/pause_sla_on_status/__init__.py
diff --git a/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json
new file mode 100644
index 0000000..5b03f25
--- /dev/null
+++ b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json
@@ -0,0 +1,33 @@
+{
+ "actions": [],
+ "creation": "2020-06-05 13:59:43.265588",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "status"
+ ],
+ "fields": [
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-06-05 15:15:29.986608",
+ "modified_by": "Administrator",
+ "module": "Support",
+ "name": "Pause SLA On Status",
+ "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/support/doctype/pause_sla_on_status/pause_sla_on_status.py b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py
new file mode 100644
index 0000000..a3b547e
--- /dev/null
+++ b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, 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 PauseSLAOnStatus(Document):
+ pass
diff --git a/erpnext/support/doctype/service_level/service_level.js b/erpnext/support/doctype/service_level/service_level.js
deleted file mode 100644
index abe254b..0000000
--- a/erpnext/support/doctype/service_level/service_level.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Service Level', {
-
-});
diff --git a/erpnext/support/doctype/service_level/service_level.json b/erpnext/support/doctype/service_level/service_level.json
deleted file mode 100644
index dced3aa..0000000
--- a/erpnext/support/doctype/service_level/service_level.json
+++ /dev/null
@@ -1,111 +0,0 @@
-{
- "autoname": "field:service_level",
- "creation": "2018-11-19 12:44:30.407502",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "service_level",
- "employee_group",
- "column_break_2",
- "holiday_list",
- "default_priority",
- "response_and_resoution_time",
- "priorities",
- "section_break_01",
- "support_and_resolution"
- ],
- "fields": [
- {
- "fieldname": "service_level",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Level",
- "reqd": 1,
- "unique": 1
- },
- {
- "fieldname": "column_break_2",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "holiday_list",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Holiday List (ignored during SLA calculation)",
- "options": "Holiday List",
- "reqd": 1
- },
- {
- "fieldname": "employee_group",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Employee Group",
- "options": "Employee Group"
- },
- {
- "fieldname": "response_and_resoution_time",
- "fieldtype": "Section Break",
- "label": "Response and Resoution Time"
- },
- {
- "fieldname": "section_break_01",
- "fieldtype": "Section Break",
- "label": "Support Hours"
- },
- {
- "fieldname": "support_and_resolution",
- "fieldtype": "Table",
- "label": "Support and Resolution",
- "options": "Service Day",
- "reqd": 1
- },
- {
- "fieldname": "priorities",
- "fieldtype": "Table",
- "label": "Priorities",
- "options": "Service Level Priority",
- "reqd": 1
- },
- {
- "fieldname": "default_priority",
- "fieldtype": "Link",
- "label": "Default Priority",
- "options": "Issue Priority",
- "read_only": 1
- }
- ],
- "modified": "2019-06-06 12:58:03.464056",
- "modified_by": "Administrator",
- "module": "Support",
- "name": "Service Level",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "All",
- "share": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC"
-}
\ No newline at end of file
diff --git a/erpnext/support/doctype/service_level/service_level.py b/erpnext/support/doctype/service_level/service_level.py
deleted file mode 100644
index 89fa25c..0000000
--- a/erpnext/support/doctype/service_level/service_level.py
+++ /dev/null
@@ -1,95 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, 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.model.document import Document
-from datetime import datetime
-from frappe.utils import get_weekdays
-
-class ServiceLevel(Document):
-
- def validate(self):
- self.check_priorities()
- self.check_support_and_resolution()
-
- def check_priorities(self):
- default_priority = []
- priorities = []
-
- for priority in self.priorities:
- # Check if response and resolution time is set for every priority
- if not (priority.response_time or priority.resolution_time):
- frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx))
-
- priorities.append(priority.priority)
-
- if priority.default_priority:
- default_priority.append(priority.default_priority)
-
- if priority.response_time_period == "Hour":
- response = priority.response_time * 0.0416667
- elif priority.response_time_period == "Day":
- response = priority.response_time
- elif priority.response_time_period == "Week":
- response = priority.response_time * 7
-
- if priority.resolution_time_period == "Hour":
- resolution = priority.resolution_time * 0.0416667
- elif priority.resolution_time_period == "Day":
- resolution = priority.resolution_time
- elif priority.resolution_time_period == "Week":
- resolution = priority.resolution_time * 7
-
- if response > resolution:
- frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx))
-
- # Check if repeated priority
- if not len(set(priorities)) == len(priorities):
- repeated_priority = get_repeated(priorities)
- frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority))
-
- # Check if repeated default priority
- if not len(set(default_priority)) == len(default_priority):
- frappe.throw(_("Select only one Priority as Default."))
-
- # set default priority from priorities
- try:
- self.default_priority = next(d.priority for d in self.priorities if d.default_priority)
- except Exception:
- frappe.throw(_("Select a Default Priority."))
-
- def check_support_and_resolution(self):
- week = get_weekdays()
- support_days = []
-
- for support_and_resolution in self.support_and_resolution:
- # Check if start and end time is set for every support day
- if not (support_and_resolution.start_time or support_and_resolution.end_time):
- frappe.throw(_("Set Start Time and End Time for \
- Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx)))
-
- support_days.append(support_and_resolution.workday)
- support_and_resolution.idx = week.index(support_and_resolution.workday) + 1
-
- if support_and_resolution.start_time >= support_and_resolution.end_time:
- frappe.throw(_("Start Time can't be greater than or equal to End Time \
- for {0}.".format(support_and_resolution.workday)))
-
- # Check for repeated workday
- if not len(set(support_days)) == len(support_days):
- repeated_days = get_repeated(support_days)
- frappe.throw(_("Workday {0} has been repeated.").format(repeated_days))
-
-def get_repeated(values):
- unique_list = []
- diff = []
- for value in values:
- if value not in unique_list:
- unique_list.append(str(value))
- else:
- if value not in diff:
- diff.append(str(value))
- return " ".join(diff)
diff --git a/erpnext/support/doctype/service_level/service_level_dashboard.py b/erpnext/support/doctype/service_level/service_level_dashboard.py
deleted file mode 100644
index 393095e..0000000
--- a/erpnext/support/doctype/service_level/service_level_dashboard.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from frappe import _
-
-def get_data():
- return {
- 'fieldname': 'service_level',
- 'transactions': [
- {
- 'label': _('Service Level Agreement'),
- 'items': ['Service Level Agreement']
- }
- ]
- }
\ No newline at end of file
diff --git a/erpnext/support/doctype/service_level/test_service_level.py b/erpnext/support/doctype/service_level/test_service_level.py
deleted file mode 100644
index 09577df..0000000
--- a/erpnext/support/doctype/service_level/test_service_level.py
+++ /dev/null
@@ -1,149 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-from __future__ import unicode_literals
-from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group
-from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities
-
-import frappe
-import unittest
-
-class TestServiceLevel(unittest.TestCase):
-
- def test_service_level(self):
- employee_group = make_employee_group()
- make_holiday_list()
- make_priorities()
-
- # Default Service Level
- test_make_service_level = create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6)
- get_make_service_level = get_service_level("__Test Service Level")
-
- self.assertEqual(test_make_service_level.name, get_make_service_level.name)
- self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list)
- self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group)
-
- # Service Level
- test_make_service_level = create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3)
- get_make_service_level = get_service_level("_Test Service Level")
-
- self.assertEqual(test_make_service_level.name, get_make_service_level.name)
- self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list)
- self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group)
-
-
-def create_service_level(service_level, holiday_list, employee_group, response_time, resolution_time):
- sl = frappe.get_doc({
- "doctype": "Service Level",
- "service_level": service_level,
- "holiday_list": holiday_list,
- "employee_group": employee_group,
- "priorities": [
- {
- "priority": "Low",
- "response_time": response_time,
- "response_time_period": "Hour",
- "resolution_time": resolution_time,
- "resolution_time_period": "Hour",
- },
- {
- "priority": "Medium",
- "response_time": response_time,
- "default_priority": 1,
- "response_time_period": "Hour",
- "resolution_time": resolution_time,
- "resolution_time_period": "Hour",
- },
- {
- "priority": "High",
- "response_time": response_time,
- "response_time_period": "Hour",
- "resolution_time": resolution_time,
- "resolution_time_period": "Hour",
- }
- ],
- "support_and_resolution": [
- {
- "workday": "Monday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Tuesday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Wednesday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Thursday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Friday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Saturday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Sunday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- }
- ]
- })
-
- sl_exists = frappe.db.exists("Service Level", {"service_level": service_level})
-
- if not sl_exists:
- sl.insert()
- return sl
- else:
- return frappe.get_doc("Service Level", {"service_level": service_level})
-
-def get_service_level(service_level):
- return frappe.get_doc("Service Level", service_level)
-
-def make_holiday_list():
- holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
- if not holiday_list:
- now = frappe.utils.now_datetime()
- holiday_list = frappe.get_doc({
- "doctype": "Holiday List",
- "holiday_list_name": "__Test Holiday List",
- "from_date": "2019-01-01",
- "to_date": "2019-12-31",
- "holidays": [
- {
- "description": "Test Holiday 1",
- "holiday_date": "2019-03-05"
- },
- {
- "description": "Test Holiday 2",
- "holiday_date": "2019-03-07"
- },
- {
- "description": "Test Holiday 3",
- "holiday_date": "2019-02-11"
- },
- ]
- }).insert()
-
-def create_service_level_for_sla():
- employee_group = make_employee_group()
- make_holiday_list()
- make_priorities()
-
- # Default Service Level
- create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6)
-
- # Service Level
- create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3)
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
index 1d486f4..5346195 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
@@ -2,28 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on('Service Level Agreement', {
- service_level: function(frm) {
- frm.fields_dict.support_and_resolution.grid.remove_all();
- frappe.call({
- "method": "frappe.client.get",
- args: {
- doctype: "Service Level",
- name: frm.doc.service_level
- },
- callback: function(data){
- let count = Math.max(data.message.priorities.length, data.message.support_and_resolution.length);
- let i = 0;
- while (i < count){
- if (data.message.priorities[i]) {
- frm.add_child("priorities", data.message.priorities[i]);
- }
- if (data.message.support_and_resolution[i]) {
- frm.add_child("support_and_resolution", data.message.support_and_resolution[i]);
- }
- i++;
- }
- frm.refresh();
- }
+ setup: function(frm) {
+ let allow_statuses = [];
+ const exclude_statuses = ['Open', 'Closed', 'Resolved'];
+
+ frappe.model.with_doctype('Issue', () => {
+ let statuses = frappe.meta.get_docfield('Issue', 'status', frm.doc.name).options;
+ statuses = statuses.split('\n');
+ allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status));
+ frappe.meta.get_docfield('Pause SLA On Status', 'status', frm.doc.name).options = [''].concat(allow_statuses);
});
- },
-});
+ }
+});
\ No newline at end of file
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
index 9a83ca7..939c199 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "format:SLA-{service_level}-{####}",
"creation": "2018-12-26 21:08:15.448812",
"doctype": "DocType",
@@ -6,12 +7,13 @@
"engine": "InnoDB",
"field_order": [
"enable",
+ "section_break_2",
"service_level",
+ "default_priority",
"default_service_level_agreement",
- "holiday_list",
"column_break_2",
"employee_group",
- "default_priority",
+ "holiday_list",
"entity_section",
"entity_type",
"column_break_10",
@@ -21,6 +23,8 @@
"active",
"column_break_7",
"end_date",
+ "section_break_18",
+ "pause_sla_on",
"response_and_resolution_time_section",
"priorities",
"support_and_resolution_section_break",
@@ -28,42 +32,31 @@
],
"fields": [
{
- "default": "0",
- "depends_on": "eval: !doc.customer;",
- "fieldname": "default_service_level_agreement",
- "fieldtype": "Check",
- "label": "Default Service Level Agreement"
- },
- {
"fieldname": "service_level",
- "fieldtype": "Link",
+ "fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Service Level",
- "options": "Service Level",
"reqd": 1
},
{
- "fetch_from": "service_level.holiday_list",
"fieldname": "holiday_list",
"fieldtype": "Link",
"label": "Holiday List",
"options": "Holiday List",
- "read_only": 1
+ "reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
- "fetch_from": "service_level.employee_group",
"fieldname": "employee_group",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee Group",
- "options": "Employee Group",
- "read_only": 1
+ "options": "Employee Group"
},
{
"fieldname": "agreement_details_section",
@@ -103,21 +96,15 @@
"fieldname": "support_and_resolution",
"fieldtype": "Table",
"label": "Support and Resolution",
- "options": "Service Day"
+ "options": "Service Day",
+ "reqd": 1
},
{
"fieldname": "priorities",
"fieldtype": "Table",
"label": "Priorities",
- "options": "Service Level Priority"
- },
- {
- "fetch_from": "service_level.default_priority",
- "fieldname": "default_priority",
- "fieldtype": "Link",
- "label": "Default Priority",
- "options": "Issue Priority",
- "read_only": 1
+ "options": "Service Level Priority",
+ "reqd": 1
},
{
"default": "1",
@@ -156,9 +143,38 @@
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "default_service_level_agreement",
+ "fieldtype": "Check",
+ "label": "Default Service Level Agreement"
+ },
+ {
+ "fieldname": "default_priority",
+ "fieldtype": "Link",
+ "label": "Default Priority",
+ "options": "Issue Priority",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_18",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "fieldname": "pause_sla_on",
+ "fieldtype": "Table",
+ "label": "Pause SLA On",
+ "options": "Pause SLA On Status"
}
],
- "modified": "2019-07-09 17:22:16.402939",
+ "links": [],
+ "modified": "2020-06-10 12:30:15.050785",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Agreement",
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index a399c58..530230e 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -6,11 +6,74 @@
import frappe
from frappe.model.document import Document
from frappe import _
-from frappe.utils import getdate
+from frappe.utils import getdate, get_weekdays
+from datetime import datetime
class ServiceLevelAgreement(Document):
def validate(self):
+ self.validate_doc()
+ self.check_priorities()
+ self.check_support_and_resolution()
+
+ def check_priorities(self):
+ default_priority = []
+ priorities = []
+
+ for priority in self.priorities:
+ # Check if response and resolution time is set for every priority
+ if not (priority.response_time or priority.resolution_time):
+ frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx))
+
+ priorities.append(priority.priority)
+
+ if priority.default_priority:
+ default_priority.append(priority.default_priority)
+
+ response = priority.response_time
+ resolution = priority.resolution_time
+
+ if response > resolution:
+ frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx))
+
+ # Check if repeated priority
+ if not len(set(priorities)) == len(priorities):
+ repeated_priority = get_repeated(priorities)
+ frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority))
+
+ # Check if repeated default priority
+ if not len(set(default_priority)) == len(default_priority):
+ frappe.throw(_("Select only one Priority as Default."))
+
+ # set default priority from priorities
+ try:
+ self.default_priority = next(d.priority for d in self.priorities if d.default_priority)
+ except Exception:
+ frappe.throw(_("Select a Default Priority."))
+
+ def check_support_and_resolution(self):
+ week = get_weekdays()
+ support_days = []
+
+ for support_and_resolution in self.support_and_resolution:
+ # Check if start and end time is set for every support day
+ if not (support_and_resolution.start_time or support_and_resolution.end_time):
+ frappe.throw(_("Set Start Time and End Time for \
+ Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx)))
+
+ support_days.append(support_and_resolution.workday)
+ support_and_resolution.idx = week.index(support_and_resolution.workday) + 1
+
+ if support_and_resolution.start_time >= support_and_resolution.end_time:
+ frappe.throw(_("Start Time can't be greater than or equal to End Time \
+ for {0}.".format(support_and_resolution.workday)))
+
+ # Check for repeated workday
+ if not len(set(support_days)) == len(support_days):
+ repeated_days = get_repeated(support_days)
+ frappe.throw(_("Workday {0} has been repeated.").format(repeated_days))
+
+ def validate_doc(self):
if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
frappe.throw(_("Service Level Agreement tracking is not enabled."))
@@ -35,9 +98,7 @@
return frappe._dict({
"priority": priority.priority,
"response_time": priority.response_time,
- "response_time_period": priority.response_time_period,
- "resolution_time": priority.resolution_time,
- "resolution_time_period": priority.resolution_time_period
+ "resolution_time": priority.resolution_time
})
def check_agreement_status():
@@ -110,4 +171,15 @@
return {
"priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])],
"service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)]
- }
\ No newline at end of file
+ }
+
+def get_repeated(values):
+ unique_list = []
+ diff = []
+ for value in values:
+ if value not in unique_list:
+ unique_list.append(str(value))
+ else:
+ if value not in diff:
+ diff.append(str(value))
+ return " ".join(diff)
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index 4a741ea..0746a9c 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -5,19 +5,20 @@
import frappe
import unittest
-from erpnext.support.doctype.service_level.test_service_level import create_service_level_for_sla
+from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group
+from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities
class TestServiceLevelAgreement(unittest.TestCase):
-
- def test_service_level_agreement(self):
+ def setUp(self):
+ frappe.db.sql("delete from `tabService Level Agreement`")
frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
- create_service_level_for_sla()
-
+ def test_service_level_agreement(self):
# Default Service Level Agreement
create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1,
- service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type=None, entity=None, response_time=4, resolution_time=6)
+ holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
+ entity_type=None, entity=None, response_time=14400, resolution_time=21600)
+
get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1)
self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name)
@@ -28,8 +29,8 @@
# Service Level Agreement for Customer
customer = create_customer()
create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
- service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type="Customer", entity=customer, response_time=2, resolution_time=3)
+ holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
+ entity_type="Customer", entity=customer, response_time=7200, resolution_time=10800)
get_customer_service_level_agreement = get_service_level_agreement(entity_type="Customer", entity=customer)
self.assertEqual(create_customer_service_level_agreement.name, get_customer_service_level_agreement.name)
@@ -40,8 +41,8 @@
# Service Level Agreement for Customer Group
customer_group = create_customer_group()
create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
- service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type="Customer Group", entity=customer_group, response_time=2, resolution_time=3)
+ holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
+ entity_type="Customer Group", entity=customer_group, response_time=7200, resolution_time=10800)
get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group)
self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name)
@@ -52,8 +53,8 @@
# Service Level Agreement for Territory
territory = create_territory()
create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
- service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type="Territory", entity=territory, response_time=2, resolution_time=3)
+ holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
+ entity_type="Territory", entity=territory, response_time=7200, resolution_time=10800)
get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory)
self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name)
@@ -71,14 +72,19 @@
service_level_agreement = frappe.get_doc("Service Level Agreement", filters)
return service_level_agreement
-def create_service_level_agreement(default_service_level_agreement, service_level, holiday_list, employee_group,
+def create_service_level_agreement(default_service_level_agreement, holiday_list, employee_group,
response_time, entity_type, entity, resolution_time):
+ employee_group = make_employee_group()
+ make_holiday_list()
+ make_priorities()
+
service_level_agreement = frappe.get_doc({
"doctype": "Service Level Agreement",
"enable": 1,
+ "service_level": "__Test Service Level",
"default_service_level_agreement": default_service_level_agreement,
- "service_level": service_level,
+ "default_priority": "Medium",
"holiday_list": holiday_list,
"employee_group": employee_group,
"entity_type": entity_type,
@@ -109,6 +115,11 @@
"resolution_time_period": "Hour",
}
],
+ "pause_sla_on": [
+ {
+ "status": "Replied"
+ }
+ ],
"support_and_resolution": [
{
"workday": "Monday",
@@ -167,6 +178,7 @@
else:
return frappe.get_doc("Service Level Agreement", service_level_agreement_exists)
+
def create_customer():
customer = frappe.get_doc({
"doctype": "Customer",
@@ -206,23 +218,42 @@
return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"})
def create_service_level_agreements_for_issues():
- create_service_level_for_sla()
-
- create_service_level_agreement(default_service_level_agreement=1,
- service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type=None, entity=None, response_time=4, resolution_time=6)
+ create_service_level_agreement(default_service_level_agreement=1, holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group", entity_type=None, entity=None, response_time=14400, resolution_time=21600)
create_customer()
- create_service_level_agreement(default_service_level_agreement=0,
- service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type="Customer", entity="_Test Customer", response_time=2, resolution_time=3)
+ create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group", entity_type="Customer", entity="_Test Customer", response_time=7200, resolution_time=10800)
create_customer_group()
- create_service_level_agreement(default_service_level_agreement=0,
- service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=2, resolution_time=3)
+ create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group", entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=7200, resolution_time=10800)
create_territory()
- create_service_level_agreement(default_service_level_agreement=0,
- service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type="Territory", entity="_Test SLA Territory", response_time=2, resolution_time=3)
+ create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800)
+
+def make_holiday_list():
+ holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
+ if not holiday_list:
+ now = frappe.utils.now_datetime()
+ holiday_list = frappe.get_doc({
+ "doctype": "Holiday List",
+ "holiday_list_name": "__Test Holiday List",
+ "from_date": "2019-01-01",
+ "to_date": "2019-12-31",
+ "holidays": [
+ {
+ "description": "Test Holiday 1",
+ "holiday_date": "2019-03-05"
+ },
+ {
+ "description": "Test Holiday 2",
+ "holiday_date": "2019-03-07"
+ },
+ {
+ "description": "Test Holiday 3",
+ "holiday_date": "2019-02-11"
+ },
+ ]
+ }).insert()
diff --git a/erpnext/support/doctype/service_level_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json
index cd87a1c..65d5169 100644
--- a/erpnext/support/doctype/service_level_priority/service_level_priority.json
+++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-05-04 05:54:03.658991",
"doctype": "DocType",
"editable_grid": 1,
@@ -9,10 +10,8 @@
"default_priority",
"sb_00",
"response_time",
- "response_time_period",
"cb_00",
- "resolution_time",
- "resolution_time_period"
+ "resolution_time"
],
"fields": [
{
@@ -28,16 +27,11 @@
"fieldtype": "Section Break"
},
{
- "columns": 1,
- "fieldname": "response_time",
- "fieldtype": "Int",
- "in_list_view": 1,
- "label": "Response Time"
- },
- {
- "columns": 1,
+ "columns": 2,
"fieldname": "resolution_time",
- "fieldtype": "Int",
+ "fieldtype": "Duration",
+ "hide_days": 1,
+ "hide_seconds": 1,
"in_list_view": 1,
"label": "Resolution Time"
},
@@ -46,35 +40,30 @@
"fieldtype": "Column Break"
},
{
- "columns": 2,
- "fieldname": "response_time_period",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Response Time Period",
- "options": "Hour\nDay\nWeek"
- },
- {
- "columns": 2,
- "fieldname": "resolution_time_period",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Resolution Time Period",
- "options": "Hour\nDay\nWeek"
- },
- {
"fieldname": "cb_01",
"fieldtype": "Column Break"
},
{
+ "columns": 1,
"default": "0",
"fieldname": "default_priority",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Default Priority"
+ },
+ {
+ "columns": 2,
+ "fieldname": "response_time",
+ "fieldtype": "Duration",
+ "hide_days": 1,
+ "hide_seconds": 1,
+ "in_list_view": 1,
+ "label": "First Response Time"
}
],
"istable": 1,
- "modified": "2019-05-21 06:54:42.674377",
+ "links": [],
+ "modified": "2020-06-10 12:45:47.545915",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Priority",
@@ -84,4 +73,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/support/doctype/support_settings/support_settings.js b/erpnext/support/doctype/support_settings/support_settings.js
index 1d1069d..78adca8 100644
--- a/erpnext/support/doctype/support_settings/support_settings.js
+++ b/erpnext/support/doctype/support_settings/support_settings.js
@@ -3,6 +3,6 @@
frappe.ui.form.on('Support Settings', {
refresh: function(frm) {
-
+ //
}
});
diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json
index be9e064..1c1b0c3 100644
--- a/erpnext/support/doctype/support_settings/support_settings.json
+++ b/erpnext/support/doctype/support_settings/support_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2017-02-17 13:07:35.686409",
"doctype": "DocType",
"editable_grid": 1,
@@ -122,13 +123,15 @@
},
{
"default": "0",
+ "depends_on": "eval:doc.track_service_level_agreement;",
"fieldname": "allow_resetting_service_level_agreement",
"fieldtype": "Check",
"label": "Allow Resetting Service Level Agreement"
}
],
"issingle": 1,
- "modified": "2019-07-10 22:52:39.663873",
+ "links": [],
+ "modified": "2020-06-05 17:56:17.491684",
"modified_by": "Administrator",
"module": "Support",
"name": "Support Settings",