From d881dc47840234b37efd53ee3c45cef310115505 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 24 Apr 2020 14:33:34 +0530 Subject: [PATCH 01/36] feat: metrics for Issue --- erpnext/support/doctype/issue/issue.json | 33 ++++++++++++++-- erpnext/support/doctype/issue/issue.py | 49 +++++++++++++++++++++++- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index c12cef4a5f..79729f2902 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -34,6 +34,8 @@ "response", "mins_to_first_response", "first_responded_on", + "column_break_26", + "avg_response_time", "additional_info", "lead", "contact", @@ -50,7 +52,9 @@ "resolution_date", "content_type", "attachment", - "via_customer_portal" + "via_customer_portal", + "operational_time", + "user_operational_time" ], "fields": [ { @@ -362,12 +366,35 @@ "label": "Issue Split From", "options": "Issue", "read_only": 1 + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "fieldname": "avg_response_time", + "fieldtype": "Time", + "label": "Average Response Time", + "read_only": 1 + }, + { + "fieldname": "operational_time", + "fieldtype": "Time", + "label": "Operational Time", + "read_only": 1 + }, + { + "fieldname": "user_operational_time", + "fieldtype": "Time", + "label": "User Operational Time", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-03-13 02:19:49.477928", + "modified": "2020-04-24 09:58:13.499635", "modified_by": "Administrator", "module": "Support", "name": "Issue", @@ -395,4 +422,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 117267f1a4..62b87ff552 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -7,7 +7,7 @@ import json 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 @@ -63,6 +63,9 @@ class Issue(Document): 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) + set_average_response_time(issue=self) + set_operational_time(issue=self) + set_user_operational_time(issue=self) self.update_agreement_status() if self.status=="Open" and status !="Open": @@ -311,6 +314,50 @@ def set_service_level_agreement_variance(issue=None): if variance < 0: frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) +def set_average_response_time(issue): + communications = frappe.get_list("Communication", filters={ + "reference_doctype": issue.doctype, + "reference_name": issue.name + }, + fields=["sent_or_received", "name", "creation"], + order_by="creation" + ) + + response_times = [] + for i in range(len(communications)-1): + if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received": + response_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) + if response_time > 0: + response_times.append(response_time) + avg_response_time = sum(response_times) / len(response_times) + avg_response_time = str(timedelta(seconds=avg_response_time)).split(".")[0] + issue.db_set('avg_response_time', avg_response_time) + +def set_operational_time(issue): + operational_time = time_diff(now_datetime(), issue.creation) + issue.db_set('operational_time', str(operational_time).split(".")[0]) + +def set_user_operational_time(issue): + 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)-1): + 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 = timedelta(seconds=sum(pending_time)) + operational_time = frappe.db.get_value('Issue', issue.name, 'operational_time') + user_operational_time = time_diff(operational_time, total_pending_time) + issue.db_set('user_operational_time', str(user_operational_time)) + + def get_list_context(context=None): return { "title": _("Issues"), From 2c3f1677f8f7f2cd19bd97a3460492366f201292 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 28 Apr 2020 14:11:58 +0530 Subject: [PATCH 02/36] fix: commonify SLA and Service Level Doctypes --- .../service_level_agreement.js | 25 +----- .../service_level_agreement.json | 44 ++++----- .../service_level_agreement.py | 89 ++++++++++++++++++- .../service_level_priority.json | 23 ++--- .../support_settings/support_settings.json | 4 +- 5 files changed, 122 insertions(+), 63 deletions(-) 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 1d486f4834..7aeaae48ba 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,5 @@ // 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(); - } - }); - }, + }); 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 9a83ca7ac0..3725e15a54 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,12 @@ "engine": "InnoDB", "field_order": [ "enable", + "section_break_2", "service_level", "default_service_level_agreement", - "holiday_list", "column_break_2", "employee_group", - "default_priority", + "holiday_list", "entity_section", "entity_type", "column_break_10", @@ -27,43 +28,31 @@ "support_and_resolution" ], "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 + "options": "Holiday List" }, { "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", @@ -111,14 +100,6 @@ "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 - }, { "default": "1", "fieldname": "active", @@ -156,9 +137,20 @@ "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" } ], - "modified": "2019-07-09 17:22:16.402939", + "links": [], + "modified": "2020-04-28 14:10:18.767202", "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 a399c58b16..9fa0e238f9 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,85 @@ from __future__ import unicode_literals 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) + + 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 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.")) @@ -110,4 +184,15 @@ def get_service_level_agreement_filters(name, customer=None): 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_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json index cd87a1c113..f56008eb4c 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, @@ -16,7 +17,7 @@ ], "fields": [ { - "columns": 2, + "columns": 1, "fieldname": "priority", "fieldtype": "Link", "in_list_view": 1, @@ -28,14 +29,7 @@ "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", "in_list_view": 1, @@ -66,15 +60,24 @@ "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": "Int", + "in_list_view": 1, + "label": "First Response Time" } ], "istable": 1, - "modified": "2019-05-21 06:54:42.674377", + "links": [], + "modified": "2020-04-24 14:50:13.774308", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json index be9e064591..3f52181a46 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, @@ -128,7 +129,8 @@ } ], "issingle": 1, - "modified": "2019-07-10 22:52:39.663873", + "links": [], + "modified": "2020-04-28 14:11:15.117019", "modified_by": "Administrator", "module": "Support", "name": "Support Settings", From 0e3a16e2670eb68bfca31d702d7ce38d348902bd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 29 Apr 2020 00:13:33 +0530 Subject: [PATCH 03/36] fix: set metrics duration --- erpnext/support/doctype/issue/issue.json | 18 +++---- erpnext/support/doctype/issue/issue.py | 61 +++++++++++++++++------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 79729f2902..dfe0647cfa 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -53,7 +53,7 @@ "content_type", "attachment", "via_customer_portal", - "operational_time", + "resolution_time", "user_operational_time" ], "fields": [ @@ -374,27 +374,27 @@ { "bold": 1, "fieldname": "avg_response_time", - "fieldtype": "Time", + "fieldtype": "Data", "label": "Average Response Time", "read_only": 1 }, { - "fieldname": "operational_time", - "fieldtype": "Time", - "label": "Operational Time", + "fieldname": "user_operational_time", + "fieldtype": "Data", + "label": "User Operational Time", "read_only": 1 }, { - "fieldname": "user_operational_time", - "fieldtype": "Time", - "label": "User Operational Time", + "fieldname": "resolution_time", + "fieldtype": "Data", + "label": "Resolution Time", "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-04-24 09:58:13.499635", + "modified": "2020-04-28 23:42:28.576580", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 62b87ff552..027225942c 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -59,12 +59,12 @@ class Issue(Document): 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=="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) set_average_response_time(issue=self) - set_operational_time(issue=self) + set_resolution_time(issue=self) set_user_operational_time(issue=self) self.update_agreement_status() @@ -323,19 +323,25 @@ def set_average_response_time(issue): order_by="creation" ) - response_times = [] - for i in range(len(communications)-1): - if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received": - response_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) - if response_time > 0: - response_times.append(response_time) - avg_response_time = sum(response_times) / len(response_times) - avg_response_time = str(timedelta(seconds=avg_response_time)).split(".")[0] - issue.db_set('avg_response_time', avg_response_time) + if len(communications): + response_times = [] + for i in range(len(communications)-1): + if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received": + response_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) + if response_time > 0: + response_times.append(response_time) + + avg_response_time = sum(response_times) / len(response_times) + avg_response_time = timedelta(seconds=avg_response_time) + duration = get_duration(avg_response_time) + issue.db_set('avg_response_time', duration) + + +def set_resolution_time(issue): + resolution_time = time_diff(now_datetime(), issue.creation) + duration = get_duration(resolution_time) + issue.db_set('resolution_time', duration) -def set_operational_time(issue): - operational_time = time_diff(now_datetime(), issue.creation) - issue.db_set('operational_time', str(operational_time).split(".")[0]) def set_user_operational_time(issue): communications = frappe.get_list("Communication", filters={ @@ -352,10 +358,31 @@ def set_user_operational_time(issue): 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 = timedelta(seconds=sum(pending_time)) - operational_time = frappe.db.get_value('Issue', issue.name, 'operational_time') - user_operational_time = time_diff(operational_time, total_pending_time) - issue.db_set('user_operational_time', str(user_operational_time)) + resolution_time_in_secs = time_diff_in_seconds(now_datetime(), issue.creation) + resolution_time = timedelta(seconds=resolution_time_in_secs) + user_operational_time = resolution_time - total_pending_time + duration = get_duration(user_operational_time) + issue.db_set('user_operational_time', duration) + + +def get_duration(time): + days = time.days + seconds = time.seconds + hours = time.seconds // 3600 + mins = (time.seconds // 60) % 60 + duration = "" + if days: + duration += str(days) + " day" + duration += "s " if days > 1 else " " + if hours: + duration += str(hours) + " hour" + duration += "s " if hours > 1 else " " + if mins: + duration += str(mins) + " min" + duration += "s" if mins > 1 else "" + return duration def get_list_context(context=None): From ec24afb283d2af641266dd250f70479f0b8eb43b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 5 May 2020 10:02:14 +0530 Subject: [PATCH 04/36] fix: change response and resolution fieldtype to Duration in SLA --- .../service_level_agreement.py | 19 +++----------- .../service_level_priority.json | 26 +++---------------- 2 files changed, 7 insertions(+), 38 deletions(-) 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 9fa0e238f9..530230e1e8 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -30,19 +30,8 @@ class ServiceLevelAgreement(Document): 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 + 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)) @@ -109,9 +98,7 @@ class ServiceLevelAgreement(Document): 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(): 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 f56008eb4c..6377d1a962 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -10,10 +10,8 @@ "default_priority", "sb_00", "response_time", - "response_time_period", "cb_00", - "resolution_time", - "resolution_time_period" + "resolution_time" ], "fields": [ { @@ -31,7 +29,7 @@ { "columns": 2, "fieldname": "resolution_time", - "fieldtype": "Int", + "fieldtype": "Duration", "in_list_view": 1, "label": "Resolution Time" }, @@ -39,22 +37,6 @@ "fieldname": "cb_00", "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" @@ -70,14 +52,14 @@ { "columns": 2, "fieldname": "response_time", - "fieldtype": "Int", + "fieldtype": "Duration", "in_list_view": 1, "label": "First Response Time" } ], "istable": 1, "links": [], - "modified": "2020-04-24 14:50:13.774308", + "modified": "2020-05-04 22:08:04.503949", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", From 35d853a476b689443b44b0c1fee11609a7ea86d9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 5 May 2020 18:54:50 +0530 Subject: [PATCH 05/36] fix: SLA in issues with Duration Fieldtype --- erpnext/support/doctype/issue/issue.py | 35 ++++++++------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 027225942c..1489f43138 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -59,7 +59,7 @@ class Issue(Document): 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": + if self.status=="Closed" and status !="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) @@ -230,24 +230,14 @@ def get_expected_time_for(parameter, service_level, start_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") + allotted_seconds = service_level.get("response_time") elif parameter == 'resolution': - allotted_days = service_level.get("resolution_time") - time_period = service_level.get("resolution_time_period") + 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"): @@ -267,25 +257,22 @@ def get_expected_time_for(parameter, service_level, start_date_time): 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 From a2a1e257ae92fce590a208e26a970bfc6fe604ef Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 6 May 2020 11:09:33 +0530 Subject: [PATCH 06/36] fix: set Issue metrics using Duration fieldtype --- erpnext/support/doctype/issue/issue.py | 38 ++++++-------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 1489f43138..df0a2f662d 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -302,6 +302,7 @@ def set_service_level_agreement_variance(issue=None): frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) def set_average_response_time(issue): + # avg response time for all the responses communications = frappe.get_list("Communication", filters={ "reference_doctype": issue.doctype, "reference_name": issue.name @@ -319,18 +320,17 @@ def set_average_response_time(issue): response_times.append(response_time) avg_response_time = sum(response_times) / len(response_times) - avg_response_time = timedelta(seconds=avg_response_time) - duration = get_duration(avg_response_time) - issue.db_set('avg_response_time', duration) + issue.db_set('avg_response_time', avg_response_time) def set_resolution_time(issue): - resolution_time = time_diff(now_datetime(), issue.creation) - duration = get_duration(resolution_time) - issue.db_set('resolution_time', duration) + # total time taken from issue creation to closing + resolution_time = time_diff_in_seconds(now_datetime(), issue.creation) + issue.db_set('resolution_time', resolution_time) def set_user_operational_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 @@ -346,30 +346,10 @@ def set_user_operational_time(issue): if wait_time > 0: pending_time.append(wait_time) - total_pending_time = timedelta(seconds=sum(pending_time)) + total_pending_time = sum(pending_time) resolution_time_in_secs = time_diff_in_seconds(now_datetime(), issue.creation) - resolution_time = timedelta(seconds=resolution_time_in_secs) - user_operational_time = resolution_time - total_pending_time - duration = get_duration(user_operational_time) - issue.db_set('user_operational_time', duration) - - -def get_duration(time): - days = time.days - seconds = time.seconds - hours = time.seconds // 3600 - mins = (time.seconds // 60) % 60 - duration = "" - if days: - duration += str(days) + " day" - duration += "s " if days > 1 else " " - if hours: - duration += str(hours) + " hour" - duration += "s " if hours > 1 else " " - if mins: - duration += str(mins) + " min" - duration += "s" if mins > 1 else "" - return duration + user_operational_time = resolution_time_in_secs - total_pending_time + issue.db_set('user_operational_time', user_operational_time) def get_list_context(context=None): From fc4c795661d17a682b4abe97fa194fd501ec196f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 6 May 2020 11:42:43 +0530 Subject: [PATCH 07/36] fix: reset issue metrics on Reopen and Split --- erpnext/support/doctype/issue/issue.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index df0a2f662d..f2ee75498e 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -71,6 +71,7 @@ class Issue(Document): 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() def update_agreement_status(self): if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": @@ -131,6 +132,7 @@ class Issue(Document): 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() @@ -224,6 +226,12 @@ class Issue(Document): self.agreement_fulfilled = "Ongoing" self.save() + def reset_issue_metrics(self): + self.db_set('resolution_time', 0) + self.db_set('user_operational_time', 0) + self.db_set('avg_response_time',0) + + def get_expected_time_for(parameter, service_level, start_date_time): current_date_time = start_date_time expected_time = current_date_time From f77d7243dc6ef71fcfbf29f20464581cd7ac96b3 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 6 May 2020 11:47:52 +0530 Subject: [PATCH 08/36] fix: remove Service Level DocType --- .../support/doctype/service_level/__init__.py | 0 .../doctype/service_level/service_level.js | 6 - .../doctype/service_level/service_level.json | 111 ------------- .../doctype/service_level/service_level.py | 95 ----------- .../service_level/service_level_dashboard.py | 12 -- .../service_level/test_service_level.py | 149 ------------------ .../service_level_agreement.json | 11 +- 7 files changed, 7 insertions(+), 377 deletions(-) delete mode 100644 erpnext/support/doctype/service_level/__init__.py delete mode 100644 erpnext/support/doctype/service_level/service_level.js delete mode 100644 erpnext/support/doctype/service_level/service_level.json delete mode 100644 erpnext/support/doctype/service_level/service_level.py delete mode 100644 erpnext/support/doctype/service_level/service_level_dashboard.py delete mode 100644 erpnext/support/doctype/service_level/test_service_level.py diff --git a/erpnext/support/doctype/service_level/__init__.py b/erpnext/support/doctype/service_level/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 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 abe254bd03..0000000000 --- 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 dced3aa9e9..0000000000 --- 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 89fa25c233..0000000000 --- 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 393095e117..0000000000 --- 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 09577df166..0000000000 --- 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.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 3725e15a54..2d33c3e033 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -40,7 +40,8 @@ "fieldname": "holiday_list", "fieldtype": "Link", "label": "Holiday List", - "options": "Holiday List" + "options": "Holiday List", + "reqd": 1 }, { "fieldname": "column_break_2", @@ -92,13 +93,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" + "options": "Service Level Priority", + "reqd": 1 }, { "default": "1", @@ -150,7 +153,7 @@ } ], "links": [], - "modified": "2020-04-28 14:10:18.767202", + "modified": "2020-05-06 11:46:38.834810", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", From f2d36364f5ce821ca172a2c76c711ee6c5b47746 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 6 May 2020 12:32:55 +0530 Subject: [PATCH 09/36] fix: handle issue metrics on Reopen and Close --- erpnext/support/doctype/issue/issue.json | 30 ++++++++++++++---------- erpnext/support/doctype/issue/issue.py | 18 +++++++------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index dfe0647cfa..131e1cb9bf 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -54,7 +54,7 @@ "attachment", "via_customer_portal", "resolution_time", - "user_operational_time" + "user_resolution_time" ], "fields": [ { @@ -374,27 +374,33 @@ { "bold": 1, "fieldname": "avg_response_time", - "fieldtype": "Data", + "fieldtype": "Duration", "label": "Average Response Time", - "read_only": 1 - }, - { - "fieldname": "user_operational_time", - "fieldtype": "Data", - "label": "User Operational Time", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "resolution_time", - "fieldtype": "Data", + "fieldtype": "Duration", "label": "Resolution Time", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "user_resolution_time", + "fieldtype": "Duration", + "label": "User Resolution Time", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-04-28 23:42:28.576580", + "modified": "2020-05-06 12:28:58.093654", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index f2ee75498e..4fb2d8a2b9 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -63,10 +63,10 @@ class Issue(Document): 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) - set_average_response_time(issue=self) - set_resolution_time(issue=self) - set_user_operational_time(issue=self) self.update_agreement_status() + set_average_response_time(issue=self) + set_resolution_time(issue=self) + set_user_resolution_time(issue=self) 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 @@ -227,9 +227,9 @@ class Issue(Document): self.save() def reset_issue_metrics(self): - self.db_set('resolution_time', 0) - self.db_set('user_operational_time', 0) - self.db_set('avg_response_time',0) + self.db_set('resolution_time', None) + self.db_set('user_resolution_time', None) + self.db_set('avg_response_time', None) def get_expected_time_for(parameter, service_level, start_date_time): @@ -337,7 +337,7 @@ def set_resolution_time(issue): issue.db_set('resolution_time', resolution_time) -def set_user_operational_time(issue): +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, @@ -356,8 +356,8 @@ def set_user_operational_time(issue): total_pending_time = sum(pending_time) resolution_time_in_secs = time_diff_in_seconds(now_datetime(), issue.creation) - user_operational_time = resolution_time_in_secs - total_pending_time - issue.db_set('user_operational_time', user_operational_time) + 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): From 3d891f8e8937e6233055ce89f176e35e9b818dcf Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 6 May 2020 13:00:33 +0530 Subject: [PATCH 10/36] fix: set SLA as Ongoing on Issue Reopen --- erpnext/support/doctype/issue/issue.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 4fb2d8a2b9..2d9392c572 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -72,6 +72,7 @@ class Issue(Document): # 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() + self.agreement_fulfilled = "Ongoing" def update_agreement_status(self): if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": From 8993d38dafd8515eff56ed026e9cfa5ff0e80ee1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 6 May 2020 13:37:10 +0530 Subject: [PATCH 11/36] fix: set avg response time only when there are responses --- erpnext/support/doctype/issue/issue.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 2d9392c572..a9c4897017 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -322,14 +322,14 @@ def set_average_response_time(issue): if len(communications): response_times = [] - for i in range(len(communications)-1): + for i in range(len(communications)): if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received": response_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) if response_time > 0: response_times.append(response_time) - - avg_response_time = sum(response_times) / len(response_times) - issue.db_set('avg_response_time', avg_response_time) + if response_times: + avg_response_time = sum(response_times) / len(response_times) + issue.db_set('avg_response_time', avg_response_time) def set_resolution_time(issue): @@ -349,7 +349,7 @@ def set_user_resolution_time(issue): ) pending_time = [] - for i in range(len(communications)-1): + 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: From 372aa44b1953850fb493c2941fa45ba20ec85cf5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 25 May 2020 21:37:58 +0530 Subject: [PATCH 12/36] feat: implement hold time on Replied status for SLA --- erpnext/support/doctype/issue/issue.js | 14 +++++++- erpnext/support/doctype/issue/issue.json | 20 +++++++++++- erpnext/support/doctype/issue/issue.py | 41 +++++++++++++++++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index bad40cc37f..66c62d1ff2 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -41,7 +41,19 @@ frappe.ui.form.on("Issue", { if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") { if (frm.doc.service_level_agreement) { - set_time_to_resolve_and_response(frm); + if (frm.doc.status == "Replied") { + frm.dashboard.clear_headline(); + let message = {"indicator": "orange", "msg": __("Replied {0}", [moment(frm.doc.on_hold_since).fromNow()])}; + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } else { + set_time_to_resolve_and_response(frm); + } } frm.add_custom_button(__("Close"), function () { diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 131e1cb9bf..82443a51cb 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -31,6 +31,8 @@ "resolution_by", "resolution_by_variance", "service_level_agreement_creation", + "on_hold_since", + "total_hold_time", "response", "mins_to_first_response", "first_responded_on", @@ -395,12 +397,28 @@ "read_only": 1, "show_days": 1, "show_seconds": 1 + }, + { + "fieldname": "on_hold_since", + "fieldtype": "Datetime", + "label": "On Hold Since", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "total_hold_time", + "fieldtype": "Duration", + "label": "Total Hold Time", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-05-06 12:28:58.093654", + "modified": "2020-05-14 23:59:01.172007", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index a9c4897017..47b9c05161 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -72,7 +72,36 @@ class Issue(Document): # 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() - self.agreement_fulfilled = "Ongoing" + + if self.status == "Replied" and status != "Replied": + self.on_hold_since = frappe.flags.current_time or now_datetime() + if not self.first_responded_on: + self.response_by = None + self.response_by_variance = None + self.resolution_by = None + self.resolution_by_variance = None + + if self.status != "Replied" and status == "Replied": + hold_time = self.total_hold_time if self.total_hold_time else 0 + self.total_hold_time = hold_time + time_diff_in_seconds(now_datetime(), self.on_hold_since) + + if self.status == "Open" and status == "Replied": + start_date_time = get_datetime(self.service_level_agreement_creation) + priority = get_priority(self) + hold_time = time_diff_in_seconds(now_datetime(), 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) + self.response_by = add_to_date(response_by, seconds=round(hold_time)) + response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime())) + self.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) + self.resolution_by = add_to_date(resolution_by, seconds=round(hold_time)) + resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime())) + self.resolution_by_variance = resolution_by_variance + (hold_time // 3600) + self.on_hold_since = None + def update_agreement_status(self): if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": @@ -233,6 +262,16 @@ class Issue(Document): self.db_set('avg_response_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 From 79ed5755be8f4d393a2d4045165d42858bede911 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 25 May 2020 22:04:04 +0530 Subject: [PATCH 13/36] refactor: remove Hold status --- erpnext/support/doctype/issue/issue.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 82443a51cb..2b79e03f45 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -120,7 +120,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nReplied\nHold\nClosed", + "options": "Open\nReplied\nClosed", "search_index": 1 }, { @@ -167,6 +167,7 @@ "options": "Service Level Agreement" }, { + "depends_on": "eval: doc.status != 'Replied';", "fieldname": "response_by", "fieldtype": "Datetime", "label": "Response By", @@ -180,6 +181,7 @@ "read_only": 1 }, { + "depends_on": "eval: doc.status != 'Replied';", "fieldname": "resolution_by", "fieldtype": "Datetime", "label": "Resolution By", @@ -334,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", @@ -342,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", @@ -418,7 +420,7 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-05-14 23:59:01.172007", + "modified": "2020-05-25 22:02:32.974165", "modified_by": "Administrator", "module": "Support", "name": "Issue", From 899ec3653259b4973987e7a5f7d324c944bb3ec8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 26 May 2020 12:23:28 +0530 Subject: [PATCH 14/36] feat: added Resolved status with same functionality as Closed --- erpnext/support/doctype/issue/issue.json | 4 ++-- erpnext/support/doctype/issue/issue.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 2b79e03f45..8fb94013af 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -120,7 +120,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nReplied\nClosed", + "options": "Open\nReplied\nResolved\nClosed", "search_index": 1 }, { @@ -420,7 +420,7 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-05-25 22:02:32.974165", + "modified": "2020-05-26 12:12:59.343559", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 47b9c05161..8bc00fd2f0 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -59,7 +59,7 @@ class Issue(Document): 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) From ec6246306cfc6fa43cca182e6d67e4ea6ba77136 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 26 May 2020 12:32:12 +0530 Subject: [PATCH 15/36] refactor: set variance and SLA as Ongoing on Issue reopen --- erpnext/support/doctype/issue/issue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 8bc00fd2f0..02a2df1c19 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -72,6 +72,8 @@ class Issue(Document): # 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() + self.agreement_fulfilled = "Ongoing" + set_service_level_agreement_variance(issue=self.name) if self.status == "Replied" and status != "Replied": self.on_hold_since = frappe.flags.current_time or now_datetime() From 5379e87880409c300a2d22a219b810e32ed4a2e9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 26 May 2020 12:48:03 +0530 Subject: [PATCH 16/36] refactor: code cleanup, break functions --- erpnext/support/doctype/issue/issue.py | 62 +++++++++++++------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 02a2df1c19..b7da29649d 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -47,8 +47,8 @@ class Issue(Document): 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,7 +56,7 @@ class Issue(Document): 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 in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]: @@ -68,13 +68,18 @@ class Issue(Document): 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() + + def handle_hold_time(self): + # set response and resolution variance as None as the issue is on Hold for status as Replied if self.status == "Replied" and status != "Replied": self.on_hold_since = frappe.flags.current_time or now_datetime() if not self.first_responded_on: @@ -83,28 +88,30 @@ class Issue(Document): self.resolution_by = None self.resolution_by_variance = None + # calculate hold time when status is changed from Replied to any other status if self.status != "Replied" and status == "Replied": hold_time = self.total_hold_time if self.total_hold_time else 0 self.total_hold_time = hold_time + time_diff_in_seconds(now_datetime(), 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 == "Replied": start_date_time = get_datetime(self.service_level_agreement_creation) priority = get_priority(self) hold_time = time_diff_in_seconds(now_datetime(), 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) + response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) self.response_by = add_to_date(response_by, seconds=round(hold_time)) response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime())) self.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) + resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) self.resolution_by = add_to_date(resolution_by, seconds=round(hold_time)) resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime())) self.resolution_by_variance = resolution_by_variance + (hold_time // 3600) self.on_hold_since = None - def update_agreement_status(self): if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \ @@ -174,7 +181,7 @@ class Issue(Document): 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) @@ -210,20 +217,15 @@ class Issue(Document): 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())) @@ -259,9 +261,9 @@ class Issue(Document): self.save() def reset_issue_metrics(self): - self.db_set('resolution_time', None) - self.db_set('user_resolution_time', None) - self.db_set('avg_response_time', None) + self.db_set("resolution_time", None) + self.db_set("user_resolution_time", None) + self.db_set("avg_response_time", None) def get_priority(issue): @@ -280,9 +282,9 @@ def get_expected_time_for(parameter, service_level, start_date_time): start_time = None end_time = None - if parameter == 'response': + if parameter == "response": allotted_seconds = service_level.get("response_time") - elif parameter == 'resolution': + elif parameter == "resolution": allotted_seconds = service_level.get("resolution_time") else: frappe.throw(_("{0} parameter is invalid").format(parameter)) @@ -292,8 +294,8 @@ def get_expected_time_for(parameter, service_level, start_date_time): 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")) @@ -370,13 +372,13 @@ def set_average_response_time(issue): response_times.append(response_time) if response_times: avg_response_time = sum(response_times) / len(response_times) - issue.db_set('avg_response_time', avg_response_time) + issue.db_set("avg_response_time", avg_response_time) def set_resolution_time(issue): # total time taken from issue creation to closing resolution_time = time_diff_in_seconds(now_datetime(), issue.creation) - issue.db_set('resolution_time', resolution_time) + issue.db_set("resolution_time", resolution_time) def set_user_resolution_time(issue): @@ -399,7 +401,7 @@ def set_user_resolution_time(issue): total_pending_time = sum(pending_time) resolution_time_in_secs = time_diff_in_seconds(now_datetime(), issue.creation) user_resolution_time = resolution_time_in_secs - total_pending_time - issue.db_set('user_resolution_time', user_resolution_time) + issue.db_set("user_resolution_time", user_resolution_time) def get_list_context(context=None): @@ -409,7 +411,7 @@ def get_list_context(context=None): "row_template": "templates/includes/issue_row.html", "show_sidebar": True, "show_search": True, - 'no_breadcrumbs': True + "no_breadcrumbs": True } @@ -417,12 +419,12 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord 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(): From f3fc544918643502e0ec12f19fa18901c62795de Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 26 May 2020 15:03:15 +0530 Subject: [PATCH 17/36] fix: SLA tests --- .../test_service_level_agreement.py | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) 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 4a741ea4e1..94e0b582f9 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,19 @@ from __future__ import unicode_literals 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): frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) - create_service_level_for_sla() - # 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", + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", entity_type=None, entity=None, response_time=4, resolution_time=6) + 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,7 +28,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # 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", + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", entity_type="Customer", entity=customer, response_time=2, resolution_time=3) get_customer_service_level_agreement = get_service_level_agreement(entity_type="Customer", entity=customer) @@ -40,7 +40,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # 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", + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", entity_type="Customer Group", entity=customer_group, response_time=2, resolution_time=3) get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group) @@ -52,7 +52,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # 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", + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", entity_type="Territory", entity=territory, response_time=2, resolution_time=3) get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory) @@ -71,14 +71,18 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ 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, "holiday_list": holiday_list, "employee_group": employee_group, "entity_type": entity_type, @@ -167,6 +171,7 @@ def create_service_level_agreement(default_service_level_agreement, service_leve else: return frappe.get_doc("Service Level Agreement", service_level_agreement_exists) + def create_customer(): customer = frappe.get_doc({ "doctype": "Customer", @@ -206,23 +211,42 @@ def create_territory(): 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=4, resolution_time=6) 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=2, resolution_time=3) 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=2, resolution_time=3) 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=2, resolution_time=3) + +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() From f91ed4ce248490d7fe93856ab4159d4a11e1f78b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 26 May 2020 16:03:47 +0530 Subject: [PATCH 18/36] fix: add default priority field in SLA --- .../service_level_agreement.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 2d33c3e033..ede5f98eba 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -9,6 +9,7 @@ "enable", "section_break_2", "service_level", + "default_priority", "default_service_level_agreement", "column_break_2", "employee_group", @@ -150,10 +151,19 @@ "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, + "show_days": 1, + "show_seconds": 1 } ], "links": [], - "modified": "2020-05-06 11:46:38.834810", + "modified": "2020-05-26 16:02:59.859980", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", From c8e8e29083aa5d75615cbf22dd1dbe60cd47f7aa Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 28 May 2020 13:48:09 +0530 Subject: [PATCH 19/36] test: Issue Metrics --- erpnext/support/doctype/issue/issue.py | 16 +++--- erpnext/support/doctype/issue/test_issue.py | 52 ++++++++++++++++++- .../test_service_level_agreement.py | 17 +++--- 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index b7da29649d..31797dff42 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -76,9 +76,9 @@ class Issue(Document): self.agreement_fulfilled = "Ongoing" set_service_level_agreement_variance(issue=self.name) - self.handle_hold_time() + self.handle_hold_time(status) - def handle_hold_time(self): + def handle_hold_time(self, status): # set response and resolution variance as None as the issue is on Hold for status as Replied if self.status == "Replied" and status != "Replied": self.on_hold_since = frappe.flags.current_time or now_datetime() @@ -91,24 +91,26 @@ class Issue(Document): # calculate hold time when status is changed from Replied to any other status if self.status != "Replied" and status == "Replied": hold_time = self.total_hold_time if self.total_hold_time else 0 - self.total_hold_time = hold_time + time_diff_in_seconds(now_datetime(), self.on_hold_since) + now_time = frappe.flags.current_time or now_datetime() + self.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 == "Replied": 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_datetime(), 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) self.response_by = add_to_date(response_by, seconds=round(hold_time)) - response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime())) + response_by_variance = round(time_diff_in_hours(self.response_by, now_time)) self.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) self.resolution_by = add_to_date(resolution_by, seconds=round(hold_time)) - resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime())) + resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_time)) self.resolution_by_variance = resolution_by_variance + (hold_time // 3600) self.on_hold_since = None @@ -377,7 +379,7 @@ def set_average_response_time(issue): def set_resolution_time(issue): # total time taken from issue creation to closing - resolution_time = time_diff_in_seconds(now_datetime(), issue.creation) + resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation) issue.db_set("resolution_time", resolution_time) @@ -399,7 +401,7 @@ def set_user_resolution_time(issue): pending_time.append(wait_time) total_pending_time = sum(pending_time) - resolution_time_in_secs = time_diff_in_seconds(now_datetime(), issue.creation) + 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) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 7a5e3e300d..2818b1b8eb 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -10,10 +10,13 @@ import datetime 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,6 +75,33 @@ class TestIssue(unittest.TestCase): self.assertEqual(issue.agreement_fulfilled, 'Fulfilled') + def test_issue_metrics(self): + creation = datetime.datetime(2020, 3, 4, 4, 0) + + # make issue with customer specific SLA + customer = create_customer("_Test Customer", "__Test SLA Customer Group", "__Test SLA Territory") + issue = make_issue(creation, "_Test Customer", 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) + issue = frappe.get_doc('Issue', issue.name) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) + 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 make_issue(creation=None, customer=None, index=0): issue = frappe.get_doc({ @@ -86,6 +116,7 @@ def make_issue(creation=None, customer=None, index=0): return issue + def create_customer(name, customer_group, territory): create_customer_group(customer_group) @@ -99,6 +130,7 @@ def create_customer(name, customer_group, territory): "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 +139,7 @@ def create_customer_group(customer_group): "customer_group_name": customer_group }).insert(ignore_permissions=True) + def create_territory(territory): if not frappe.db.exists("Territory", {"territory_name": territory}): @@ -114,3 +147,20 @@ def create_territory(territory): "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 + }).insert(ignore_permissions=True) 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 94e0b582f9..57d4747e5c 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 @@ -16,7 +16,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # Default Service Level Agreement create_default_service_level_agreement = 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=4, resolution_time=6) + 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) @@ -29,7 +29,7 @@ class TestServiceLevelAgreement(unittest.TestCase): customer = create_customer() create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer", entity=customer, response_time=2, resolution_time=3) + 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) @@ -41,7 +41,7 @@ class TestServiceLevelAgreement(unittest.TestCase): customer_group = create_customer_group() create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity=customer_group, response_time=2, resolution_time=3) + 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) @@ -53,7 +53,7 @@ class TestServiceLevelAgreement(unittest.TestCase): territory = create_territory() create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Territory", entity=territory, response_time=2, resolution_time=3) + 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) @@ -83,6 +83,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "enable": 1, "service_level": "__Test Service Level", "default_service_level_agreement": default_service_level_agreement, + "default_priority": "Medium", "holiday_list": holiday_list, "employee_group": employee_group, "entity_type": entity_type, @@ -212,19 +213,19 @@ def create_territory(): def create_service_level_agreements_for_issues(): 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=4, resolution_time=6) + 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, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type="Customer", entity="_Test Customer", response_time=2, resolution_time=3) + 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, 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) + 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, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type="Territory", entity="_Test SLA Territory", response_time=2, resolution_time=3) + 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") From 376a46f1e5d14d6fc0c2b1f9fa05af174cf2b629 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 28 May 2020 14:33:30 +0530 Subject: [PATCH 20/36] test: hold time for Replied status --- erpnext/support/doctype/issue/issue.py | 2 +- erpnext/support/doctype/issue/test_issue.py | 45 ++++++++++++++++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 31797dff42..c09c729c5c 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -100,7 +100,7 @@ class Issue(Document): 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_datetime(), self.on_hold_since) + 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) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 2818b1b8eb..a004843270 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -78,9 +78,7 @@ class TestIssue(unittest.TestCase): def test_issue_metrics(self): creation = datetime.datetime(2020, 3, 4, 4, 0) - # make issue with customer specific SLA - customer = create_customer("_Test Customer", "__Test SLA Customer Group", "__Test SLA Territory") - issue = make_issue(creation, "_Test Customer", 1) + issue = make_issue(creation, index=1) create_communication(issue.name, "test@example.com", "Received", creation) creation = datetime.datetime(2020, 3, 4, 4, 15) @@ -91,9 +89,9 @@ class TestIssue(unittest.TestCase): creation = datetime.datetime(2020, 3, 4, 5, 5) create_communication(issue.name, "test@admin.com", "Sent", creation) - issue = frappe.get_doc('Issue', issue.name) frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) + issue.reload() issue.status = 'Closed' issue.save() @@ -101,9 +99,43 @@ class TestIssue(unittest.TestCase): 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), @@ -163,4 +195,5 @@ def create_communication(reference_name, sender, sent_or_received, creation): "reference_doctype": "Issue", "creation": creation, "reference_name": reference_name - }).insert(ignore_permissions=True) + }) + issue.save() From bc38289a595749824530ed5f6399b9d50153f3db Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 29 May 2020 11:43:05 +0530 Subject: [PATCH 21/36] fix: added patch for sla enhancements --- erpnext/patches.txt | 3 +- .../patches/v13_0/update_sla_enhancements.py | 111 ++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/update_sla_enhancements.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b0421f43c6..3ea15375d2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -694,4 +694,5 @@ execute:frappe.delete_doc("Report", "Department Analytics") execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True) 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 \ No newline at end of file +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 0000000000..1d5f372b9b --- /dev/null +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -0,0 +1,111 @@ +# 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] + 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() + + # set issue status as Replied since Hold status is removed + if frappe.db.exists('DocType', 'Issue'): + issues_on_hold = frappe.db.sql(""" + SELECT + name + FROM + `tabIssue` + WHERE + status = 'Hold' + """, as_dict=1) + + issues = [entry.name for entry in issues_on_hold] + + frappe.reload_doc('support', 'doctype', 'issue') + frappe.db.sql(""" + UPDATE + `tabIssue` + SET + status='Replied' + WHERE + name in %(issues)s + """, {'issues': issues}, debug=1) + + +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 From b3338a149bbb14c982e75a3dfce1b25425d7087e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 4 Jun 2020 12:35:00 +0530 Subject: [PATCH 22/36] fix: code clean-up --- .../patches/v13_0/update_sla_enhancements.py | 26 ++++++++----------- erpnext/support/doctype/issue/issue.js | 4 +-- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py index 1d5f372b9b..91c613fa46 100644 --- a/erpnext/patches/v13_0/update_sla_enhancements.py +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -22,10 +22,10 @@ def execute(): 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 - }) + frappe.db.set_value('Service Level Agreement', entry.name, { + 'holiday_list': holiday_list, + 'employee_group': employee_group + }) priority_dict = {} @@ -76,18 +76,14 @@ def execute(): row.db_update() sla.db_update() + frappe.delete_doc('DocType', 'Service Level') + # set issue status as Replied since Hold status is removed if frappe.db.exists('DocType', 'Issue'): - issues_on_hold = frappe.db.sql(""" - SELECT - name - FROM - `tabIssue` - WHERE - status = 'Hold' - """, as_dict=1) - + issues_on_hold = frappe.db.get_all('Issue', {'status': 'Hold'}) issues = [entry.name for entry in issues_on_hold] + if not issues: + return frappe.reload_doc('support', 'doctype', 'issue') frappe.db.sql(""" @@ -96,8 +92,8 @@ def execute(): SET status='Replied' WHERE - name in %(issues)s - """, {'issues': issues}, debug=1) + name IN %(issues)s + """, {'issues': issues}) def convert_to_seconds(value, unit): diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 66c62d1ff2..6632ab69ba 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -43,11 +43,11 @@ frappe.ui.form.on("Issue", { if (frm.doc.service_level_agreement) { if (frm.doc.status == "Replied") { frm.dashboard.clear_headline(); - let message = {"indicator": "orange", "msg": __("Replied {0}", [moment(frm.doc.on_hold_since).fromNow()])}; + let message = {"indicator": "orange", "msg": __("Replied {0}", [frappe.datetime.comment_when(frm.doc.on_hold_since)])}; frm.dashboard.set_headline_alert( '
' + '
' + - ' ' + + ''+ message.msg +' ' + '
' + '
' ); From 94d03c6100e5b6d0f14bca541a72602b3b492bbb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 4 Jun 2020 12:35:36 +0530 Subject: [PATCH 23/36] fix: remove Service Level DocType from Support Desk Page --- erpnext/support/desk_page/support/support.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index a3fe72d051..b1ad7c8aa0 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" } ] From 3f083501818d5522faf76a53307b3eb585478629 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 4 Jun 2020 15:56:59 +0530 Subject: [PATCH 24/36] fix: retain Hold status --- .../patches/v13_0/update_sla_enhancements.py | 17 ----------------- erpnext/support/doctype/issue/issue.json | 4 ++-- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py index 91c613fa46..2356fb2679 100644 --- a/erpnext/patches/v13_0/update_sla_enhancements.py +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -78,23 +78,6 @@ def execute(): frappe.delete_doc('DocType', 'Service Level') - # set issue status as Replied since Hold status is removed - if frappe.db.exists('DocType', 'Issue'): - issues_on_hold = frappe.db.get_all('Issue', {'status': 'Hold'}) - issues = [entry.name for entry in issues_on_hold] - if not issues: - return - - frappe.reload_doc('support', 'doctype', 'issue') - frappe.db.sql(""" - UPDATE - `tabIssue` - SET - status='Replied' - WHERE - name IN %(issues)s - """, {'issues': issues}) - def convert_to_seconds(value, unit): seconds = 0 diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 8fb94013af..70595e483a 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -120,7 +120,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nReplied\nResolved\nClosed", + "options": "Open\nReplied\nHold\nResolved\nClosed", "search_index": 1 }, { @@ -420,7 +420,7 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-05-26 12:12:59.343559", + "modified": "2020-06-04 15:53:38.322514", "modified_by": "Administrator", "module": "Support", "name": "Issue", From b29cb878683f79e0d7e2dd50ec5bc3f20fe50956 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 5 Jun 2020 13:35:38 +0530 Subject: [PATCH 25/36] refactor: setting avg_response_time moved to communication --- erpnext/support/doctype/issue/issue.py | 23 ------------------- .../service_level_priority.json | 22 +++++++++++++----- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index c09c729c5c..ac700c91e7 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -64,7 +64,6 @@ class Issue(Document): if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing": set_service_level_agreement_variance(issue=self.name) self.update_agreement_status() - set_average_response_time(issue=self) set_resolution_time(issue=self) set_user_resolution_time(issue=self) @@ -265,7 +264,6 @@ class Issue(Document): def reset_issue_metrics(self): self.db_set("resolution_time", None) self.db_set("user_resolution_time", None) - self.db_set("avg_response_time", None) def get_priority(issue): @@ -355,27 +353,6 @@ def set_service_level_agreement_variance(issue=None): if variance < 0: frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) -def set_average_response_time(issue): - # avg response time for all the responses - communications = frappe.get_list("Communication", filters={ - "reference_doctype": issue.doctype, - "reference_name": issue.name - }, - fields=["sent_or_received", "name", "creation"], - order_by="creation" - ) - - if len(communications): - response_times = [] - for i in range(len(communications)): - if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received": - response_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) - if response_time > 0: - response_times.append(response_time) - if response_times: - avg_response_time = sum(response_times) / len(response_times) - issue.db_set("avg_response_time", avg_response_time) - def set_resolution_time(issue): # total time taken from issue creation to closing 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 6377d1a962..3995d1e248 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -20,11 +20,15 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Priority", - "options": "Issue Priority" + "options": "Issue Priority", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sb_00", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "columns": 2, @@ -35,11 +39,15 @@ }, { "fieldname": "cb_00", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cb_01", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "columns": 1, @@ -47,7 +55,9 @@ "fieldname": "default_priority", "fieldtype": "Check", "in_list_view": 1, - "label": "Default Priority" + "label": "Default Priority", + "show_days": 1, + "show_seconds": 1 }, { "columns": 2, @@ -59,7 +69,7 @@ ], "istable": 1, "links": [], - "modified": "2020-05-04 22:08:04.503949", + "modified": "2020-06-05 13:08:26.428657", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", From 2c9f7cf371ac0ee8d6751a87b6f44bd33edb17f7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 5 Jun 2020 16:35:57 +0530 Subject: [PATCH 26/36] feat: pause SLA on statuses configuration --- erpnext/support/doctype/issue/issue.json | 3 +- erpnext/support/doctype/issue/issue.py | 64 ++++++++++--------- .../doctype/pause_sla_on_status/__init__.py | 0 .../pause_sla_on_status.json | 33 ++++++++++ .../pause_sla_on_status.py | 10 +++ .../support_settings/support_settings.js | 10 ++- .../support_settings/support_settings.json | 13 +++- 7 files changed, 100 insertions(+), 33 deletions(-) create mode 100644 erpnext/support/doctype/pause_sla_on_status/__init__.py create mode 100644 erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json create mode 100644 erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 70595e483a..712b70c2dd 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -403,6 +403,7 @@ { "fieldname": "on_hold_since", "fieldtype": "Datetime", + "hidden": 1, "label": "On Hold Since", "read_only": 1, "show_days": 1, @@ -420,7 +421,7 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-06-04 15:53:38.322514", + "modified": "2020-06-05 15:45:24.474425", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index ac700c91e7..146eb5a249 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -78,40 +78,44 @@ class Issue(Document): self.handle_hold_time(status) def handle_hold_time(self, status): - # set response and resolution variance as None as the issue is on Hold for status as Replied - if self.status == "Replied" and status != "Replied": - self.on_hold_since = frappe.flags.current_time or now_datetime() - if not self.first_responded_on: - self.response_by = None - self.response_by_variance = None - self.resolution_by = None - self.resolution_by_variance = None + if frappe.db.get_single_value("Support Settings", "track_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"]) + hold_statuses = [entry.status for entry in pause_sla_on] - # calculate hold time when status is changed from Replied to any other status - if self.status != "Replied" and status == "Replied": - hold_time = self.total_hold_time if self.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime() - self.total_hold_time = hold_time + time_diff_in_seconds(now_time, self.on_hold_since) + if self.status in hold_statuses and status not in hold_statuses: + self.on_hold_since = frappe.flags.current_time or now_datetime() + if not self.first_responded_on: + self.response_by = None + self.response_by_variance = None + self.resolution_by = None + self.resolution_by_variance = None - # re-calculate SLA variables after issue changes from Replied to Open - # add hold time to SLA variables - if self.status == "Open" and status == "Replied": - 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) + # 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() + self.total_hold_time = 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) - self.response_by = add_to_date(response_by, seconds=round(hold_time)) - response_by_variance = round(time_diff_in_hours(self.response_by, now_time)) - self.response_by_variance = response_by_variance + (hold_time // 3600) + # 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) - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - self.resolution_by = add_to_date(resolution_by, seconds=round(hold_time)) - resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_time)) - self.resolution_by_variance = resolution_by_variance + (hold_time // 3600) - self.on_hold_since = None + if not self.first_responded_on: + response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + self.response_by = add_to_date(response_by, seconds=round(hold_time)) + response_by_variance = round(time_diff_in_hours(self.response_by, now_time)) + self.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) + self.resolution_by = add_to_date(resolution_by, seconds=round(hold_time)) + resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_time)) + self.resolution_by_variance = resolution_by_variance + (hold_time // 3600) + self.on_hold_since = None def update_agreement_status(self): if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": diff --git a/erpnext/support/doctype/pause_sla_on_status/__init__.py b/erpnext/support/doctype/pause_sla_on_status/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..5b03f25f48 --- /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 0000000000..a3b547e480 --- /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/support_settings/support_settings.js b/erpnext/support/doctype/support_settings/support_settings.js index 1d1069d58b..33ddf0b079 100644 --- a/erpnext/support/doctype/support_settings/support_settings.js +++ b/erpnext/support/doctype/support_settings/support_settings.js @@ -2,7 +2,15 @@ // For license information, please see license.txt frappe.ui.form.on('Support Settings', { - refresh: function(frm) { + 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); + }); } }); diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json index 3f52181a46..da4b607e2c 100644 --- a/erpnext/support/doctype/support_settings/support_settings.json +++ b/erpnext/support/doctype/support_settings/support_settings.json @@ -10,6 +10,7 @@ "allow_resetting_service_level_agreement", "issues_sb", "close_issue_after_days", + "pause_sla_on_status", "portal_sb", "get_started_sections", "show_latest_forum_posts", @@ -123,14 +124,24 @@ }, { "default": "0", + "depends_on": "eval:doc.track_service_level_agreement;", "fieldname": "allow_resetting_service_level_agreement", "fieldtype": "Check", "label": "Allow Resetting Service Level Agreement" + }, + { + "depends_on": "eval:doc.track_service_level_agreement;", + "fieldname": "pause_sla_on_status", + "fieldtype": "Table", + "label": "Pause SLA On", + "options": "Pause SLA On Status", + "show_days": 1, + "show_seconds": 1 } ], "issingle": 1, "links": [], - "modified": "2020-04-28 14:11:15.117019", + "modified": "2020-06-05 16:35:13.905096", "modified_by": "Administrator", "module": "Support", "name": "Support Settings", From 8d3992841847d09a118ae6d4efe6fa974c7fab63 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 5 Jun 2020 16:36:31 +0530 Subject: [PATCH 27/36] refactor: dashboard indicator when SLA is on hold --- erpnext/support/doctype/issue/issue.js | 42 +++++++++++++++++--------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 6632ab69ba..32a77739d0 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -38,22 +38,35 @@ frappe.ui.form.on("Issue", { }, refresh: function (frm) { - if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") { if (frm.doc.service_level_agreement) { - if (frm.doc.status == "Replied") { - frm.dashboard.clear_headline(); - let message = {"indicator": "orange", "msg": __("Replied {0}", [frappe.datetime.comment_when(frm.doc.on_hold_since)])}; - frm.dashboard.set_headline_alert( - '
' + - '
' + - ''+ message.msg +' ' + - '
' + - '
' - ); - } else { - set_time_to_resolve_and_response(frm); - } + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Support Settings', + name: 'Support Settings' + }, + callback: function(data) { + let statuses = data.message.pause_sla_on_status; + 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( + '
' + + '
' + + ''+ message.msg +' ' + + '
' + + '
' + ); + } else { + set_time_to_resolve_and_response(frm); + } + } + }); } frm.add_custom_button(__("Close"), function () { @@ -67,6 +80,7 @@ frappe.ui.form.on("Issue", { frm: frm }); }, __("Make")); + } else { if (frm.doc.service_level_agreement) { frm.dashboard.clear_headline(); From 25ef6a963811210c65d6ef3778bc0923c365c5ad Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 5 Jun 2020 16:37:20 +0530 Subject: [PATCH 28/36] fix: add default hold statuses in fixtures and patch --- erpnext/patches/v13_0/update_sla_enhancements.py | 4 ++++ erpnext/setup/install.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py index 2356fb2679..884d01b10c 100644 --- a/erpnext/patches/v13_0/update_sla_enhancements.py +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from erpnext.setup.install import add_sla_hold_statuses_to_support_settings def execute(): # add holiday list and employee group fields in SLA @@ -78,6 +79,9 @@ def execute(): frappe.delete_doc('DocType', 'Service Level') + # add SLA hold statuses to Support Settings + add_sla_hold_statuses_to_support_settings() + def convert_to_seconds(value, unit): seconds = 0 diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index e666a41f30..90e5f5a0ae 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -25,6 +25,7 @@ def after_install(): create_default_success_action() create_default_energy_point_rules() add_company_to_session_defaults() + add_sla_hold_statuses_to_support_settings() frappe.db.commit() @@ -105,3 +106,13 @@ def add_company_to_session_defaults(): "ref_doctype": "Company" }) settings.save() + +def add_sla_hold_statuses_to_support_settings(): + settings = frappe.get_single("Support Settings") + settings.append("pause_sla_on_status", { + "status": "Replied" + }) + settings.append("pause_sla_on_status", { + "status": "Hold" + }) + settings.save() From d7d3ca4214e2be005da27342bf2d0fd6e621baa5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 5 Jun 2020 16:57:37 +0530 Subject: [PATCH 29/36] fix: tests --- erpnext/support/doctype/issue/test_issue.py | 2 ++ .../service_level_agreement/test_service_level_agreement.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index a004843270..93a6005cc2 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe import unittest from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues +from erpnext.setup.install import add_sla_hold_statuses_to_support_settings from frappe.utils import now_datetime, get_datetime import datetime from datetime import timedelta @@ -100,6 +101,7 @@ class TestIssue(unittest.TestCase): self.assertEqual(issue.user_resolution_time, 1200) def test_hold_time_on_replied(self): + add_sla_hold_statuses_to_support_settings() creation = datetime.datetime(2020, 3, 4, 4, 0) issue = make_issue(creation, index=1) 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 57d4747e5c..26740ed899 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 @@ -9,10 +9,11 @@ from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_ 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) + def test_service_level_agreement(self): # Default Service Level Agreement create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1, holiday_list="__Test Holiday List", employee_group="_Test Employee Group", From 73326d308b610458e28dc0516ff50aee9d1c7d35 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 5 Jun 2020 18:23:39 +0530 Subject: [PATCH 30/36] refactor: move pause SLA configuration to SLA DocType --- .../patches/v13_0/update_sla_enhancements.py | 4 ---- erpnext/setup/install.py | 10 ---------- erpnext/support/doctype/issue/issue.js | 6 +++--- erpnext/support/doctype/issue/issue.py | 5 +++-- erpnext/support/doctype/issue/test_issue.py | 2 -- .../service_level_agreement.js | 12 +++++++++++- .../service_level_agreement.json | 19 ++++++++++++++++++- .../test_service_level_agreement.py | 5 +++++ .../support_settings/support_settings.js | 12 ++---------- .../support_settings/support_settings.json | 12 +----------- 10 files changed, 43 insertions(+), 44 deletions(-) diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py index 884d01b10c..2356fb2679 100644 --- a/erpnext/patches/v13_0/update_sla_enhancements.py +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import frappe -from erpnext.setup.install import add_sla_hold_statuses_to_support_settings def execute(): # add holiday list and employee group fields in SLA @@ -79,9 +78,6 @@ def execute(): frappe.delete_doc('DocType', 'Service Level') - # add SLA hold statuses to Support Settings - add_sla_hold_statuses_to_support_settings() - def convert_to_seconds(value, unit): seconds = 0 diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 90e5f5a0ae..74ff0ecfd8 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -25,7 +25,6 @@ def after_install(): create_default_success_action() create_default_energy_point_rules() add_company_to_session_defaults() - add_sla_hold_statuses_to_support_settings() frappe.db.commit() @@ -107,12 +106,3 @@ def add_company_to_session_defaults(): }) settings.save() -def add_sla_hold_statuses_to_support_settings(): - settings = frappe.get_single("Support Settings") - settings.append("pause_sla_on_status", { - "status": "Replied" - }) - settings.append("pause_sla_on_status", { - "status": "Hold" - }) - settings.save() diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 32a77739d0..e7e5bd312b 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -43,11 +43,11 @@ frappe.ui.form.on("Issue", { frappe.call({ 'method': 'frappe.client.get', args: { - doctype: 'Support Settings', - name: 'Support Settings' + doctype: 'Service Level Agreement', + name: frm.doc.service_level_agreement }, callback: function(data) { - let statuses = data.message.pause_sla_on_status; + let statuses = data.message.pause_sla_on; const hold_statuses = []; $.each(statuses, (_i, entry) => { hold_statuses.push(entry.status); diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 146eb5a249..4003047e81 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -78,9 +78,10 @@ class Issue(Document): self.handle_hold_time(status) def handle_hold_time(self, status): - if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): + 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"]) + 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] if self.status in hold_statuses and status not in hold_statuses: diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 93a6005cc2..a004843270 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import frappe import unittest from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues -from erpnext.setup.install import add_sla_hold_statuses_to_support_settings from frappe.utils import now_datetime, get_datetime import datetime from datetime import timedelta @@ -101,7 +100,6 @@ class TestIssue(unittest.TestCase): self.assertEqual(issue.user_resolution_time, 1200) def test_hold_time_on_replied(self): - add_sla_hold_statuses_to_support_settings() creation = datetime.datetime(2020, 3, 4, 4, 0) issue = make_issue(creation, index=1) 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 7aeaae48ba..5346195a39 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -2,5 +2,15 @@ // For license information, please see license.txt frappe.ui.form.on('Service Level Agreement', { + 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 ede5f98eba..e65169c0d4 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -23,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", @@ -160,10 +162,25 @@ "read_only": 1, "show_days": 1, "show_seconds": 1 + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "hide_border": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "pause_sla_on", + "fieldtype": "Table", + "label": "Pause SLA On", + "options": "Pause SLA On Status", + "show_days": 1, + "show_seconds": 1 } ], "links": [], - "modified": "2020-05-26 16:02:59.859980", + "modified": "2020-06-05 17:51:15.050785", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", 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 26740ed899..0746a9c73e 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 @@ -115,6 +115,11 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "resolution_time_period": "Hour", } ], + "pause_sla_on": [ + { + "status": "Replied" + } + ], "support_and_resolution": [ { "workday": "Monday", diff --git a/erpnext/support/doctype/support_settings/support_settings.js b/erpnext/support/doctype/support_settings/support_settings.js index 33ddf0b079..78adca81ca 100644 --- a/erpnext/support/doctype/support_settings/support_settings.js +++ b/erpnext/support/doctype/support_settings/support_settings.js @@ -2,15 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Support Settings', { - 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); - }); + refresh: function(frm) { + // } }); diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json index da4b607e2c..1c1b0c3517 100644 --- a/erpnext/support/doctype/support_settings/support_settings.json +++ b/erpnext/support/doctype/support_settings/support_settings.json @@ -10,7 +10,6 @@ "allow_resetting_service_level_agreement", "issues_sb", "close_issue_after_days", - "pause_sla_on_status", "portal_sb", "get_started_sections", "show_latest_forum_posts", @@ -128,20 +127,11 @@ "fieldname": "allow_resetting_service_level_agreement", "fieldtype": "Check", "label": "Allow Resetting Service Level Agreement" - }, - { - "depends_on": "eval:doc.track_service_level_agreement;", - "fieldname": "pause_sla_on_status", - "fieldtype": "Table", - "label": "Pause SLA On", - "options": "Pause SLA On Status", - "show_days": 1, - "show_seconds": 1 } ], "issingle": 1, "links": [], - "modified": "2020-06-05 16:35:13.905096", + "modified": "2020-06-05 17:56:17.491684", "modified_by": "Administrator", "module": "Support", "name": "Support Settings", From bb3b616a046b07996a47e1154e59d878aff45cd1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 9 Jun 2020 23:17:51 +0530 Subject: [PATCH 31/36] fix: set valuesin db for handle hold time --- erpnext/support/doctype/issue/issue.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 4003047e81..a23fe0564f 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -83,20 +83,21 @@ class Issue(Document): 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: - self.on_hold_since = frappe.flags.current_time or now_datetime() + update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() if not self.first_responded_on: - self.response_by = None - self.response_by_variance = None - self.resolution_by = None - self.resolution_by_variance = None + 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() - self.total_hold_time = hold_time + time_diff_in_seconds(now_time, self.on_hold_since) + 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 @@ -108,15 +109,17 @@ class Issue(Document): if not self.first_responded_on: response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - self.response_by = add_to_date(response_by, seconds=round(hold_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)) - self.response_by_variance = response_by_variance + (hold_time // 3600) + 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) - self.resolution_by = add_to_date(resolution_by, seconds=round(hold_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)) - self.resolution_by_variance = resolution_by_variance + (hold_time // 3600) - self.on_hold_since = None + 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": From 1e49c6b68b6794794c4113319aadf211d848d7fe Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 10 Jun 2020 12:58:16 +0530 Subject: [PATCH 32/36] fix: update duration options --- erpnext/support/doctype/issue/issue.json | 22 +++++----------- .../service_level_agreement.json | 14 +++------- .../service_level_priority.json | 26 +++++++------------ 3 files changed, 20 insertions(+), 42 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 712b70c2dd..6525ab27d3 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -380,48 +380,38 @@ "fieldname": "avg_response_time", "fieldtype": "Duration", "label": "Average Response Time", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "resolution_time", "fieldtype": "Duration", "label": "Resolution Time", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "user_resolution_time", "fieldtype": "Duration", "label": "User Resolution Time", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "on_hold_since", "fieldtype": "Datetime", "hidden": 1, "label": "On Hold Since", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_hold_time", "fieldtype": "Duration", "label": "Total Hold Time", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-06-05 15:45:24.474425", + "modified": "2020-06-10 12:47:37.146914", "modified_by": "Administrator", "module": "Support", "name": "Issue", 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 e65169c0d4..939c199982 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -159,28 +159,22 @@ "fieldtype": "Link", "label": "Default Priority", "options": "Issue Priority", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "section_break_18", "fieldtype": "Section Break", - "hide_border": 1, - "show_days": 1, - "show_seconds": 1 + "hide_border": 1 }, { "fieldname": "pause_sla_on", "fieldtype": "Table", "label": "Pause SLA On", - "options": "Pause SLA On Status", - "show_days": 1, - "show_seconds": 1 + "options": "Pause SLA On Status" } ], "links": [], - "modified": "2020-06-05 17:51:15.050785", + "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_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json index 3995d1e248..0a88c53bf0 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -20,34 +20,28 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Priority", - "options": "Issue Priority", - "show_days": 1, - "show_seconds": 1 + "options": "Issue Priority" }, { "fieldname": "sb_00", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "columns": 2, "fieldname": "resolution_time", "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Resolution Time" }, { "fieldname": "cb_00", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "cb_01", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "columns": 1, @@ -55,21 +49,21 @@ "fieldname": "default_priority", "fieldtype": "Check", "in_list_view": 1, - "label": "Default Priority", - "show_days": 1, - "show_seconds": 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, "links": [], - "modified": "2020-06-05 13:08:26.428657", + "modified": "2020-06-10 12:45:47.545915", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", From 9c27d683ba542574c07ff45d9f72a9d158c4cb67 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 11 Jun 2020 17:03:53 +0530 Subject: [PATCH 33/36] fix: priority column width Co-authored-by: Himanshu --- .../service_level_priority/service_level_priority.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0a88c53bf0..65d51694cc 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -15,7 +15,7 @@ ], "fields": [ { - "columns": 1, + "columns": 2, "fieldname": "priority", "fieldtype": "Link", "in_list_view": 1, @@ -73,4 +73,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From d7481f59ba6dec000b0fd5b970cf970e96211369 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 11 Jun 2020 17:41:05 +0530 Subject: [PATCH 34/36] fix: patch --- .../patches/v13_0/update_sla_enhancements.py | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py index 2356fb2679..3eb0411f82 100644 --- a/erpnext/patches/v13_0/update_sla_enhancements.py +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -45,38 +45,39 @@ def execute(): # copy Service Levels to Service Level Agreements sl = [entry.service_level for entry in sla_details] - 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) + 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) + 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() + 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('DocType', 'Service Level') + frappe.delete_doc_if_exists('DocType', 'Service Level') def convert_to_seconds(value, unit): From 30487bd85430b686c957943982eddbdfd7b95a4b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 15 Jun 2020 13:49:58 +0530 Subject: [PATCH 35/36] fix: codacy issues --- erpnext/support/doctype/issue/issue.py | 2 +- .../doctype/service_level_agreement/service_level_agreement.py | 1 - .../service_level_agreement/test_service_level_agreement.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index a23fe0564f..883e603fd3 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -7,7 +7,7 @@ import json 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, time_diff_in_seconds, time_diff +from frappe.utils import 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 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 530230e1e8..c692315706 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -7,7 +7,6 @@ import frappe from frappe.model.document import Document from frappe import _ from frappe.utils import getdate, get_weekdays -from datetime import datetime class ServiceLevelAgreement(Document): 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 0746a9c73e..07ef368cbe 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 @@ -236,7 +236,6 @@ def create_service_level_agreements_for_issues(): 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 096792791ba459ef83ae8e1e9ecf5cec6488c9e6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 15 Jun 2020 14:07:06 +0530 Subject: [PATCH 36/36] fix(patch): reload child tables --- erpnext/patches/v13_0/update_sla_enhancements.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py index 3eb0411f82..c156ba9577 100644 --- a/erpnext/patches/v13_0/update_sla_enhancements.py +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -15,7 +15,9 @@ def execute(): }) frappe.reload_doc('support', 'doctype', 'service_level_agreement') + frappe.reload_doc('support', 'doctype', 'pause_sla_on_status') frappe.reload_doc('support', 'doctype', 'service_level_priority') + frappe.reload_doc('support', 'doctype', 'service_day') for entry in sla_details: values = frappe.db.get_value('Service Level', entry.service_level, ['holiday_list', 'employee_group'])