From d385a958235cca7cb74e3794e22111e4fe3451a8 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Thu, 25 Jun 2020 00:09:22 +0530 Subject: [PATCH 01/49] fix: SLA for multiple doctypes --- erpnext/hooks.py | 3 + erpnext/support/doctype/issue/issue.js | 1 + erpnext/support/doctype/issue/issue.py | 298 +------------- .../service_level_agreement.json | 15 +- .../service_level_agreement.py | 386 ++++++++++++++++-- 5 files changed, 373 insertions(+), 330 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2a695896ed05..11f0808879ea 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -217,6 +217,9 @@ } doc_events = { + "*": { + "validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply" + }, "Stock Entry": { "on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty" diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index e7e5bd312bc0..d6270cde0cbd 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -12,6 +12,7 @@ frappe.ui.form.on("Issue", { frappe.call({ method: "erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters", args: { + doctype: frm.doc.doctype, name: frm.doc.service_level_agreement, customer: frm.doc.customer }, diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 883e603fd31a..3ff1f5c66c3a 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -11,7 +11,6 @@ from datetime import datetime, timedelta from frappe.model.mapper import get_mapped_doc from frappe.utils.user import is_website_user -from erpnext.support.doctype.service_level_agreement.service_level_agreement import get_active_service_level_agreement_for from frappe.email.inbox import link_communication_to_document class Issue(Document): @@ -25,8 +24,6 @@ def validate(self): if not self.raised_by: self.raised_by = frappe.session.user - self.change_service_level_agreement_and_priority() - self.update_status() self.set_lead_contact(self.raised_by) def on_update(self): @@ -54,94 +51,6 @@ def set_lead_contact(self, email_id): self.company = frappe.db.get_value("Lead", self.lead, "company") or \ frappe.db.get_default("Company") - 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: - self.first_responded_on = frappe.flags.current_time or now_datetime() - - if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]: - self.resolution_date = frappe.flags.current_time or now_datetime() - if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing": - set_service_level_agreement_variance(issue=self.name) - self.update_agreement_status() - set_resolution_time(issue=self) - set_user_resolution_time(issue=self) - - if self.status == "Open" and status != "Open": - # if no date, it should be set as None and not a blank string "", as per mysql strict config - self.resolution_date = None - self.reset_issue_metrics() - # enable SLA and variance on Reopen - self.agreement_fulfilled = "Ongoing" - set_service_level_agreement_variance(issue=self.name) - - self.handle_hold_time(status) - - def handle_hold_time(self, status): - if self.service_level_agreement: - # set response and resolution variance as None as the issue is on Hold for status as Replied - pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"], - filters={"parent": self.service_level_agreement}) - hold_statuses = [entry.status for entry in pause_sla_on] - update_values = {} - - if self.status in hold_statuses and status not in hold_statuses: - update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() - if not self.first_responded_on: - update_values['response_by'] = None - update_values['response_by_variance'] = 0 - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 - - # calculate hold time when status is changed from Replied to any other status - if self.status not in hold_statuses and status in hold_statuses: - hold_time = self.total_hold_time if self.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime() - update_values['total_hold_time'] = hold_time + time_diff_in_seconds(now_time, self.on_hold_since) - - # re-calculate SLA variables after issue changes from Replied to Open - # add hold time to SLA variables - if self.status == "Open" and status in hold_statuses: - start_date_time = get_datetime(self.service_level_agreement_creation) - priority = get_priority(self) - now_time = frappe.flags.current_time or now_datetime() - hold_time = time_diff_in_seconds(now_time, self.on_hold_since) - - if not self.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - update_values['response_by'] = add_to_date(response_by, seconds=round(hold_time)) - response_by_variance = round(time_diff_in_hours(self.response_by, now_time)) - update_values['response_by_variance'] = response_by_variance + (hold_time // 3600) - - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - update_values['resolution_by'] = add_to_date(resolution_by, seconds=round(hold_time)) - resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_time)) - update_values['resolution_by_variance'] = resolution_by_variance + (hold_time // 3600) - update_values['on_hold_since'] = None - - self.db_set(update_values) - - def update_agreement_status(self): - if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": - if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \ - frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0: - - self.agreement_fulfilled = "Failed" - else: - self.agreement_fulfilled = "Fulfilled" - - def update_agreement_fulfilled_on_custom_status(self): - """ - Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status - """ - if not self.first_responded_on: # first_responded_on set when first reply is sent to customer - self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime()), 2) - - if not self.resolution_date: # resolution_date set when issue has been closed - self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()), 2) - - self.agreement_fulfilled = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" - def create_communication(self): communication = frappe.new_doc("Communication") communication.update({ @@ -207,189 +116,6 @@ def split_issue(self, subject, communication_id): return replicated_issue.name - def before_insert(self): - if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): - self.set_response_and_resolution_time() - - def set_response_and_resolution_time(self, priority=None, service_level_agreement=None): - service_level_agreement = get_active_service_level_agreement_for(priority=priority, - customer=self.customer, service_level_agreement=service_level_agreement) - - if not service_level_agreement: - if frappe.db.get_value("Issue", self.name, "service_level_agreement"): - frappe.throw(_("Couldn't Set Service Level Agreement {0}.").format(self.service_level_agreement)) - return - - if (service_level_agreement.customer and self.customer) and not (service_level_agreement.customer == self.customer): - frappe.throw(_("This Service Level Agreement is specific to Customer {0}").format(service_level_agreement.customer)) - - self.service_level_agreement = service_level_agreement.name - self.priority = service_level_agreement.default_priority if not priority else priority - - 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_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())) - - def change_service_level_agreement_and_priority(self): - if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \ - frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): - - if not self.priority == frappe.db.get_value("Issue", self.name, "priority"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - frappe.msgprint(_("Priority has been changed to {0}.").format(self.priority)) - - if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) - - def reset_service_level_agreement(self, reason, user): - if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): - frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) - - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Info", - "reference_doctype": self.doctype, - "reference_name": self.name, - "comment_email": user, - "content": " resetted Service Level Agreement - {0}".format(_(reason)), - }).insert(ignore_permissions=True) - - self.service_level_agreement_creation = now_datetime() - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - self.agreement_fulfilled = "Ongoing" - self.save() - - def reset_issue_metrics(self): - self.db_set("resolution_time", None) - self.db_set("user_resolution_time", None) - - -def get_priority(issue): - service_level_agreement = frappe.get_doc("Service Level Agreement", issue.service_level_agreement) - priority = service_level_agreement.get_service_level_agreement_priority(issue.priority) - priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list - }) - return priority - - -def get_expected_time_for(parameter, service_level, start_date_time): - current_date_time = start_date_time - expected_time = current_date_time - start_time = None - end_time = None - - if parameter == "response": - allotted_seconds = service_level.get("response_time") - elif parameter == "resolution": - allotted_seconds = service_level.get("resolution_time") - else: - frappe.throw(_("{0} parameter is invalid").format(parameter)) - - expected_time_is_set = 0 - - support_days = {} - for service in service_level.get("support_and_resolution"): - support_days[service.workday] = frappe._dict({ - "start_time": service.start_time, - "end_time": service.end_time, - }) - - holidays = get_holidays(service_level.get("holiday_list")) - weekdays = get_weekdays() - - while not expected_time_is_set: - current_weekday = weekdays[current_date_time.weekday()] - - if not is_holiday(current_date_time, holidays) and current_weekday in support_days: - start_time = current_date_time - datetime(current_date_time.year, current_date_time.month, current_date_time.day) \ - 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_seconds(end_time, start_time) - - # no time left for support today - 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, seconds=allotted_seconds) - expected_time_is_set = 1 - else: - 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 allotted_seconds >= 86400: - current_date_time = datetime.combine(getdate(current_date_time), get_time(end_time)) - else: - current_date_time = expected_time - - return current_date_time - -def set_service_level_agreement_variance(issue=None): - current_time = frappe.flags.current_time or now_datetime() - - filters = {"status": "Open", "agreement_fulfilled": "Ongoing"} - if issue: - filters = {"name": issue} - - for issue in frappe.get_list("Issue", filters=filters): - doc = frappe.get_doc("Issue", issue.name) - - if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer - variance = round(time_diff_in_hours(doc.response_by, current_time), 2) - frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False) - if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) - - if not doc.resolution_date: # resolution_date set when issue has been closed - variance = round(time_diff_in_hours(doc.resolution_by, current_time), 2) - frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False) - if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) - - -def set_resolution_time(issue): - # total time taken from issue creation to closing - resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation) - issue.db_set("resolution_time", resolution_time) - - -def set_user_resolution_time(issue): - # total time taken by a user to close the issue apart from wait_time - communications = frappe.get_list("Communication", filters={ - "reference_doctype": issue.doctype, - "reference_name": issue.name - }, - fields=["sent_or_received", "name", "creation"], - order_by="creation" - ) - - pending_time = [] - for i in range(len(communications)): - if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent": - wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) - if wait_time > 0: - pending_time.append(wait_time) - - total_pending_time = sum(pending_time) - resolution_time_in_secs = time_diff_in_seconds(issue.resolution_date, issue.creation) - user_resolution_time = resolution_time_in_secs - total_pending_time - issue.db_set("user_resolution_time", user_resolution_time) - def get_list_context(context=None): return { @@ -429,15 +155,12 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord @frappe.whitelist() def set_multiple_status(names, status): - names = json.loads(names) - for name in names: - set_status(name, status) + for name in json.loads(names): + frappe.db.set_value("Issue", name, "status", status) @frappe.whitelist() def set_status(name, status): - st = frappe.get_doc("Issue", name) - st.status = status - st.save() + frappe.db.set_value("Issue", name, "status", status) def auto_close_tickets(): """Auto-close replied support tickets after 7 days""" @@ -463,14 +186,6 @@ def update_issue(contact, method): """Called when Contact is deleted""" frappe.db.sql("""UPDATE `tabIssue` set contact='' where contact=%s""", contact.name) -def get_holidays(holiday_list_name): - holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name) - holidays = [holiday.holiday_date for holiday in holiday_list.holidays] - return holidays - -def is_holiday(date, holidays): - return getdate(date) in holidays - @frappe.whitelist() def make_task(source_name, target_doc=None): return get_mapped_doc("Issue", source_name, { @@ -495,10 +210,3 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals link_communication_to_document(doc, "Issue", issue.name, ignore_communication_links) return issue.name - -def get_time_in_timedelta(time): - """ - Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215) - """ - import datetime - return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) \ 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 939c19998288..e047aa908ccc 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -8,11 +8,12 @@ "field_order": [ "enable", "section_break_2", - "service_level", - "default_priority", + "document_type", "default_service_level_agreement", - "column_break_2", "employee_group", + "default_priority", + "column_break_2", + "service_level", "holiday_list", "entity_section", "entity_type", @@ -171,10 +172,16 @@ "fieldtype": "Table", "label": "Pause SLA On", "options": "Pause SLA On Status" + }, + { + "fieldname": "document_type", + "fieldtype": "Link", + "label": "Document Type", + "options": "DocType" } ], "links": [], - "modified": "2020-06-10 12:30:15.050785", + "modified": "2020-06-23 17:25:17.766570", "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 c69231570649..754c0e0c933a 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,7 +6,8 @@ import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import getdate, get_weekdays +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 class ServiceLevelAgreement(Document): @@ -76,20 +77,19 @@ 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.")) - if self.default_service_level_agreement: - if frappe.db.exists("Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]}): - frappe.throw(_("A Default Service Level Agreement already exists.")) - else: - if self.start_date and self.end_date: - if getdate(self.start_date) >= getdate(self.end_date): - frappe.throw(_("Start Date of Agreement can't be greater than or equal to End Date.")) + if self.default_service_level_agreement and frappe.db.exists("Service Level Agreement", \ + {"document_type": self.document_type, "default_service_level_agreement": "1", "name": ["!=", self.name]}): + + frappe.throw(_("Default Service Level Agreement for {0} already exists.").format(self.document_type)) + + if self.start_date and self.end_date: + self.validate_from_to_dates(self.start_date, self.end_date) - if getdate(self.end_date) < getdate(frappe.utils.getdate()): - frappe.throw(_("End Date of Agreement can't be less than today.")) + if self.entity_type and self.entity and frappe.db.exists("Service Level Agreement", {"entity_type": self.entity_type, \ + "entity": self.entity, "name": ["!=", self.name]}): - if self.entity_type and self.entity: - if frappe.db.exists("Service Level Agreement", {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]}): - frappe.throw(_("Service Level Agreement with Entity Type {0} and Entity {1} already exists.").format(self.entity_type, self.entity)) + frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format(frappe.bold(self.entity_type), \ + frappe.bold(self.entity))) def get_service_level_agreement_priority(self, priority): priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name}) @@ -111,21 +111,26 @@ def check_agreement_status(): if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()): frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0) -def get_active_service_level_agreement_for(priority, customer=None, service_level_agreement=None): +def get_active_service_level_agreement_for(doctype, priority, customer=None, service_level_agreement=None): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): return filters = [ + ["Service Level Agreement", "document_type", "=", doctype], ["Service Level Agreement", "active", "=", 1], ["Service Level Agreement", "enable", "=", 1] ] + or_filters = [] + if priority: filters.append(["Service Level Priority", "priority", "=", priority]) - or_filters = [ - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] - ] + if customer: + or_filters.append( + ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] + ) + if service_level_agreement: or_filters = [ ["Service Level Agreement", "name", "=", service_level_agreement], @@ -139,33 +144,31 @@ def get_active_service_level_agreement_for(priority, customer=None, service_leve return agreement[0] if agreement else None def get_customer_group(customer): - if customer: - return frappe.db.get_value("Customer", customer, "customer_group") + return frappe.db.get_value("Customer", customer, "customer_group") if customer else None def get_customer_territory(customer): - if customer: - return frappe.db.get_value("Customer", customer, "territory") + return frappe.db.get_value("Customer", customer, "territory") if customer else None @frappe.whitelist() -def get_service_level_agreement_filters(name, customer=None): +def get_service_level_agreement_filters(doctype, name, customer=None): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): return filters = [ + ["Service Level Agreement", "document_type", "=", doctype], ["Service Level Agreement", "active", "=", 1], ["Service Level Agreement", "enable", "=", 1] ] - if not customer: - or_filters = [ - ["Service Level Agreement", "default_service_level_agreement", "=", 1] - ] - else: + or_filters = [ + ["Service Level Agreement", "default_service_level_agreement", "=", 1] + ] + + if customer: # Include SLA with No Entity and Entity Type - or_filters = [ - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]], - ["Service Level Agreement", "default_service_level_agreement", "=", 1] - ] + or_filters.append( + ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]] + ) return { "priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])], @@ -182,3 +185,324 @@ def get_repeated(values): if value not in diff: diff.append(str(value)) return " ".join(diff) + +def apply(doc, method=None): + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard: + return + + service_level_agreement = get_active_service_level_agreement_for(doctype=doc.get("doctype"), priority=doc.get("priority"), + customer=doc.get("customer"), service_level_agreement=doc.get("service_level_agreement")) + + if not service_level_agreement: + return + + meta = frappe.get_meta(doc.doctype) + from_db = frappe._dict({}) if doc.is_new() else frappe.get_doc(doc.doctype, doc.name) + + if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \ + not service_level_agreement.customer == doc.get("customer"): + + frappe.throw(_("Service Level Agreement {0} is specific to Customer {1}").format(service_level_agreement.name, + service_level_agreement.customer)) + + doc.service_level_agreement = service_level_agreement.name + doc.priority = doc.priority or service_level_agreement.default_priority + priority = get_priority(doc) + + if not doc.creation: + doc.creation = now_datetime() + + if meta.has_field("service_level_agreement_creation"): + doc.service_level_agreement_creation = now_datetime() + + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + + set_response_by_and_variance(doc, meta, start_date_time, priority) + set_resolution_by_and_variance(doc, meta, start_date_time, priority) + + if meta.has_field("status") and meta.has_field("first_responded_on") and doc.status != "Open" and \ + from_db.status == "Open" and not doc.first_responded_on: + + doc.first_responded_on = frappe.flags.current_time or now_datetime() + + if meta.has_field("status") and doc.status in ["Closed", "Resolved"] and from_db.status not in ["Closed", "Resolved"]: + if meta.has_field("resolution_date"): + doc.resolution_date = frappe.flags.current_time or now_datetime() + + if meta.has_field("agreement_fulfilled") and from_db.agreement_fulfilled == "Ongoing": + set_service_level_agreement_variance(doc.doctype, doc.name) + update_agreement_status(doc, meta) + + set_resolution_time(doc, meta) + set_user_resolution_time(doc, meta) + + if meta.has_field("status") and doc.status == "Open" and not from_db.status == "Open": + # if no date, it should be set as None and not a blank string "", as per mysql strict config + # enable SLA and variance on Reopen + reset_metrics(doc, meta) + set_service_level_agreement_variance(doc.doctype, doc.name) + + # handle_hold_time(doc, meta, status) + +def get_expected_time_for(parameter, service_level, start_date_time): + current_date_time = start_date_time + expected_time = current_date_time + start_time = None + end_time = None + + if parameter == "response": + allotted_seconds = service_level.get("response_time") + elif parameter == "resolution": + allotted_seconds = service_level.get("resolution_time") + else: + frappe.throw(_("{0} parameter is invalid").format(parameter)) + + expected_time_is_set = 0 + + support_days = {} + for service in service_level.get("support_and_resolution"): + support_days[service.workday] = frappe._dict({ + "start_time": service.start_time, + "end_time": service.end_time, + }) + + holidays = get_holidays(service_level.get("holiday_list")) + weekdays = get_weekdays() + + while not expected_time_is_set: + current_weekday = weekdays[current_date_time.weekday()] + + if not is_holiday(current_date_time, holidays) and current_weekday in support_days: + start_time = current_date_time - datetime(current_date_time.year, current_date_time.month, current_date_time.day) \ + 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_seconds(end_time, start_time) + + # no time left for support today + 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, seconds=allotted_seconds) + expected_time_is_set = 1 + else: + 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 allotted_seconds >= 86400: + current_date_time = datetime.combine(getdate(current_date_time), get_time(end_time)) + else: + current_date_time = expected_time + + return current_date_time + +def set_service_level_agreement_variance(doctype, doc=None): + current_time = frappe.flags.current_time or now_datetime() + + filters = {"status": "Open", "agreement_fulfilled": "Ongoing"} + + if doc: + filters = {"name": doc} + + for doc in frappe.get_list(doctype, filters=filters): + doc = frappe.get_doc(doctype, doc.name) + + if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer + variance = round(time_diff_in_hours(doc.response_by, current_time), 2) + frappe.db.set_value(doc.doctype, doc.name, "response_by_variance", variance, update_modified=False) + + if variance < 0: + frappe.db.set_value(doc.doctype, doc.name, "agreement_fulfilled", "Failed", update_modified=False) + + if not doc.resolution_date: # resolution_date set when issue has been closed + variance = round(time_diff_in_hours(doc.resolution_by, current_time), 2) + frappe.db.set_value(doc.doctype, doc.name, "resolution_by_variance", variance, update_modified=False) + + if variance < 0: + frappe.db.set_value(doc.doctype, doc.name, "agreement_fulfilled", "Failed", update_modified=False) + +def set_user_resolution_time(doc, meta): + # total time taken by a user to close the issue apart from wait_time + if not meta.has_field("user_resolution_time"): + return + + communications = frappe.get_list("Communication", filters={ + "reference_doctype": doc.doctype, + "reference_name": doc.name + }, fields=["sent_or_received", "name", "creation"], order_by="creation") + + pending_time = [] + for i in range(len(communications)): + if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent": + wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) + if wait_time > 0: + pending_time.append(wait_time) + + total_pending_time = sum(pending_time) + resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, doc.creation) + doc.user_resolution_time = resolution_time_in_secs - total_pending_time + +def change_service_level_agreement_and_priority(self): + if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \ + frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): + + if not self.priority == frappe.db.get_value("Issue", self.name, "priority"): + self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + frappe.msgprint(_("Priority has been changed to {0}.").format(self.priority)) + + if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"): + self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) + +def get_priority(doc): + service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) + priority = service_level_agreement.get_service_level_agreement_priority(doc.priority) + priority.update({ + "support_and_resolution": service_level_agreement.support_and_resolution, + "holiday_list": service_level_agreement.holiday_list + }) + return priority + +def reset_service_level_agreement(doc, reason, user): + if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): + frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) + + frappe.get_doc({ + "doctype": "Comment", + "comment_type": "Info", + "reference_doctype": doc.doctype, + "reference_name": doc.name, + "comment_email": user, + "content": " resetted Service Level Agreement - {0}".format(_(reason)), + }).insert(ignore_permissions=True) + + doc.service_level_agreement_creation = now_datetime() + doc.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + doc.agreement_fulfilled = "Ongoing" + doc.save() + +def reset_metrics(doc, meta): + + if meta.has_field("resolution_date"): + doc.resolution_date = None + + if not meta.has_field("resolution_time"): + doc.resolution_time = None + + if not meta.has_field("user_resolution_time"): + doc.user_resolution_time = None + + if meta.has_field("agreement_fulfilled"): + doc.agreement_fulfilled = "Ongoing" + +def set_resolution_time(doc, meta): + # total time taken from issue creation to closing + if not meta.has_field("resolution_time"): + return + + doc.resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation) + +def handle_hold_time(doc, meta, status): + if meta.has_field("service_level_agreement") and doc.service_level_agreement: + # set response and resolution variance as None as the issue is on Hold for status as Replied + hold_statuses = [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={ + "parent": doc.service_level_agreement + }, fields=["status"])] + + update_values = {} + + if doc.status in hold_statuses and status not in hold_statuses: + update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() + if not doc.first_responded_on: + update_values['response_by'] = None + update_values['response_by_variance'] = 0 + update_values['resolution_by'] = None + update_values['resolution_by_variance'] = 0 + + # calculate hold time when status is changed from Replied to any other status + if doc.status not in hold_statuses and status in hold_statuses: + hold_time = doc.total_hold_time if doc.total_hold_time else 0 + now_time = frappe.flags.current_time or now_datetime() + update_values['total_hold_time'] = hold_time + time_diff_in_seconds(now_time, doc.on_hold_since) + + # re-calculate SLA variables after issue changes from Replied to Open + # add hold time to SLA variables + if doc.status == "Open" and status in hold_statuses: + start_date_time = get_datetime(doc.service_level_agreement_creation) + priority = get_priority(doc) + now_time = frappe.flags.current_time or now_datetime() + hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) + + if not doc.first_responded_on: + response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + update_values['response_by'] = add_to_date(response_by, seconds=round(hold_time)) + response_by_variance = round(time_diff_in_hours(doc.response_by, now_time)) + update_values['response_by_variance'] = response_by_variance + (hold_time // 3600) + + resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + update_values['resolution_by'] = add_to_date(resolution_by, seconds=round(hold_time)) + resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_time)) + update_values['resolution_by_variance'] = resolution_by_variance + (hold_time // 3600) + update_values['on_hold_since'] = None + + doc.db_set(update_values) + +def update_agreement_fulfilled_on_custom_status(doc): + """ + Update Agreement Fulfilled status using Custom Scripts for Custom Status + """ + meta = frappe.get_meta(doc.doctype) + if meta.has_field("first_responded_on") and not doc.first_responded_on: + # first_responded_on set when first reply is sent to customer + doc.response_by_variance = round(time_diff_in_hours(doc.response_by, now_datetime()), 2) + + if meta.has_field("resolution_date") and not doc.resolution_date: + # resolution_date set when issue has been closed + doc.resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_datetime()), 2) + + if meta.has_field("agreement_fulfilled"): + doc.agreement_fulfilled = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" + +def update_agreement_status(doc, meta): + + if meta.had_field("service_level_agreement") and meta.had_field("agreement_fulfilled") and \ + doc.service_level_agreement and self.agreement_fulfilled == "Ongoing": + + if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \ + frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0: + + self.agreement_fulfilled = "Failed" + else: + self.agreement_fulfilled = "Fulfilled" + +def get_holidays(holiday_list_name): + holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name) + holidays = [holiday.holiday_date for holiday in holiday_list.holidays] + return holidays + +def is_holiday(date, holidays): + return getdate(date) in holidays + +def get_time_in_timedelta(time): + """ + Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215) + """ + import datetime + return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) + +def set_response_by_and_variance(doc, meta, start_date_time, priority): + if meta.has_field("response_by"): + doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + + if meta.has_field("response_by_variance"): + doc.response_by_variance = round(time_diff_in_hours(doc.response_by, now_datetime())) + +def set_resolution_by_and_variance(doc, meta, start_date_time, priority): + if meta.has_field("resolution_by"): + doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + + if meta.has_field("resolution_by_variance"): + doc.resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_datetime())) \ No newline at end of file From 926c112091e6801560c760c5a26e819df21ed89f Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Thu, 25 Jun 2020 15:30:03 +0530 Subject: [PATCH 02/49] fix: set hold time --- erpnext/support/doctype/issue/issue.py | 6 + .../service_level_agreement.py | 127 +++++++++++------- 2 files changed, 83 insertions(+), 50 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 3ff1f5c66c3a..df4f10e3ea6c 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -155,6 +155,7 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord @frappe.whitelist() def set_multiple_status(names, status): + for name in json.loads(names): frappe.db.set_value("Issue", name, "status", status) @@ -210,3 +211,8 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals link_communication_to_document(doc, "Issue", issue.name, ignore_communication_links) return issue.name + +def get_holidays(holiday_list_name): + holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name) + holidays = [holiday.holiday_date for holiday in holiday_list.holidays] + return holidays \ No newline at end of file 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 754c0e0c933a..836d0d107724 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -8,6 +8,7 @@ from frappe import _ 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 erpnext.support.doctype.issue.issue import get_holidays class ServiceLevelAgreement(Document): @@ -175,6 +176,7 @@ def get_service_level_agreement_filters(doctype, name, customer=None): "service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)] } + def get_repeated(values): unique_list = [] diff = [] @@ -186,7 +188,11 @@ def get_repeated(values): diff.append(str(value)) return " ".join(diff) + def apply(doc, method=None): + """ + Applies SLA to document on validate + """ if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard: return @@ -220,6 +226,10 @@ def apply(doc, method=None): set_response_by_and_variance(doc, meta, start_date_time, priority) set_resolution_by_and_variance(doc, meta, start_date_time, priority) + update_status(doc, from_db, meta) + + +def update_status(doc, from_db, meta): if meta.has_field("status") and meta.has_field("first_responded_on") and doc.status != "Open" and \ from_db.status == "Open" and not doc.first_responded_on: @@ -231,7 +241,7 @@ def apply(doc, method=None): if meta.has_field("agreement_fulfilled") and from_db.agreement_fulfilled == "Ongoing": set_service_level_agreement_variance(doc.doctype, doc.name) - update_agreement_status(doc, meta) + update_agreement_status(doc, from_db, meta) set_resolution_time(doc, meta) set_user_resolution_time(doc, meta) @@ -242,7 +252,7 @@ def apply(doc, method=None): reset_metrics(doc, meta) set_service_level_agreement_variance(doc.doctype, doc.name) - # handle_hold_time(doc, meta, status) + handle_hold_time(doc, meta, from_db.status) def get_expected_time_for(parameter, service_level, start_date_time): current_date_time = start_date_time @@ -299,6 +309,7 @@ def get_expected_time_for(parameter, service_level, start_date_time): return current_date_time + def set_service_level_agreement_variance(doctype, doc=None): current_time = frappe.flags.current_time or now_datetime() @@ -324,6 +335,7 @@ def set_service_level_agreement_variance(doctype, doc=None): if variance < 0: frappe.db.set_value(doc.doctype, doc.name, "agreement_fulfilled", "Failed", update_modified=False) + def set_user_resolution_time(doc, meta): # total time taken by a user to close the issue apart from wait_time if not meta.has_field("user_resolution_time"): @@ -345,6 +357,7 @@ def set_user_resolution_time(doc, meta): resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, doc.creation) doc.user_resolution_time = resolution_time_in_secs - total_pending_time + def change_service_level_agreement_and_priority(self): if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \ frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): @@ -357,6 +370,7 @@ def change_service_level_agreement_and_priority(self): self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) + def get_priority(doc): service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) priority = service_level_agreement.get_service_level_agreement_priority(doc.priority) @@ -366,6 +380,7 @@ def get_priority(doc): }) return priority + def reset_service_level_agreement(doc, reason, user): if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) @@ -384,8 +399,8 @@ def reset_service_level_agreement(doc, reason, user): doc.agreement_fulfilled = "Ongoing" doc.save() -def reset_metrics(doc, meta): +def reset_metrics(doc, meta): if meta.has_field("resolution_date"): doc.resolution_date = None @@ -398,12 +413,14 @@ def reset_metrics(doc, meta): if meta.has_field("agreement_fulfilled"): doc.agreement_fulfilled = "Ongoing" + def set_resolution_time(doc, meta): # total time taken from issue creation to closing if not meta.has_field("resolution_time"): return - doc.resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation) + doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) + def handle_hold_time(doc, meta, status): if meta.has_field("service_level_agreement") and doc.service_level_agreement: @@ -414,41 +431,52 @@ def handle_hold_time(doc, meta, status): update_values = {} - if doc.status in hold_statuses and status not in hold_statuses: - update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() - if not doc.first_responded_on: - update_values['response_by'] = None - update_values['response_by_variance'] = 0 - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 - - # calculate hold time when status is changed from Replied to any other status - if doc.status not in hold_statuses and status in hold_statuses: - hold_time = doc.total_hold_time if doc.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime() - update_values['total_hold_time'] = hold_time + time_diff_in_seconds(now_time, doc.on_hold_since) - - # re-calculate SLA variables after issue changes from Replied to Open - # add hold time to SLA variables - if doc.status == "Open" and status in hold_statuses: - start_date_time = get_datetime(doc.service_level_agreement_creation) - priority = get_priority(doc) - now_time = frappe.flags.current_time or now_datetime() - hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) - - if not doc.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - update_values['response_by'] = add_to_date(response_by, seconds=round(hold_time)) - response_by_variance = round(time_diff_in_hours(doc.response_by, now_time)) - update_values['response_by_variance'] = response_by_variance + (hold_time // 3600) - - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - update_values['resolution_by'] = add_to_date(resolution_by, seconds=round(hold_time)) - resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_time)) - update_values['resolution_by_variance'] = resolution_by_variance + (hold_time // 3600) - update_values['on_hold_since'] = None - - doc.db_set(update_values) + if hold_statuses: + if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses: + update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() + + if meta.has_field("first_responded_on") and not doc.first_responded_on: + update_values['response_by'] = None + update_values['response_by_variance'] = 0 + + update_values['resolution_by'] = None + update_values['resolution_by_variance'] = 0 + # calculate hold time when status is changed from any hold status to any non-hold status + + if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses: + hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0 + now_time = frappe.flags.current_time or now_datetime() + last_hold_time = 0 + + if meta.has_field("on_hold_since") and doc.on_hold_since: + # last_hold_time will be added to the sla variables + last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) + update_values['total_hold_time'] = hold_time + last_hold_time + + # re-calculate SLA variables after issue changes from any hold status to any non-hold status + # add hold time to SLA variables + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + priority = get_priority(doc) + now_time = frappe.flags.current_time or now_datetime() + + if meta.has_field("first_responded_on") and not doc.first_responded_on: + response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + response_by = add_to_date(response_by, seconds=round(last_hold_time)) + response_by_variance = round(time_diff_in_hours(response_by, now_time)) + + update_values['response_by'] = response_by + update_values['response_by_variance'] = response_by_variance + (last_hold_time // 3600) + + resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) + resolution_by_variance = round(time_diff_in_hours(resolution_by, now_time)) + + update_values['resolution_by'] = resolution_by + update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600) + update_values['on_hold_since'] = None + + doc.update(update_values) + def update_agreement_fulfilled_on_custom_status(doc): """ @@ -466,26 +494,23 @@ def update_agreement_fulfilled_on_custom_status(doc): if meta.has_field("agreement_fulfilled"): doc.agreement_fulfilled = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" -def update_agreement_status(doc, meta): - if meta.had_field("service_level_agreement") and meta.had_field("agreement_fulfilled") and \ - doc.service_level_agreement and self.agreement_fulfilled == "Ongoing": +def update_agreement_status(doc, from_db, meta): + if meta.has_field("service_level_agreement") and meta.has_field("agreement_fulfilled") and \ + doc.service_level_agreement and doc.agreement_fulfilled == "Ongoing": - if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \ - frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0: + if (meta.has_field("response_by_variance") and from_db.response_by_variance < 0) or \ + (meta.has_field("resolution_by_variance") and from_db.resolution_by_variance < 0): - self.agreement_fulfilled = "Failed" + doc.agreement_fulfilled = "Failed" else: - self.agreement_fulfilled = "Fulfilled" + doc.agreement_fulfilled = "Fulfilled" -def get_holidays(holiday_list_name): - holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name) - holidays = [holiday.holiday_date for holiday in holiday_list.holidays] - return holidays def is_holiday(date, holidays): return getdate(date) in holidays + def get_time_in_timedelta(time): """ Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215) @@ -493,6 +518,7 @@ def get_time_in_timedelta(time): import datetime return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) + def set_response_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) @@ -500,6 +526,7 @@ def set_response_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("response_by_variance"): doc.response_by_variance = round(time_diff_in_hours(doc.response_by, now_datetime())) + def set_resolution_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) From 10eab1ed171e1a866c5152325fe99f2da5fad95f Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Thu, 25 Jun 2020 17:05:03 +0530 Subject: [PATCH 03/49] fix: remove unwanted fields --- .../service_level_agreement.json | 51 ++++++++----------- .../service_level_agreement.py | 32 +++++++++--- .../test_service_level_agreement.py | 28 +++++----- 3 files changed, 57 insertions(+), 54 deletions(-) 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 e047aa908ccc..50d68f822023 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -6,11 +6,10 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "enable", + "enabled", "section_break_2", "document_type", "default_service_level_agreement", - "employee_group", "default_priority", "column_break_2", "service_level", @@ -21,10 +20,10 @@ "entity", "agreement_details_section", "start_date", - "active", "column_break_7", "end_date", "section_break_18", + "pause_sla_on_section", "pause_sla_on", "response_and_resolution_time_section", "priorities", @@ -37,7 +36,7 @@ "fieldtype": "Data", "in_list_view": 1, "in_standard_filter": 1, - "label": "Service Level", + "label": "Service Level Name", "reqd": 1 }, { @@ -52,20 +51,12 @@ "fieldtype": "Column Break" }, { - "fieldname": "employee_group", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Employee Group", - "options": "Employee Group" - }, - { + "depends_on": "eval: !doc.default_service_level_agreement", "fieldname": "agreement_details_section", "fieldtype": "Section Break", "label": "Agreement Details" }, { - "depends_on": "eval: !doc.default_service_level_agreement", "fieldname": "start_date", "fieldtype": "Date", "label": "Start Date" @@ -82,13 +73,11 @@ "label": "End Date" }, { - "collapsible": 1, "fieldname": "response_and_resolution_time_section", "fieldtype": "Section Break", "label": "Response and Resolution Time" }, { - "collapsible": 1, "fieldname": "support_and_resolution_section_break", "fieldtype": "Section Break", "label": "Support Hours" @@ -107,13 +96,6 @@ "options": "Service Level Priority", "reqd": 1 }, - { - "default": "1", - "fieldname": "active", - "fieldtype": "Check", - "label": "Active", - "read_only": 1 - }, { "fieldname": "column_break_10", "fieldtype": "Column Break" @@ -139,15 +121,10 @@ "label": "Entity Type", "options": "\nCustomer\nCustomer Group\nTerritory" }, - { - "default": "1", - "fieldname": "enable", - "fieldtype": "Check", - "label": "Enable" - }, { "fieldname": "section_break_2", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { "default": "0", @@ -177,11 +154,23 @@ "fieldname": "document_type", "fieldtype": "Link", "label": "Document Type", - "options": "DocType" + "options": "DocType", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "pause_sla_on_section", + "fieldtype": "Section Break", + "label": "Pause SLA On" } ], "links": [], - "modified": "2020-06-23 17:25:17.766570", + "modified": "2020-06-25 17:02:37.864016", "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 836d0d107724..72a5c2f37977 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -101,16 +101,27 @@ def get_service_level_agreement_priority(self, priority): "resolution_time": priority.resolution_time }) + def after_insert(self): + self.set_documents_with_active_service_level_agreement() + + def on_trash(self): + self.set_documents_with_active_service_level_agreement() + + def set_documents_with_active_service_level_agreement(self): + frappe.cache().hset("service_level_agreement", "active", [sla.name for sla in frappe.get_all("Service Level Agreement")]) + + def check_agreement_status(): service_level_agreements = frappe.get_list("Service Level Agreement", filters=[ - {"active": 1}, + {"enabled": 1}, {"default_service_level_agreement": 0} ], fields=["name"]) for service_level_agreement in service_level_agreements: doc = frappe.get_doc("Service Level Agreement", service_level_agreement.name) if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()): - frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0) + frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "enabled", 0) + def get_active_service_level_agreement_for(doctype, priority, customer=None, service_level_agreement=None): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): @@ -118,8 +129,7 @@ def get_active_service_level_agreement_for(doctype, priority, customer=None, ser filters = [ ["Service Level Agreement", "document_type", "=", doctype], - ["Service Level Agreement", "active", "=", 1], - ["Service Level Agreement", "enable", "=", 1] + ["Service Level Agreement", "enabled", "=", 1] ] or_filters = [] @@ -144,12 +154,15 @@ def get_active_service_level_agreement_for(doctype, priority, customer=None, ser return agreement[0] if agreement else None + def get_customer_group(customer): return frappe.db.get_value("Customer", customer, "customer_group") if customer else None + def get_customer_territory(customer): return frappe.db.get_value("Customer", customer, "territory") if customer else None + @frappe.whitelist() def get_service_level_agreement_filters(doctype, name, customer=None): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): @@ -157,8 +170,7 @@ def get_service_level_agreement_filters(doctype, name, customer=None): filters = [ ["Service Level Agreement", "document_type", "=", doctype], - ["Service Level Agreement", "active", "=", 1], - ["Service Level Agreement", "enable", "=", 1] + ["Service Level Agreement", "enabled", "=", 1] ] or_filters = [ @@ -189,11 +201,17 @@ def get_repeated(values): return " ".join(diff) +def get_documents_with_active_service_level_agreement(): + return frappe.cache().hget("service_level_agreement", "active") or [] + + def apply(doc, method=None): """ Applies SLA to document on validate """ - if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard: + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ + not doc.doctype in get_documents_with_active_service_level_agreement(): + return service_level_agreement = get_active_service_level_agreement_for(doctype=doc.get("doctype"), priority=doc.get("priority"), 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 07ef368cbe3d..dc318ac1b658 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,8 +16,7 @@ def setUp(self): 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", - entity_type=None, entity=None, response_time=14400, resolution_time=21600) + holiday_list="__Test Holiday List", 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,8 +28,8 @@ def test_service_level_agreement(self): # Service Level Agreement for Customer 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=7200, resolution_time=10800) + holiday_list="__Test Holiday List", 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,8 +40,8 @@ def test_service_level_agreement(self): # 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, - holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity=customer_group, response_time=7200, resolution_time=10800) + holiday_list="__Test Holiday List", 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 +52,7 @@ def test_service_level_agreement(self): # Service Level Agreement for Territory 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", + holiday_list="__Test Holiday List", 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) @@ -62,7 +61,6 @@ def test_service_level_agreement(self): self.assertEqual(create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity) self.assertEqual(create_territory_service_level_agreement.default_service_level_agreement, get_territory_service_level_agreement.default_service_level_agreement) - def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None): if default_service_level_agreement: filters = {"default_service_level_agreement": default_service_level_agreement} @@ -72,10 +70,9 @@ 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, holiday_list, employee_group, - response_time, entity_type, entity, resolution_time): +def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type, + entity, resolution_time): - employee_group = make_employee_group() make_holiday_list() make_priorities() @@ -86,7 +83,6 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "default_service_level_agreement": default_service_level_agreement, "default_priority": "Medium", "holiday_list": holiday_list, - "employee_group": employee_group, "entity_type": entity_type, "entity": entity, "start_date": frappe.utils.getdate(), @@ -219,19 +215,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=14400, resolution_time=21600) + 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=7200, resolution_time=10800) + 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=7200, resolution_time=10800) + 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=7200, resolution_time=10800) + 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 a0932d75a78960cb51de138d4919527d53b5df46 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 26 Jun 2020 13:17:30 +0530 Subject: [PATCH 04/49] fix: add custom fields --- .../service_level_agreement.js | 11 +++ .../service_level_agreement.json | 4 +- .../service_level_agreement.py | 83 +++++++++++++++++++ 3 files changed, 96 insertions(+), 2 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 5346195a396a..db9163a791eb 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -12,5 +12,16 @@ frappe.ui.form.on('Service Level Agreement', { 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); }); + }, + onload: function(frm) { + frm.set_query("document_type", function() { + return { + filters: [ + ['DocType', 'issingle', '=', 0], + ['DocType', 'name', 'not in', frappe.model.core_doctypes_list], + ['DocType', 'module', 'not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] + ] + }; + }); } }); \ 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 50d68f822023..043baacf47ae 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "format:SLA-{service_level}-{####}", + "autoname": "format:SLA-{document_type}-{service_level}", "creation": "2018-12-26 21:08:15.448812", "doctype": "DocType", "editable_grid": 1, @@ -170,7 +170,7 @@ } ], "links": [], - "modified": "2020-06-25 17:02:37.864016", + "modified": "2020-06-26 11:05:23.188537", "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 72a5c2f37977..5fbcd53ea84c 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -103,6 +103,7 @@ def get_service_level_agreement_priority(self, priority): def after_insert(self): self.set_documents_with_active_service_level_agreement() + self.create_custom_fields() def on_trash(self): self.set_documents_with_active_service_level_agreement() @@ -110,6 +111,88 @@ def on_trash(self): def set_documents_with_active_service_level_agreement(self): frappe.cache().hset("service_level_agreement", "active", [sla.name for sla in frappe.get_all("Service Level Agreement")]) + def create_custom_fields(self): + meta = frappe.get_meta(self.document_type) + + service_level_agreement_fields = [ + { + "collapsible": 1, + "fieldname": "service_level_section", + "fieldtype": "Section Break", + "label": "Service Level" + }, + { + "fieldname": "service_level_agreement", + "fieldtype": "Link", + "label": "Service Level Agreement", + "options": "Service Level Agreement" + }, + { + "fieldname": "response_by", + "fieldtype": "Datetime", + "label": "Response By", + "read_only": 1 + }, + { + "description": "in hours", + "fieldname": "response_by_variance", + "fieldtype": "Float", + "label": "Response By Variance", + "read_only": 1 + }, + { + "fieldname": "cb", + "fieldtype": "Column Break", + "read_only": 1 + }, + { + "default": "Ongoing", + "fieldname": "agreement_fulfilled", + "fieldtype": "Select", + "label": "Service Level Agreement Fulfilled", + "options": "Ongoing\nFulfilled\nFailed", + "read_only": 1 + }, + { + "fieldname": "resolution_by", + "fieldtype": "Datetime", + "label": "Resolution By", + "read_only": 1 + }, + { + "description": "in hours", + "fieldname": "resolution_by_variance", + "fieldtype": "Float", + "label": "Resolution By Variance", + "read_only": 1 + }, + { + "fieldname": "service_level_agreement_creation", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Service Level Agreement Creation", + "read_only": 1 + } + ] + + for field in service_level_agreement_fields: + if not meta.has_field(field.get("fieldname")): + frappe.get_doc({ + "doctype": "Custom Field", + "dt": self.document_type, + "label": field.get("label"), + "fieldname": field.get("fieldname"), + "fieldtype": field.get("fieldtype"), + "insert_after": "append", + "collapsible": field.get("collapsible"), + "hidden": field.get("hidden"), + "options": field.get("options"), + "read_only": field.get("read_only"), + "hidden": field.get("hidden"), + "description": field.get("description"), + "default": field.get("default"), + }).insert(ignore_permissions=True) + def check_agreement_status(): service_level_agreements = frappe.get_list("Service Level Agreement", filters=[ From 36972a4d89dcefe8f5b3e0af2f4de0a8f0bd0329 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 26 Jun 2020 14:30:46 +0530 Subject: [PATCH 05/49] fix: remove unused imports --- erpnext/support/doctype/issue/issue.py | 2 +- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- .../service_level_agreement/test_service_level_agreement.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index df4f10e3ea6c..1fd880a582b9 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -7,7 +7,7 @@ from frappe import _ from frappe import utils from frappe.model.document import Document -from frappe.utils import 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 now_datetime 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 5fbcd53ea84c..7071c6c08d89 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,7 +6,7 @@ import frappe from frappe.model.document import Document from frappe import _ -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 frappe.utils import time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, get_time, get_datetime, time_diff_in_seconds from datetime import datetime, timedelta from erpnext.support.doctype.issue.issue import get_holidays 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 dc318ac1b658..f630d17528f0 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 @@ -78,7 +78,8 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list service_level_agreement = frappe.get_doc({ "doctype": "Service Level Agreement", - "enable": 1, + "enabled": 1, + "document_type": "Issue", "service_level": "__Test Service Level", "default_service_level_agreement": default_service_level_agreement, "default_priority": "Medium", From efd7ab2320fed923d3abd95ccef65964d0ab9ccc Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 26 Jun 2020 18:06:07 +0530 Subject: [PATCH 06/49] fix: ignore permissions --- .../service_level_agreement.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 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 7071c6c08d89..00b49fbe1abd 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -195,7 +195,7 @@ def create_custom_fields(self): def check_agreement_status(): - service_level_agreements = frappe.get_list("Service Level Agreement", filters=[ + service_level_agreements = frappe.get_all("Service Level Agreement", filters=[ {"enabled": 1}, {"default_service_level_agreement": 0} ], fields=["name"]) @@ -232,7 +232,7 @@ def get_active_service_level_agreement_for(doctype, priority, customer=None, ser or_filters.append(["Service Level Agreement", "default_service_level_agreement", "=", 1]) - agreement = frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters, + agreement = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters, fields=["name", "default_priority"]) return agreement[0] if agreement else None @@ -267,8 +267,8 @@ def get_service_level_agreement_filters(doctype, 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)] + "priority": [priority.priority for priority in frappe.get_all("Service Level Priority", filters={"parent": name}, fields=["priority"])], + "service_level_agreements": [d.name for d in frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters)] } @@ -419,7 +419,7 @@ def set_service_level_agreement_variance(doctype, doc=None): if doc: filters = {"name": doc} - for doc in frappe.get_list(doctype, filters=filters): + for doc in frappe.get_all(doctype, filters=filters): doc = frappe.get_doc(doctype, doc.name) if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer @@ -442,7 +442,7 @@ def set_user_resolution_time(doc, meta): if not meta.has_field("user_resolution_time"): return - communications = frappe.get_list("Communication", filters={ + communications = frappe.get_all("Communication", filters={ "reference_doctype": doc.doctype, "reference_name": doc.name }, fields=["sent_or_received", "name", "creation"], order_by="creation") From 748163cc5d3928e80858eaace999858a66c08bdd Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Tue, 30 Jun 2020 16:59:18 +0530 Subject: [PATCH 07/49] fix: sla not being applied --- erpnext/patches/v13_0/add_doctype_to_sla.py | 12 +++ .../service_level_agreement.py | 92 +++++++++++++++---- 2 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 erpnext/patches/v13_0/add_doctype_to_sla.py diff --git a/erpnext/patches/v13_0/add_doctype_to_sla.py b/erpnext/patches/v13_0/add_doctype_to_sla.py new file mode 100644 index 000000000000..65fb82cb7649 --- /dev/null +++ b/erpnext/patches/v13_0/add_doctype_to_sla.py @@ -0,0 +1,12 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + frappe.reload_doc('support', 'doctype', 'service_level_agreement') + + for sla in frappe.get_all("Service Level Agreement"): + frappe.db.set_value("Issue", sla.name, "document_type", "Issue") \ No newline at end of file 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 00b49fbe1abd..5227b2a81f7d 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -101,19 +101,7 @@ def get_service_level_agreement_priority(self, priority): "resolution_time": priority.resolution_time }) - def after_insert(self): - self.set_documents_with_active_service_level_agreement() - self.create_custom_fields() - - def on_trash(self): - self.set_documents_with_active_service_level_agreement() - - def set_documents_with_active_service_level_agreement(self): - frappe.cache().hset("service_level_agreement", "active", [sla.name for sla in frappe.get_all("Service Level Agreement")]) - - def create_custom_fields(self): - meta = frappe.get_meta(self.document_type) - + def before_insert(self): service_level_agreement_fields = [ { "collapsible": 1, @@ -172,9 +160,57 @@ def create_custom_fields(self): "hidden": 1, "label": "Service Level Agreement Creation", "read_only": 1 - } + }, + { + "fieldname": "priority", + "fieldtype": "Link", + "label": "Priority", + "options": "Issue Priority" + }, ] + meta = frappe.get_meta(self.document_type) + + if meta.custom: + self.create_docfields(meta, service_level_agreement_fields) + else: + self.create_custom_fields(meta, service_level_agreement_fields) + + def on_trash(self): + set_documents_with_active_service_level_agreement() + + def after_insert(self): + set_documents_with_active_service_level_agreement() + + def create_docfields(self, meta, service_level_agreement_fields): + last_index = len(meta.fields) + + for field in service_level_agreement_fields: + if not meta.has_field(field.get("fieldname")): + last_index += 1 + + frappe.get_doc({ + "doctype": "DocField", + "idx": last_index, + "parenttype": "DocType", + "parentfield": "fields", + "parent": self.document_type, + "label": field.get("label"), + "fieldname": field.get("fieldname"), + "fieldtype": field.get("fieldtype"), + "collapsible": field.get("collapsible"), + "hidden": field.get("hidden"), + "options": field.get("options"), + "read_only": field.get("read_only"), + "hidden": field.get("hidden"), + "description": field.get("description"), + "default": field.get("default"), + }).insert(ignore_permissions=True) + else: + existing_field = meta.get_field(field.get("fieldname")) + self.reset_field_properties(existing_field, field) + + def create_custom_fields(self, meta, service_level_agreement_fields): for field in service_level_agreement_fields: if not meta.has_field(field.get("fieldname")): frappe.get_doc({ @@ -192,7 +228,23 @@ def create_custom_fields(self): "description": field.get("description"), "default": field.get("default"), }).insert(ignore_permissions=True) - + else: + existing_field = meta.get_field(field.get("fieldname")) + self.reset_field_properties(existing_field, field) + + def reset_field_properties(self, field, sla_field): + field = frappe.get_doc(field.doctype, field.name) + field.label = sla_field.get("label") + field.fieldname = sla_field.get("fieldname") + field.fieldtype = sla_field.get("fieldtype") + field.collapsible = sla_field.get("collapsible") + field.hidden = sla_field.get("hidden") + field.options = sla_field.get("options") + field.read_only = sla_field.get("read_only") + field.hidden = sla_field.get("hidden") + field.description = sla_field.get("description") + field.default = sla_field.get("default") + field.save(ignore_permissions=True) def check_agreement_status(): service_level_agreements = frappe.get_all("Service Level Agreement", filters=[ @@ -285,7 +337,15 @@ def get_repeated(values): def get_documents_with_active_service_level_agreement(): - return frappe.cache().hget("service_level_agreement", "active") or [] + if not frappe.cache().hget("service_level_agreement", "active"): + set_documents_with_active_service_level_agreement() + + return frappe.cache().hget("service_level_agreement", "active") + + +def set_documents_with_active_service_level_agreement(): + active = [sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])] + frappe.cache().hset("service_level_agreement", "active", active) def apply(doc, method=None): From 1d4407fe99028ec3e7990e950f91594e8d9181c7 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 3 Jul 2020 13:07:15 +0530 Subject: [PATCH 08/49] fix: consider user tz for sla --- .../service_level_agreement.py | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 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 5227b2a81f7d..4a374d565c06 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,7 +6,7 @@ import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, get_time, get_datetime, time_diff_in_seconds +from frappe.utils import time_diff_in_hours, getdate, get_weekdays, add_to_date, get_time, get_datetime, time_diff_in_seconds, get_time_zone from datetime import datetime, timedelta from erpnext.support.doctype.issue.issue import get_holidays @@ -377,10 +377,10 @@ def apply(doc, method=None): priority = get_priority(doc) if not doc.creation: - doc.creation = now_datetime() + doc.creation = now_datetime(doc.get("owner")) if meta.has_field("service_level_agreement_creation"): - doc.service_level_agreement_creation = now_datetime() + doc.service_level_agreement_creation = now_datetime(doc.get("owner")) start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) @@ -394,11 +394,11 @@ def update_status(doc, from_db, meta): if meta.has_field("status") and meta.has_field("first_responded_on") and doc.status != "Open" and \ from_db.status == "Open" and not doc.first_responded_on: - doc.first_responded_on = frappe.flags.current_time or now_datetime() + doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) if meta.has_field("status") and doc.status in ["Closed", "Resolved"] and from_db.status not in ["Closed", "Resolved"]: if meta.has_field("resolution_date"): - doc.resolution_date = frappe.flags.current_time or now_datetime() + doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) if meta.has_field("agreement_fulfilled") and from_db.agreement_fulfilled == "Ongoing": set_service_level_agreement_variance(doc.doctype, doc.name) @@ -472,7 +472,6 @@ def get_expected_time_for(parameter, service_level, start_date_time): def set_service_level_agreement_variance(doctype, doc=None): - current_time = frappe.flags.current_time or now_datetime() filters = {"status": "Open", "agreement_fulfilled": "Ongoing"} @@ -481,6 +480,7 @@ def set_service_level_agreement_variance(doctype, doc=None): for doc in frappe.get_all(doctype, filters=filters): doc = frappe.get_doc(doctype, doc.name) + current_time = frappe.flags.current_time or now_datetime(doc.get("owner")) if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer variance = round(time_diff_in_hours(doc.response_by, current_time), 2) @@ -555,7 +555,7 @@ def reset_service_level_agreement(doc, reason, user): "content": " resetted Service Level Agreement - {0}".format(_(reason)), }).insert(ignore_permissions=True) - doc.service_level_agreement_creation = now_datetime() + doc.service_level_agreement_creation = now_datetime(doc.get("owner")) doc.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) doc.agreement_fulfilled = "Ongoing" doc.save() @@ -594,7 +594,7 @@ def handle_hold_time(doc, meta, status): if hold_statuses: if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses: - update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() + update_values['on_hold_since'] = frappe.flags.current_time or now_datetime(doc.get("owner")) if meta.has_field("first_responded_on") and not doc.first_responded_on: update_values['response_by'] = None @@ -606,7 +606,7 @@ def handle_hold_time(doc, meta, status): if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses: hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime() + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) last_hold_time = 0 if meta.has_field("on_hold_since") and doc.on_hold_since: @@ -618,7 +618,7 @@ def handle_hold_time(doc, meta, status): # add hold time to SLA variables start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) priority = get_priority(doc) - now_time = frappe.flags.current_time or now_datetime() + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) if meta.has_field("first_responded_on") and not doc.first_responded_on: response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) @@ -646,11 +646,11 @@ def update_agreement_fulfilled_on_custom_status(doc): meta = frappe.get_meta(doc.doctype) if meta.has_field("first_responded_on") and not doc.first_responded_on: # first_responded_on set when first reply is sent to customer - doc.response_by_variance = round(time_diff_in_hours(doc.response_by, now_datetime()), 2) + doc.response_by_variance = round(time_diff_in_hours(doc.response_by, now_datetime(doc.get("owner"))), 2) if meta.has_field("resolution_date") and not doc.resolution_date: # resolution_date set when issue has been closed - doc.resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_datetime()), 2) + doc.resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_datetime(doc.get("owner"))), 2) if meta.has_field("agreement_fulfilled"): doc.agreement_fulfilled = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" @@ -685,7 +685,7 @@ def set_response_by_and_variance(doc, meta, start_date_time, priority): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) if meta.has_field("response_by_variance"): - doc.response_by_variance = round(time_diff_in_hours(doc.response_by, now_datetime())) + doc.response_by_variance = round(time_diff_in_hours(doc.response_by, now_datetime(doc.get("owner")))) def set_resolution_by_and_variance(doc, meta, start_date_time, priority): @@ -693,4 +693,21 @@ def set_resolution_by_and_variance(doc, meta, start_date_time, priority): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) if meta.has_field("resolution_by_variance"): - doc.resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_datetime())) \ No newline at end of file + doc.resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_datetime(doc.get("owner")))) + +def now_datetime(user): + dt = convert_utc_to_user_timezone(datetime.utcnow(), user) + return dt.replace(tzinfo=None) + +def convert_utc_to_user_timezone(utc_timestamp, user): + from pytz import timezone, UnknownTimeZoneError + + user_tz = get_tz() + utcnow = timezone('UTC').localize(utc_timestamp) + try: + return utcnow.astimezone(timezone(user_tz)) + except UnknownTimeZoneError: + return utcnow + +def get_tz(user): + return frappe.db.get_value("User", user, "time_zone") or get_time_zone() From 2d94968dfcb38fe80b6e5acf5234d7c4636b7864 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 3 Jul 2020 14:05:08 +0530 Subject: [PATCH 09/49] fix: pass user as arg --- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4a374d565c06..1eaf9ca6c956 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -702,7 +702,7 @@ def now_datetime(user): def convert_utc_to_user_timezone(utc_timestamp, user): from pytz import timezone, UnknownTimeZoneError - user_tz = get_tz() + user_tz = get_tz(user) utcnow = timezone('UTC').localize(utc_timestamp) try: return utcnow.astimezone(timezone(user_tz)) From 08eace6a3be66dfb23b976f1c25f593c91270448 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Mon, 6 Jul 2020 14:57:00 +0530 Subject: [PATCH 10/49] fix: convert time str to timedelta --- .../doctype/service_day/service_day.json | 13 +++++++--- .../service_level_agreement.py | 26 +++---------------- .../service_level_priority.json | 15 ++++++----- 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/erpnext/support/doctype/service_day/service_day.json b/erpnext/support/doctype/service_day/service_day.json index 68614b18072e..966213099beb 100644 --- a/erpnext/support/doctype/service_day/service_day.json +++ b/erpnext/support/doctype/service_day/service_day.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-03-04 12:55:36.403035", "doctype": "DocType", "editable_grid": 1, @@ -16,7 +17,8 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Workday", - "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday" + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 }, { "fieldname": "section_break_2", @@ -26,7 +28,8 @@ "fieldname": "start_time", "fieldtype": "Time", "in_list_view": 1, - "label": "Start Time" + "label": "Start Time", + "reqd": 1 }, { "fieldname": "column_break_3", @@ -36,11 +39,13 @@ "fieldname": "end_time", "fieldtype": "Time", "in_list_view": 1, - "label": "End Time" + "label": "End Time", + "reqd": 1 } ], "istable": 1, - "modified": "2019-05-05 19:15:08.999579", + "links": [], + "modified": "2020-07-06 13:28:47.303873", "modified_by": "Administrator", "module": "Support", "name": "Service Day", 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 1eaf9ca6c956..9d8af25e4e9a 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,7 +6,7 @@ import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import time_diff_in_hours, getdate, get_weekdays, add_to_date, get_time, get_datetime, time_diff_in_seconds, get_time_zone +from frappe.utils import time_diff_in_hours, getdate, get_weekdays, add_to_date, get_time, get_datetime, time_diff_in_seconds, get_time_zone, to_timedelta from datetime import datetime, timedelta from erpnext.support.doctype.issue.issue import get_holidays @@ -18,23 +18,12 @@ def validate(self): self.check_support_and_resolution() def check_priorities(self): - default_priority = [] priorities = [] for priority in self.priorities: - # Check if response and resolution time is set for every priority - if not (priority.response_time or priority.resolution_time): - frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx)) - priorities.append(priority.priority) - if priority.default_priority: - default_priority.append(priority.default_priority) - - response = priority.response_time - resolution = priority.resolution_time - - if response > resolution: + if priority.response_time > priority.resolution_time: 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 @@ -42,10 +31,6 @@ def check_priorities(self): 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) @@ -57,15 +42,10 @@ def check_support_and_resolution(self): 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: + if to_timedelta(support_and_resolution.start_time) >= to_timedelta(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))) 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 65d51694cc3a..c166ebbc7fe9 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -15,12 +15,13 @@ ], "fields": [ { - "columns": 2, + "columns": 1, "fieldname": "priority", "fieldtype": "Link", "in_list_view": 1, "label": "Priority", - "options": "Issue Priority" + "options": "Issue Priority", + "reqd": 1 }, { "fieldname": "sb_00", @@ -33,7 +34,8 @@ "hide_days": 1, "hide_seconds": 1, "in_list_view": 1, - "label": "Resolution Time" + "label": "Resolution Time", + "reqd": 1 }, { "fieldname": "cb_00", @@ -58,12 +60,13 @@ "hide_days": 1, "hide_seconds": 1, "in_list_view": 1, - "label": "First Response Time" + "label": "First Response Time", + "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-06-10 12:45:47.545915", + "modified": "2020-07-06 13:29:20.183505", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", @@ -73,4 +76,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file From 086dbd8407e37dd6bb883e65af36adab984c0f15 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 8 Jul 2020 15:05:21 +0530 Subject: [PATCH 11/49] fix: add first responded on field --- .../service_level_agreement/service_level_agreement.py | 6 ++++++ 1 file changed, 6 insertions(+) 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 9d8af25e4e9a..13c407540378 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -147,6 +147,12 @@ def before_insert(self): "label": "Priority", "options": "Issue Priority" }, + { + "fieldname": "first_responded_on", + "fieldtype": "Datetime", + "label": "First Responded On", + "read_only": 1 + } ] meta = frappe.get_meta(self.document_type) From 2831187ac84617c26f2a4add1086baef244540d9 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 8 Jul 2020 15:11:02 +0530 Subject: [PATCH 12/49] fix: get timezone --- .../doctype/service_level_agreement/service_level_agreement.py | 1 + 1 file changed, 1 insertion(+) 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 13c407540378..cb263909b46e 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -695,5 +695,6 @@ def convert_utc_to_user_timezone(utc_timestamp, user): except UnknownTimeZoneError: return utcnow +@frappe.whitelist() def get_tz(user): return frappe.db.get_value("User", user, "time_zone") or get_time_zone() From d4d8ed5cfa6237ad830fc34feab953519e8bf0c2 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 8 Jul 2020 19:37:09 +0530 Subject: [PATCH 13/49] fix: helper function to get user time --- .../service_level_agreement/service_level_agreement.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 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 cb263909b46e..ab01cf5affa6 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,7 +6,8 @@ import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import time_diff_in_hours, getdate, get_weekdays, add_to_date, get_time, get_datetime, time_diff_in_seconds, get_time_zone, to_timedelta +from frappe.utils import time_diff_in_hours, getdate, get_weekdays, add_to_date, get_time, get_datetime, \ + time_diff_in_seconds, get_time_zone, to_timedelta, get_datetime_str from datetime import datetime, timedelta from erpnext.support.doctype.issue.issue import get_holidays @@ -695,6 +696,9 @@ def convert_utc_to_user_timezone(utc_timestamp, user): except UnknownTimeZoneError: return utcnow -@frappe.whitelist() def get_tz(user): return frappe.db.get_value("User", user, "time_zone") or get_time_zone() + +@frappe.whitelist() +def get_user_time(user, to_string=False): + return get_datetime_str(now_datetime(user)) if to_string else now_datetime(user) From e40b90ed67f7f7a165b4c8bf7f595097b5e51e41 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 10 Jul 2020 17:38:49 +0530 Subject: [PATCH 14/49] fix: set service_level_agreement_creation --- .../service_level_agreement/service_level_agreement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 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 ab01cf5affa6..97629cd3b565 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -366,8 +366,8 @@ def apply(doc, method=None): if not doc.creation: doc.creation = now_datetime(doc.get("owner")) - if meta.has_field("service_level_agreement_creation"): - doc.service_level_agreement_creation = now_datetime(doc.get("owner")) + if meta.has_field("service_level_agreement_creation"): + doc.service_level_agreement_creation = now_datetime(doc.get("owner")) start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) From 012e6f959d0ad4441a2c589461d78806647e9e5a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Jul 2020 17:48:55 +0530 Subject: [PATCH 15/49] fix: Codacy Issues --- .../service_level_agreement/service_level_agreement.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 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 40765527d5ea..b62c323171c6 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -8,7 +8,7 @@ from frappe import _ from frappe.utils import time_diff_in_hours, getdate, get_weekdays, add_to_date, get_time, get_datetime, \ time_diff_in_seconds, get_time_zone, to_timedelta, get_datetime_str, get_link_to_form -from datetime import datetime, timedelta +from datetime import datetime from erpnext.support.doctype.issue.issue import get_holidays class ServiceLevelAgreement(Document): @@ -197,7 +197,6 @@ def create_docfields(self, meta, service_level_agreement_fields): "fieldname": field.get("fieldname"), "fieldtype": field.get("fieldtype"), "collapsible": field.get("collapsible"), - "hidden": field.get("hidden"), "options": field.get("options"), "read_only": field.get("read_only"), "hidden": field.get("hidden"), @@ -347,9 +346,8 @@ def set_documents_with_active_service_level_agreement(): def apply(doc, method=None): - """ - Applies SLA to document on validate - """ + "Applies SLA to document on validate" + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ not doc.doctype in get_documents_with_active_service_level_agreement(): @@ -554,7 +552,7 @@ def reset_service_level_agreement(doc, reason, user): }).insert(ignore_permissions=True) doc.service_level_agreement_creation = now_datetime(doc.get("owner")) - doc.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=self.service_level_agreement) doc.agreement_fulfilled = "Ongoing" doc.save() From 0afd3d2dc2e5a81d1f1873c9f48806d636552b4a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Jul 2020 20:44:25 +0530 Subject: [PATCH 16/49] fix: Codacy issues and travis --- .../service_level_agreement.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 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 b62c323171c6..df7e602fe1be 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -28,9 +28,6 @@ def check_priorities(self): priorities.append(priority.priority) - if priority.default_priority: - default_priority.append(priority.default_priority) - response = priority.response_time resolution = priority.resolution_time @@ -218,7 +215,6 @@ def create_custom_fields(self, meta, service_level_agreement_fields): "fieldtype": field.get("fieldtype"), "insert_after": "append", "collapsible": field.get("collapsible"), - "hidden": field.get("hidden"), "options": field.get("options"), "read_only": field.get("read_only"), "hidden": field.get("hidden"), @@ -346,7 +342,7 @@ def set_documents_with_active_service_level_agreement(): def apply(doc, method=None): - "Applies SLA to document on validate" + # Applies SLA to document on validate if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ not doc.doctype in get_documents_with_active_service_level_agreement(): @@ -552,7 +548,7 @@ def reset_service_level_agreement(doc, reason, user): }).insert(ignore_permissions=True) doc.service_level_agreement_creation = now_datetime(doc.get("owner")) - doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=self.service_level_agreement) + doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=doc.service_level_agreement) doc.agreement_fulfilled = "Ongoing" doc.save() @@ -636,9 +632,8 @@ def handle_hold_time(doc, meta, status): def update_agreement_fulfilled_on_custom_status(doc): - """ - Update Agreement Fulfilled status using Custom Scripts for Custom Status - """ + # Update Agreement Fulfilled status using Custom Scripts for Custom Status + meta = frappe.get_meta(doc.doctype) if meta.has_field("first_responded_on") and not doc.first_responded_on: # first_responded_on set when first reply is sent to customer From ebb7ee7a0f098658d4f39ef43ab005f5b1b49117 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 29 Jul 2020 16:39:00 +0530 Subject: [PATCH 17/49] fix: Add resolved status field in Pause SLA --- .../service_level_agreement.js | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 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 db9163a791eb..8cee1cfc33f7 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -2,17 +2,30 @@ // For license information, please see license.txt frappe.ui.form.on('Service Level Agreement', { - setup: function(frm) { + refresh: function(frm) { + frm.trigger('fetch_status_fields'); + }, + + document_type: function(frm) { + frm.trigger('fetch_status_fields'); + }, + + fetch_status_fields: function(frm) { let allow_statuses = []; - const exclude_statuses = ['Open', 'Closed', 'Resolved']; + const exclude_statuses = ['Open', 'Closed']; - 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); - }); + if (frm.doc.document_type) { + frappe.model.with_doctype(frm.doc.document_type, () => { + let statuses = frappe.meta.get_docfield(frm.doc.document_type, '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); + }); + } + + frm.refresh_field('pause_sla_on'); }, + onload: function(frm) { frm.set_query("document_type", function() { return { From 681e2a296a8f971eee0f6c3d9ecc1f1ad6c6992b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 29 Jul 2020 17:58:09 +0530 Subject: [PATCH 18/49] fix: Put SLA on hold for resolved status --- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 df7e602fe1be..c0a2a7d0f9b6 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -388,7 +388,7 @@ def update_status(doc, from_db, meta): doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("status") and doc.status in ["Closed", "Resolved"] and from_db.status not in ["Closed", "Resolved"]: + if meta.has_field("status") and doc.status == "Closed" and from_db.status != "Closed": if meta.has_field("resolution_date"): doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) From dbe074c631dabb94985dfdbfe304c512bb61e9d2 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 5 Oct 2020 11:03:10 +0530 Subject: [PATCH 19/49] fix: codacy issues and patch --- erpnext/patches.txt | 1 + erpnext/patches/v13_0/add_doctype_to_sla.py | 4 ++-- .../service_level_agreement/service_level_agreement.py | 4 +--- .../service_level_agreement/test_service_level_agreement.py | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6087ce29aa5b..b48e6ad64d80 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -729,3 +729,4 @@ erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports erpnext.patches.v13_0.rename_issue_doctype_fields erpnext.patches.v13_0.change_default_pos_print_format erpnext.patches.v13_0.set_youtube_video_id +erpnext.patches.v13_0.add_doctype_to_sla diff --git a/erpnext/patches/v13_0/add_doctype_to_sla.py b/erpnext/patches/v13_0/add_doctype_to_sla.py index 65fb82cb7649..943ab0e838a5 100644 --- a/erpnext/patches/v13_0/add_doctype_to_sla.py +++ b/erpnext/patches/v13_0/add_doctype_to_sla.py @@ -8,5 +8,5 @@ def execute(): frappe.reload_doc('support', 'doctype', 'service_level_agreement') - for sla in frappe.get_all("Service Level Agreement"): - frappe.db.set_value("Issue", sla.name, "document_type", "Issue") \ No newline at end of file + for sla in frappe.get_all('Service Level Agreement'): + frappe.db.set_value('Service Level Agreement', sla.name, 'document_type', 'Issue') \ No newline at end of file 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 c0a2a7d0f9b6..d99bcbe85569 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -664,9 +664,7 @@ def is_holiday(date, holidays): def get_time_in_timedelta(time): - """ - Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215) - """ + """Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215)""" import datetime return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) 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 f630d17528f0..dab49c77ad82 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,7 +5,6 @@ import frappe import unittest -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): From e598d63e524079362487702734cae728f3089a8c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 5 Oct 2020 11:21:52 +0530 Subject: [PATCH 20/49] fix: codacy issue --- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d99bcbe85569..749567898b51 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -664,7 +664,7 @@ def is_holiday(date, holidays): def get_time_in_timedelta(time): - """Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215)""" + """Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215).""" import datetime return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) From 923ba47e769d66d3381c8f697d92699a20577075 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 9 Oct 2020 14:04:05 +0530 Subject: [PATCH 21/49] fix: validate if doctype for SLA configuration has status field --- .../service_level_agreement/service_level_agreement.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 749567898b51..977874948b56 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -12,9 +12,9 @@ from erpnext.support.doctype.issue.issue import get_holidays class ServiceLevelAgreement(Document): - def validate(self): self.validate_doc() + self.validate_status_field() self.check_priorities() self.check_support_and_resolution() @@ -81,6 +81,12 @@ def validate_doc(self): frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format(frappe.bold(self.entity_type), \ frappe.bold(self.entity))) + def validate_status_field(self): + meta = frappe.get_meta(self.document_type) + if not meta.get_field("status"): + frappe.throw(_("The Document Type {0} must have a Status field to configure Service Level Agreement").format( + frappe.bold(self.document_type))) + def get_service_level_agreement_priority(self, priority): priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name}) From aa6a1246d9160a16f508380141ec267b1899a443 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 13:06:20 +0530 Subject: [PATCH 22/49] fix: remove duplicate code, fix sider and translation issues --- erpnext/support/doctype/issue/issue.py | 185 +----------------- .../service_level_agreement.py | 6 +- 2 files changed, 4 insertions(+), 187 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 829825ec3a15..389e4f6f93dc 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -116,191 +116,8 @@ def split_issue(self, subject, communication_id): return replicated_issue.name - def before_insert(self): - if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): - self.set_response_and_resolution_time() - def set_response_and_resolution_time(self, priority=None, service_level_agreement=None): - service_level_agreement = get_active_service_level_agreement_for(priority=priority, - customer=self.customer, service_level_agreement=service_level_agreement) - - if not service_level_agreement: - if frappe.db.get_value("Issue", self.name, "service_level_agreement"): - frappe.throw(_("Couldn't Set Service Level Agreement {0}.").format(self.service_level_agreement)) - return - - if (service_level_agreement.customer and self.customer) and not (service_level_agreement.customer == self.customer): - frappe.throw(_("This Service Level Agreement is specific to Customer {0}").format(service_level_agreement.customer)) - - self.service_level_agreement = service_level_agreement.name - self.priority = service_level_agreement.default_priority if not priority else priority - - 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_variance = round(time_diff_in_seconds(self.response_by, now_datetime())) - self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime())) - - def change_service_level_agreement_and_priority(self): - if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \ - frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): - - if not self.priority == frappe.db.get_value("Issue", self.name, "priority"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - frappe.msgprint(_("Priority has been changed to {0}.").format(self.priority)) - - if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) - - def reset_service_level_agreement(self, reason, user): - if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): - frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) - - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Info", - "reference_doctype": self.doctype, - "reference_name": self.name, - "comment_email": user, - "content": " resetted Service Level Agreement - {0}".format(_(reason)), - }).insert(ignore_permissions=True) - - self.service_level_agreement_creation = now_datetime() - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - self.agreement_status = "Ongoing" - self.save() - - def reset_issue_metrics(self): - self.db_set("resolution_time", None) - self.db_set("user_resolution_time", None) - - -def get_priority(issue): - service_level_agreement = frappe.get_doc("Service Level Agreement", issue.service_level_agreement) - priority = service_level_agreement.get_service_level_agreement_priority(issue.priority) - priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list - }) - return priority - - -def get_expected_time_for(parameter, service_level, start_date_time): - current_date_time = start_date_time - expected_time = current_date_time - start_time = None - end_time = None - - if parameter == "response": - allotted_seconds = service_level.get("response_time") - elif parameter == "resolution": - allotted_seconds = service_level.get("resolution_time") - else: - frappe.throw(_("{0} parameter is invalid").format(parameter)) - - expected_time_is_set = 0 - - support_days = {} - for service in service_level.get("support_and_resolution"): - support_days[service.workday] = frappe._dict({ - "start_time": service.start_time, - "end_time": service.end_time, - }) - - holidays = get_holidays(service_level.get("holiday_list")) - weekdays = get_weekdays() - - while not expected_time_is_set: - current_weekday = weekdays[current_date_time.weekday()] - - if not is_holiday(current_date_time, holidays) and current_weekday in support_days: - start_time = current_date_time - datetime(current_date_time.year, current_date_time.month, current_date_time.day) \ - 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_seconds(end_time, start_time) - - # no time left for support today - 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, seconds=allotted_seconds) - expected_time_is_set = 1 - else: - 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 allotted_seconds >= 86400: - current_date_time = datetime.combine(getdate(current_date_time), get_time(end_time)) - else: - current_date_time = expected_time - - return current_date_time - -def set_service_level_agreement_variance(issue=None): - current_time = frappe.flags.current_time or now_datetime() - - filters = {"status": "Open", "agreement_status": "Ongoing"} - if issue: - filters = {"name": issue} - - for issue in frappe.get_list("Issue", filters=filters): - doc = frappe.get_doc("Issue", issue.name) - - if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer - variance = round(time_diff_in_seconds(doc.response_by, current_time), 2) - frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False) - if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) - - if not doc.resolution_date: # resolution_date set when issue has been closed - variance = round(time_diff_in_seconds(doc.resolution_by, current_time), 2) - frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False) - if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) - - -def set_resolution_time(issue): - # total time taken from issue creation to closing - resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation) - issue.db_set("resolution_time", resolution_time) - - -def set_user_resolution_time(issue): - # total time taken by a user to close the issue apart from wait_time - communications = frappe.get_list("Communication", filters={ - "reference_doctype": issue.doctype, - "reference_name": issue.name - }, - fields=["sent_or_received", "name", "creation"], - order_by="creation" - ) - - pending_time = [] - for i in range(len(communications)): - if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent": - wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) - if wait_time > 0: - pending_time.append(wait_time) - - total_pending_time = sum(pending_time) - resolution_time_in_secs = time_diff_in_seconds(issue.resolution_date, issue.creation) - user_resolution_time = resolution_time_in_secs - total_pending_time - issue.db_set("user_resolution_time", user_resolution_time) - - - def get_list_context(context=None): +def get_list_context(context=None): return { "title": _("Issues"), "get_list": get_issue_list, 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 977874948b56..01d72f0aaf66 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -54,8 +54,8 @@ def check_support_and_resolution(self): support_and_resolution.idx = week.index(support_and_resolution.workday) + 1 if to_timedelta(support_and_resolution.start_time) >= to_timedelta(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))) + 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): @@ -351,7 +351,7 @@ def apply(doc, method=None): # Applies SLA to document on validate if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ - not doc.doctype in get_documents_with_active_service_level_agreement(): + doc.doctype not in get_documents_with_active_service_level_agreement(): return From 9531f6af9c1117e7cea97c016d33b96228a52749 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 14:18:09 +0530 Subject: [PATCH 23/49] fix: SLA status not getting updated --- erpnext/support/doctype/issue/issue.py | 2 +- .../service_level_agreement.py | 52 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 389e4f6f93dc..01ed768b5ee5 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -7,7 +7,7 @@ from frappe import _ from frappe import utils from frappe.model.document import Document -from frappe.utils import now_datetime, getdate, get_weekdays, add_to_date, get_time, get_datetime, time_diff_in_seconds +from frappe.utils import now_datetime 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 01d72f0aaf66..9b7b0fb872b3 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,7 +6,7 @@ import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import time_diff_in_hours, getdate, get_weekdays, add_to_date, get_time, get_datetime, \ +from frappe.utils import time_diff_in_seconds, getdate, get_weekdays, add_to_date, get_time, get_datetime, \ time_diff_in_seconds, get_time_zone, to_timedelta, get_datetime_str, get_link_to_form from datetime import datetime from erpnext.support.doctype.issue.issue import get_holidays @@ -130,9 +130,9 @@ def before_insert(self): }, { "default": "Ongoing", - "fieldname": "agreement_fulfilled", + "fieldname": "agreement_status", "fieldtype": "Select", - "label": "Service Level Agreement Fulfilled", + "label": "Service Level Agreement Status", "options": "Ongoing\nFulfilled\nFailed", "read_only": 1 }, @@ -362,7 +362,6 @@ def apply(doc, method=None): return meta = frappe.get_meta(doc.doctype) - from_db = frappe._dict({}) if doc.is_new() else frappe.get_doc(doc.doctype, doc.name) if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \ not service_level_agreement.customer == doc.get("customer"): @@ -385,6 +384,7 @@ def apply(doc, method=None): set_response_by_and_variance(doc, meta, start_date_time, priority) set_resolution_by_and_variance(doc, meta, start_date_time, priority) + from_db = frappe._dict({}) if doc.is_new() else frappe.get_doc(doc.doctype, doc.name) update_status(doc, from_db, meta) @@ -398,7 +398,7 @@ def update_status(doc, from_db, meta): if meta.has_field("resolution_date"): doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("agreement_fulfilled") and from_db.agreement_fulfilled == "Ongoing": + if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": set_service_level_agreement_variance(doc.doctype, doc.name) update_agreement_status(doc, from_db, meta) @@ -471,7 +471,7 @@ def get_expected_time_for(parameter, service_level, start_date_time): def set_service_level_agreement_variance(doctype, doc=None): - filters = {"status": "Open", "agreement_fulfilled": "Ongoing"} + filters = {"status": "Open", "agreement_status": "Ongoing"} if doc: filters = {"name": doc} @@ -481,18 +481,18 @@ def set_service_level_agreement_variance(doctype, doc=None): current_time = frappe.flags.current_time or now_datetime(doc.get("owner")) if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer - variance = round(time_diff_in_hours(doc.response_by, current_time), 2) + variance = round(time_diff_in_seconds(doc.response_by, current_time), 2) frappe.db.set_value(doc.doctype, doc.name, "response_by_variance", variance, update_modified=False) if variance < 0: - frappe.db.set_value(doc.doctype, doc.name, "agreement_fulfilled", "Failed", update_modified=False) + frappe.db.set_value(doc.doctype, doc.name, "agreement_status", "Failed", update_modified=False) if not doc.resolution_date: # resolution_date set when issue has been closed - variance = round(time_diff_in_hours(doc.resolution_by, current_time), 2) + variance = round(time_diff_in_seconds(doc.resolution_by, current_time), 2) frappe.db.set_value(doc.doctype, doc.name, "resolution_by_variance", variance, update_modified=False) if variance < 0: - frappe.db.set_value(doc.doctype, doc.name, "agreement_fulfilled", "Failed", update_modified=False) + frappe.db.set_value(doc.doctype, doc.name, "agreement_status", "Failed", update_modified=False) def set_user_resolution_time(doc, meta): @@ -555,7 +555,7 @@ def reset_service_level_agreement(doc, reason, user): doc.service_level_agreement_creation = now_datetime(doc.get("owner")) doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=doc.service_level_agreement) - doc.agreement_fulfilled = "Ongoing" + doc.agreement_status = "Ongoing" doc.save() @@ -569,8 +569,8 @@ def reset_metrics(doc, meta): if not meta.has_field("user_resolution_time"): doc.user_resolution_time = None - if meta.has_field("agreement_fulfilled"): - doc.agreement_fulfilled = "Ongoing" + if meta.has_field("agreement_status"): + doc.agreement_status = "Ongoing" def set_resolution_time(doc, meta): @@ -621,14 +621,14 @@ def handle_hold_time(doc, meta, status): if meta.has_field("first_responded_on") and not doc.first_responded_on: response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_hours(response_by, now_time)) + response_by_variance = round(time_diff_in_seconds(response_by, now_time)) update_values['response_by'] = response_by update_values['response_by_variance'] = response_by_variance + (last_hold_time // 3600) resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) - resolution_by_variance = round(time_diff_in_hours(resolution_by, now_time)) + resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) update_values['resolution_by'] = resolution_by update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600) @@ -637,32 +637,32 @@ def handle_hold_time(doc, meta, status): doc.update(update_values) -def update_agreement_fulfilled_on_custom_status(doc): +def update_agreement_status_on_custom_status(doc): # Update Agreement Fulfilled status using Custom Scripts for Custom Status meta = frappe.get_meta(doc.doctype) if meta.has_field("first_responded_on") and not doc.first_responded_on: # first_responded_on set when first reply is sent to customer - doc.response_by_variance = round(time_diff_in_hours(doc.response_by, now_datetime(doc.get("owner"))), 2) + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_datetime(doc.get("owner"))), 2) if meta.has_field("resolution_date") and not doc.resolution_date: # resolution_date set when issue has been closed - doc.resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_datetime(doc.get("owner"))), 2) + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_datetime(doc.get("owner"))), 2) - if meta.has_field("agreement_fulfilled"): - doc.agreement_fulfilled = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" + if meta.has_field("agreement_status"): + doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" def update_agreement_status(doc, from_db, meta): - if meta.has_field("service_level_agreement") and meta.has_field("agreement_fulfilled") and \ - doc.service_level_agreement and doc.agreement_fulfilled == "Ongoing": + if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \ + doc.service_level_agreement and doc.agreement_status == "Ongoing": if (meta.has_field("response_by_variance") and from_db.response_by_variance < 0) or \ (meta.has_field("resolution_by_variance") and from_db.resolution_by_variance < 0): - doc.agreement_fulfilled = "Failed" + doc.agreement_status = "Failed" else: - doc.agreement_fulfilled = "Fulfilled" + doc.agreement_status = "Fulfilled" def is_holiday(date, holidays): @@ -680,7 +680,7 @@ def set_response_by_and_variance(doc, meta, start_date_time, priority): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) if meta.has_field("response_by_variance"): - doc.response_by_variance = round(time_diff_in_hours(doc.response_by, now_datetime(doc.get("owner")))) + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_datetime(doc.get("owner")))) def set_resolution_by_and_variance(doc, meta, start_date_time, priority): @@ -688,7 +688,7 @@ def set_resolution_by_and_variance(doc, meta, start_date_time, priority): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) if meta.has_field("resolution_by_variance"): - doc.resolution_by_variance = round(time_diff_in_hours(doc.resolution_by, now_datetime(doc.get("owner")))) + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_datetime(doc.get("owner")))) def now_datetime(user): dt = convert_utc_to_user_timezone(datetime.utcnow(), user) From 0de31634813dd797b58b176df4c31fff382cd535 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 14:42:32 +0530 Subject: [PATCH 24/49] fix: sider --- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9b7b0fb872b3..6816245dd431 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,7 @@ from frappe.model.document import Document from frappe import _ from frappe.utils import time_diff_in_seconds, getdate, get_weekdays, add_to_date, get_time, get_datetime, \ - time_diff_in_seconds, get_time_zone, to_timedelta, get_datetime_str, get_link_to_form + get_time_zone, to_timedelta, get_datetime_str, get_link_to_form from datetime import datetime from erpnext.support.doctype.issue.issue import get_holidays From 7c47cfcb087a4dd34fa0b52bfc42ce4d33730f92 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 15:46:26 +0530 Subject: [PATCH 25/49] fix: Response and Resolution variance duration fieldtype --- .../service_level_agreement/service_level_agreement.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 6816245dd431..eeaa28094b86 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -117,9 +117,9 @@ def before_insert(self): "read_only": 1 }, { - "description": "in hours", "fieldname": "response_by_variance", - "fieldtype": "Float", + "fieldtype": "Duration", + "hide_seconds": 1, "label": "Response By Variance", "read_only": 1 }, @@ -143,9 +143,9 @@ def before_insert(self): "read_only": 1 }, { - "description": "in hours", "fieldname": "resolution_by_variance", - "fieldtype": "Float", + "fieldtype": "Duration", + "hide_seconds": 1, "label": "Resolution By Variance", "read_only": 1 }, From 83927e228774afb26bd70d8a9e02e360373be695 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 19:23:02 +0530 Subject: [PATCH 26/49] fix: SLA not getting applied for custom doctypes --- .../service_level_agreement.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 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 eeaa28094b86..02afa6720a38 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -167,6 +167,14 @@ def before_insert(self): "fieldtype": "Datetime", "label": "First Responded On", "read_only": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "resolution_date", + "fieldtype": "Datetime", + "label": "Resolution Date", + "no_copy": 1, + "read_only": 1 } ] @@ -370,7 +378,7 @@ def apply(doc, method=None): service_level_agreement.customer)) doc.service_level_agreement = service_level_agreement.name - doc.priority = doc.priority or service_level_agreement.default_priority + doc.priority = doc.get("priority") or service_level_agreement.default_priority priority = get_priority(doc) if not doc.creation: @@ -384,7 +392,7 @@ def apply(doc, method=None): set_response_by_and_variance(doc, meta, start_date_time, priority) set_resolution_by_and_variance(doc, meta, start_date_time, priority) - from_db = frappe._dict({}) if doc.is_new() else frappe.get_doc(doc.doctype, doc.name) + from_db = frappe._dict({}) if doc.__unsaved else frappe.get_doc(doc.doctype, doc.name) update_status(doc, from_db, meta) @@ -487,7 +495,7 @@ def set_service_level_agreement_variance(doctype, doc=None): if variance < 0: frappe.db.set_value(doc.doctype, doc.name, "agreement_status", "Failed", update_modified=False) - if not doc.resolution_date: # resolution_date set when issue has been closed + if not doc.get("resolution_date"): # resolution_date set when issue has been closed variance = round(time_diff_in_seconds(doc.resolution_by, current_time), 2) frappe.db.set_value(doc.doctype, doc.name, "resolution_by_variance", variance, update_modified=False) From c4afa5a3c53c0b4f50769c2e8f9c719973fb4dbc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 20:04:52 +0530 Subject: [PATCH 27/49] fix(hooks): update sla variance --- erpnext/hooks.py | 2 +- .../service_level_agreement.py | 30 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a19f56a0e237..c48cd708dc55 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -309,7 +309,7 @@ "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", - "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", + "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance", ], "daily": [ "erpnext.stock.reorder_item.reorder_item", 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 02afa6720a38..c8ade9e4002b 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -110,6 +110,12 @@ def before_insert(self): "label": "Service Level Agreement", "options": "Service Level Agreement" }, + { + "fieldname": "priority", + "fieldtype": "Link", + "label": "Priority", + "options": "Issue Priority" + }, { "fieldname": "response_by", "fieldtype": "Datetime", @@ -123,6 +129,12 @@ def before_insert(self): "label": "Response By Variance", "read_only": 1 }, + { + "fieldname": "first_responded_on", + "fieldtype": "Datetime", + "label": "First Responded On", + "read_only": 1 + }, { "fieldname": "cb", "fieldtype": "Column Break", @@ -156,18 +168,6 @@ def before_insert(self): "label": "Service Level Agreement Creation", "read_only": 1 }, - { - "fieldname": "priority", - "fieldtype": "Link", - "label": "Priority", - "options": "Issue Priority" - }, - { - "fieldname": "first_responded_on", - "fieldtype": "Datetime", - "label": "First Responded On", - "read_only": 1 - }, { "depends_on": "eval:!doc.__islocal", "fieldname": "resolution_date", @@ -392,7 +392,11 @@ def apply(doc, method=None): set_response_by_and_variance(doc, meta, start_date_time, priority) set_resolution_by_and_variance(doc, meta, start_date_time, priority) - from_db = frappe._dict({}) if doc.__unsaved else frappe.get_doc(doc.doctype, doc.name) + if frappe.db.exists(doc.doctype, doc.name): + from_db = frappe.get_doc(doc.doctype, doc.name) + else: + from_db = frappe._dict({}) + update_status(doc, from_db, meta) From 6bffd7c4aaa35080ff2b52dc84589b37c97317c7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 20:52:40 +0530 Subject: [PATCH 28/49] feat: show SLA indicators --- erpnext/public/js/utils.js | 146 ++++++++++++++++++ erpnext/support/doctype/issue/issue.js | 130 +--------------- .../service_level_agreement.py | 14 ++ 3 files changed, 162 insertions(+), 128 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 891bbe5b598b..8d28df2387e3 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -734,3 +734,149 @@ $(document).on('app_ready', function() { }); } }); + +// Show SLA dashboard +$(document).on('app_ready', function() { + frappe.call({ + method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_sla_doctypes', + callback: function(r) { + if (r.message) { + $.each(r.message, function(_i, d) { + frappe.ui.form.on(d, { + onload: function(frm) { + if (frm.doc.service_level_agreement) { + frappe.call({ + method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters', + args: { + doctype: frm.doc.doctype, + name: frm.doc.service_level_agreement, + customer: frm.doc.customer + }, + callback: function (r) { + if (r && r.message) { + frm.set_query('priority', function() { + return { + filters: { + 'name': ['in', r.message.priority], + } + }; + }); + frm.set_query('service_level_agreement', function() { + return { + filters: { + 'name': ['in', r.message.service_level_agreements], + } + }; + }); + } + } + }); + } + }, + + refresh: function(frm) { + if (frm.doc.status !== 'Closed' && frm.doc.agreement_status === 'Ongoing') { + if (frm.doc.service_level_agreement) { + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Service Level Agreement', + name: frm.doc.service_level_agreement + }, + callback: function(data) { + let statuses = data.message.pause_sla_on; + const hold_statuses = []; + $.each(statuses, (_i, entry) => { + hold_statuses.push(entry.status); + }); + if (hold_statuses.includes(frm.doc.status)) { + frm.dashboard.clear_headline(); + let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])}; + frm.dashboard.set_headline_alert( + '
' + + '
' + + ''+ message.msg +' ' + + '
' + + '
' + ); + } else { + set_time_to_resolve_and_response(frm); + } + } + }); + } + + frm.add_custom_button(__('Close'), function () { + frm.set_value('status', 'Closed'); + frm.save(); + }); + + } else { + if (frm.doc.service_level_agreement) { + frm.dashboard.clear_headline(); + + let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ? + {'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} : + {'indicator': 'red', 'msg': 'Service Level Agreement Failed'}; + + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + + frm.add_custom_button(__('Reopen'), function () { + frm.set_value('status', 'Open'); + frm.save(); + }); + } + }, + }); + }); + } + } + }); +}); + +function set_time_to_resolve_and_response(frm) { + frm.dashboard.clear_headline(); + + var time_to_respond = get_status(frm.doc.response_by_variance); + if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') { + time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status); + } + + var time_to_resolve = get_status(frm.doc.resolution_by_variance); + if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') { + time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); + } + + frm.dashboard.set_headline_alert( + '
' + + '
' + + 'Time to Respond: '+ time_to_respond.diff_display +' ' + + '
' + + '
' + + 'Time to Resolve: '+ time_to_resolve.diff_display +' ' + + '
' + + '
' + ); +} + +function get_time_left(timestamp, agreement_status) { + const diff = moment(timestamp).diff(moment()); + const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed'; + let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green'; + return {'diff_display': diff_display, 'indicator': indicator}; +} + +function get_status(variance) { + if (variance > 0) { + return {'diff_display': 'Fulfilled', 'indicator': 'green'}; + } else { + return {'diff_display': 'Failed', 'indicator': 'red'}; + } +} diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 4d091d3973aa..dd5149e73d4d 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -11,102 +11,16 @@ frappe.ui.form.on("Issue", { frm.set_df_property("reset_service_level_agreement", "hidden", 1); } }); - - if (frm.doc.service_level_agreement) { - frappe.call({ - method: "erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters", - args: { - doctype: frm.doc.doctype, - name: frm.doc.service_level_agreement, - customer: frm.doc.customer - }, - callback: function (r) { - if (r && r.message) { - frm.set_query('priority', function() { - return { - filters: { - "name": ["in", r.message.priority], - } - }; - }); - frm.set_query('service_level_agreement', function() { - return { - filters: { - "name": ["in", r.message.service_level_agreements], - } - }; - }); - } - } - }); - } }, refresh: function (frm) { if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") { - if (frm.doc.service_level_agreement) { - frappe.call({ - 'method': 'frappe.client.get', - args: { - doctype: 'Service Level Agreement', - name: frm.doc.service_level_agreement - }, - callback: function(data) { - let statuses = data.message.pause_sla_on; - const hold_statuses = []; - $.each(statuses, (_i, entry) => { - hold_statuses.push(entry.status); - }); - if (hold_statuses.includes(frm.doc.status)) { - frm.dashboard.clear_headline(); - let message = {"indicator": "orange", "msg": __("SLA is on hold since {0}", [moment(frm.doc.on_hold_since).fromNow(true)])}; - frm.dashboard.set_headline_alert( - '
' + - '
' + - ''+ message.msg +' ' + - '
' + - '
' - ); - } else { - set_time_to_resolve_and_response(frm); - } - } - }); - } - - frm.add_custom_button(__("Close"), function () { - frm.set_value("status", "Closed"); - frm.save(); - }); - frm.add_custom_button(__("Task"), function () { frappe.model.open_mapped_doc({ method: "erpnext.support.doctype.issue.issue.make_task", frm: frm }); }, __("Create")); - - } else { - if (frm.doc.service_level_agreement) { - frm.dashboard.clear_headline(); - - let agreement_status = (frm.doc.agreement_status == "Fulfilled") ? - {"indicator": "green", "msg": "Service Level Agreement has been fulfilled"} : - {"indicator": "red", "msg": "Service Level Agreement Failed"}; - - frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' - ); - } - - frm.add_custom_button(__("Reopen"), function () { - frm.set_value("status", "Open"); - frm.save(); - }); } }, @@ -132,7 +46,7 @@ frappe.ui.form.on("Issue", { message: __('Resetting Service Level Agreement.') }); - frm.call("reset_service_level_agreement", { + frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", { reason: values.reason, user: frappe.session.user_email }, () => { @@ -197,44 +111,4 @@ frappe.ui.form.on("Issue", { } } }, -}); - -function set_time_to_resolve_and_response(frm) { - frm.dashboard.clear_headline(); - - var time_to_respond = get_status(frm.doc.response_by_variance); - if (!frm.doc.first_responded_on && frm.doc.agreement_status === "Ongoing") { - time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status); - } - - var time_to_resolve = get_status(frm.doc.resolution_by_variance); - if (!frm.doc.resolution_date && frm.doc.agreement_status === "Ongoing") { - time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); - } - - frm.dashboard.set_headline_alert( - '
' + - '
' + - 'Time to Respond: '+ time_to_respond.diff_display +' ' + - '
' + - '
' + - 'Time to Resolve: '+ time_to_resolve.diff_display +' ' + - '
' + - '
' - ); -} - -function get_time_left(timestamp, agreement_status) { - const diff = moment(timestamp).diff(moment()); - const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed"; - let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green"; - return {"diff_display": diff_display, "indicator": indicator}; -} - -function get_status(variance) { - if (variance > 0) { - return {"diff_display": "Fulfilled", "indicator": "green"}; - } else { - return {"diff_display": "Failed", "indicator": "red"}; - } -} +}); \ No newline at end of file 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 c8ade9e4002b..01d0bf645ae8 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -722,3 +722,17 @@ def get_tz(user): @frappe.whitelist() def get_user_time(user, to_string=False): return get_datetime_str(now_datetime(user)) if to_string else now_datetime(user) + +@frappe.whitelist() +def get_sla_doctypes(): + doctypes = [] + data = frappe.get_list('Service Level Agreement', + {'enabled': 1}, + ['document_type'], + distinct=1 + ) + + for entry in data: + doctypes.append(entry.document_type) + + return doctypes From 9e0f2bcbf14242afe2983abed721a6794a93abbe Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 21:14:00 +0530 Subject: [PATCH 29/49] fix: set SLA fulfilled on Resolved --- .../service_level_agreement.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 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 01d0bf645ae8..b36d30c295d8 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -401,27 +401,28 @@ def apply(doc, method=None): def update_status(doc, from_db, meta): - if meta.has_field("status") and meta.has_field("first_responded_on") and doc.status != "Open" and \ - from_db.status == "Open" and not doc.first_responded_on: + if meta.has_field("status"): + if meta.has_field("first_responded_on") and doc.status != "Open" and \ + from_db.status == "Open" and not doc.first_responded_on: - doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) + doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("status") and doc.status == "Closed" and from_db.status != "Closed": - if meta.has_field("resolution_date"): - doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) + if doc.status in ["Resolved", "Closed"] and from_db.status not in ["Resolved", "Closed"]: + if meta.has_field("resolution_date"): + doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": - set_service_level_agreement_variance(doc.doctype, doc.name) - update_agreement_status(doc, from_db, meta) + if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": + set_service_level_agreement_variance(doc.doctype, doc.name) + update_agreement_status(doc, from_db, meta) - set_resolution_time(doc, meta) - set_user_resolution_time(doc, meta) + set_resolution_time(doc, meta) + set_user_resolution_time(doc, meta) - if meta.has_field("status") and doc.status == "Open" and not from_db.status == "Open": - # if no date, it should be set as None and not a blank string "", as per mysql strict config - # enable SLA and variance on Reopen - reset_metrics(doc, meta) - set_service_level_agreement_variance(doc.doctype, doc.name) + if doc.status == "Open" and from_db.status != "Open": + # if no date, it should be set as None and not a blank string "", as per mysql strict config + # enable SLA and variance on Reopen + reset_metrics(doc, meta) + set_service_level_agreement_variance(doc.doctype, doc.name) handle_hold_time(doc, meta, from_db.status) From bf718380398894dd3ab0121f069d76ffd776d9e7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 23:52:19 +0530 Subject: [PATCH 30/49] fix: travis --- .../service_level_agreement/service_level_agreement.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 043baacf47ae..d88582ecbd6b 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "format:SLA-{document_type}-{service_level}", + "autoname": "format:SLA-{document_type}-{service_level}-{####}", "creation": "2018-12-26 21:08:15.448812", "doctype": "DocType", "editable_grid": 1, @@ -170,7 +170,7 @@ } ], "links": [], - "modified": "2020-06-26 11:05:23.188537", + "modified": "2020-11-09 23:52:04.126930", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", From d7c794985bbad08cc66ebe7066ce1a59ec63e8c6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 10 Nov 2020 12:14:40 +0530 Subject: [PATCH 31/49] feat: Hold time for SLA on custom doctypes --- .../service_level_agreement.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 b36d30c295d8..e1d0e65ae8f5 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -135,6 +135,19 @@ def before_insert(self): "label": "First Responded On", "read_only": 1 }, + { + "fieldname": "on_hold_since", + "fieldtype": "Datetime", + "hidden": 1, + "label": "On Hold Since", + "read_only": 1 + }, + { + "fieldname": "total_hold_time", + "fieldtype": "Duration", + "label": "Total Hold Time", + "read_only": 1 + }, { "fieldname": "cb", "fieldtype": "Column Break", @@ -647,7 +660,7 @@ def handle_hold_time(doc, meta, status): update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600) update_values['on_hold_since'] = None - doc.update(update_values) + doc.db_set(update_values) def update_agreement_status_on_custom_status(doc): From 20e1781ea8a1668ca45f6a85a9dac0fd4478b9d7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 10 Nov 2020 13:54:18 +0530 Subject: [PATCH 32/49] fix(patch): rename enable to enabled in SLA --- erpnext/patches/v13_0/add_doctype_to_sla.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/patches/v13_0/add_doctype_to_sla.py b/erpnext/patches/v13_0/add_doctype_to_sla.py index 943ab0e838a5..a62eea112fa9 100644 --- a/erpnext/patches/v13_0/add_doctype_to_sla.py +++ b/erpnext/patches/v13_0/add_doctype_to_sla.py @@ -4,9 +4,12 @@ from __future__ import unicode_literals import frappe +from frappe.model.utils.rename_field import rename_field def execute(): frappe.reload_doc('support', 'doctype', 'service_level_agreement') + if frappe.db.has_column('Service Level Agreement', 'enable'): + rename_field('Service Level Agreement', 'enable', 'enabled') for sla in frappe.get_all('Service Level Agreement'): frappe.db.set_value('Service Level Agreement', sla.name, 'document_type', 'Issue') \ No newline at end of file From 628ddfe9f3709d5f3b30026312f42a5399784c25 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 10 Nov 2020 14:18:19 +0530 Subject: [PATCH 33/49] fix: incorrect response and resolution variance calculation --- .../service_level_agreement.py | 212 +++++++++--------- 1 file changed, 109 insertions(+), 103 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 e1d0e65ae8f5..aa3dc2accbd1 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -63,7 +63,7 @@ def check_support_and_resolution(self): 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") and self.enable: + if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement") and self.enabled: frappe.throw(_("{0} is not enabled in {1}").format(frappe.bold("Track Service Level Agreement"), get_link_to_form("Support Settings", "Support Settings"))) @@ -97,100 +97,7 @@ def get_service_level_agreement_priority(self, priority): }) def before_insert(self): - service_level_agreement_fields = [ - { - "collapsible": 1, - "fieldname": "service_level_section", - "fieldtype": "Section Break", - "label": "Service Level" - }, - { - "fieldname": "service_level_agreement", - "fieldtype": "Link", - "label": "Service Level Agreement", - "options": "Service Level Agreement" - }, - { - "fieldname": "priority", - "fieldtype": "Link", - "label": "Priority", - "options": "Issue Priority" - }, - { - "fieldname": "response_by", - "fieldtype": "Datetime", - "label": "Response By", - "read_only": 1 - }, - { - "fieldname": "response_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Response By Variance", - "read_only": 1 - }, - { - "fieldname": "first_responded_on", - "fieldtype": "Datetime", - "label": "First Responded On", - "read_only": 1 - }, - { - "fieldname": "on_hold_since", - "fieldtype": "Datetime", - "hidden": 1, - "label": "On Hold Since", - "read_only": 1 - }, - { - "fieldname": "total_hold_time", - "fieldtype": "Duration", - "label": "Total Hold Time", - "read_only": 1 - }, - { - "fieldname": "cb", - "fieldtype": "Column Break", - "read_only": 1 - }, - { - "default": "Ongoing", - "fieldname": "agreement_status", - "fieldtype": "Select", - "label": "Service Level Agreement Status", - "options": "Ongoing\nFulfilled\nFailed", - "read_only": 1 - }, - { - "fieldname": "resolution_by", - "fieldtype": "Datetime", - "label": "Resolution By", - "read_only": 1 - }, - { - "fieldname": "resolution_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Resolution By Variance", - "read_only": 1 - }, - { - "fieldname": "service_level_agreement_creation", - "fieldtype": "Datetime", - "hidden": 1, - "label": "Service Level Agreement Creation", - "read_only": 1 - }, - { - "depends_on": "eval:!doc.__islocal", - "fieldname": "resolution_date", - "fieldtype": "Datetime", - "label": "Resolution Date", - "no_copy": 1, - "read_only": 1 - } - ] - + service_level_agreement_fields = get_service_level_agreement_fields() meta = frappe.get_meta(self.document_type) if meta.custom: @@ -266,6 +173,7 @@ def reset_field_properties(self, field, sla_field): field.default = sla_field.get("default") field.save(ignore_permissions=True) + def check_agreement_status(): service_level_agreements = frappe.get_all("Service Level Agreement", filters=[ {"enabled": 1}, @@ -287,11 +195,11 @@ def get_active_service_level_agreement_for(doctype, priority, customer=None, ser ["Service Level Agreement", "enabled", "=", 1] ] - or_filters = [] - if priority: filters.append(["Service Level Priority", "priority", "=", priority]) + or_filters = [] + if customer: or_filters.append( ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] @@ -370,10 +278,8 @@ def set_documents_with_active_service_level_agreement(): def apply(doc, method=None): # Applies SLA to document on validate - if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ doc.doctype not in get_documents_with_active_service_level_agreement(): - return service_level_agreement = get_active_service_level_agreement_for(doctype=doc.get("doctype"), priority=doc.get("priority"), @@ -386,7 +292,6 @@ def apply(doc, method=None): if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \ not service_level_agreement.customer == doc.get("customer"): - frappe.throw(_("Service Level Agreement {0} is specific to Customer {1}").format(service_level_agreement.name, service_level_agreement.customer)) @@ -417,7 +322,6 @@ def update_status(doc, from_db, meta): if meta.has_field("status"): if meta.has_field("first_responded_on") and doc.status != "Open" and \ from_db.status == "Open" and not doc.first_responded_on: - doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) if doc.status in ["Resolved", "Closed"] and from_db.status not in ["Resolved", "Closed"]: @@ -439,6 +343,7 @@ def update_status(doc, from_db, meta): handle_hold_time(doc, meta, from_db.status) + def get_expected_time_for(parameter, service_level, start_date_time): current_date_time = start_date_time expected_time = current_date_time @@ -650,19 +555,115 @@ def handle_hold_time(doc, meta, status): response_by_variance = round(time_diff_in_seconds(response_by, now_time)) update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + (last_hold_time // 3600) + update_values['response_by_variance'] = response_by_variance + last_hold_time resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600) + update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time update_values['on_hold_since'] = None doc.db_set(update_values) +def get_service_level_agreement_fields(): + return [ + { + "collapsible": 1, + "fieldname": "service_level_section", + "fieldtype": "Section Break", + "label": "Service Level" + }, + { + "fieldname": "service_level_agreement", + "fieldtype": "Link", + "label": "Service Level Agreement", + "options": "Service Level Agreement" + }, + { + "fieldname": "priority", + "fieldtype": "Link", + "label": "Priority", + "options": "Issue Priority" + }, + { + "fieldname": "response_by", + "fieldtype": "Datetime", + "label": "Response By", + "read_only": 1 + }, + { + "fieldname": "response_by_variance", + "fieldtype": "Duration", + "hide_seconds": 1, + "label": "Response By Variance", + "read_only": 1 + }, + { + "fieldname": "first_responded_on", + "fieldtype": "Datetime", + "label": "First Responded On", + "read_only": 1 + }, + { + "fieldname": "on_hold_since", + "fieldtype": "Datetime", + "hidden": 1, + "label": "On Hold Since", + "read_only": 1 + }, + { + "fieldname": "total_hold_time", + "fieldtype": "Duration", + "label": "Total Hold Time", + "read_only": 1 + }, + { + "fieldname": "cb", + "fieldtype": "Column Break", + "read_only": 1 + }, + { + "default": "Ongoing", + "fieldname": "agreement_status", + "fieldtype": "Select", + "label": "Service Level Agreement Status", + "options": "Ongoing\nFulfilled\nFailed", + "read_only": 1 + }, + { + "fieldname": "resolution_by", + "fieldtype": "Datetime", + "label": "Resolution By", + "read_only": 1 + }, + { + "fieldname": "resolution_by_variance", + "fieldtype": "Duration", + "hide_seconds": 1, + "label": "Resolution By Variance", + "read_only": 1 + }, + { + "fieldname": "service_level_agreement_creation", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Service Level Agreement Creation", + "read_only": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "resolution_date", + "fieldtype": "Datetime", + "label": "Resolution Date", + "no_copy": 1, + "read_only": 1 + } + ] + + def update_agreement_status_on_custom_status(doc): # Update Agreement Fulfilled status using Custom Scripts for Custom Status @@ -716,10 +717,12 @@ def set_resolution_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("resolution_by_variance"): doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_datetime(doc.get("owner")))) + def now_datetime(user): dt = convert_utc_to_user_timezone(datetime.utcnow(), user) return dt.replace(tzinfo=None) + def convert_utc_to_user_timezone(utc_timestamp, user): from pytz import timezone, UnknownTimeZoneError @@ -730,13 +733,16 @@ def convert_utc_to_user_timezone(utc_timestamp, user): except UnknownTimeZoneError: return utcnow + def get_tz(user): return frappe.db.get_value("User", user, "time_zone") or get_time_zone() + @frappe.whitelist() def get_user_time(user, to_string=False): return get_datetime_str(now_datetime(user)) if to_string else now_datetime(user) + @frappe.whitelist() def get_sla_doctypes(): doctypes = [] From 81e5662c587ad5085b713649a5d74e3c599e1c77 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 10 Nov 2020 18:55:53 +0530 Subject: [PATCH 34/49] fix: travis and hook for updating hold time on communication --- erpnext/hooks.py | 3 + erpnext/support/doctype/issue/test_issue.py | 2 +- .../service_level_agreement.py | 61 +++++++++++++------ 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c48cd708dc55..4a2404f0e824 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -234,6 +234,9 @@ "on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions", "erpnext.portal.utils.set_default_role"] }, + "Communication": { + "on_update": "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time" + }, ("Sales Taxes and Charges Template", 'Price List'): { "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" }, diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index c962dc6b3174..f687570cbe52 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -69,7 +69,7 @@ def test_response_time_and_resolution_time_based_on_different_sla(self): self.assertEquals(issue.resolution_by, datetime.datetime(2019, 3, 6, 12, 0)) frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 0) - + issue.reload() issue.status = 'Closed' issue.save() 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 aa3dc2accbd1..020c08bde795 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,6 +6,7 @@ import frappe from frappe.model.document import Document from frappe import _ +from frappe.core.utils import get_parent_doc from frappe.utils import time_diff_in_seconds, getdate, get_weekdays, add_to_date, get_time, get_datetime, \ get_time_zone, to_timedelta, get_datetime_str, get_link_to_form from datetime import datetime @@ -302,8 +303,8 @@ def apply(doc, method=None): if not doc.creation: doc.creation = now_datetime(doc.get("owner")) - if meta.has_field("service_level_agreement_creation"): - doc.service_level_agreement_creation = now_datetime(doc.get("owner")) + if meta.has_field("service_level_agreement_creation"): + doc.service_level_agreement_creation = now_datetime(doc.get("owner")) start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) @@ -330,7 +331,7 @@ def update_status(doc, from_db, meta): if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": set_service_level_agreement_variance(doc.doctype, doc.name) - update_agreement_status(doc, from_db, meta) + update_agreement_status(doc, meta) set_resolution_time(doc, meta) set_user_resolution_time(doc, meta) @@ -407,23 +408,23 @@ def set_service_level_agreement_variance(doctype, doc=None): if doc: filters = {"name": doc} - for doc in frappe.get_all(doctype, filters=filters): - doc = frappe.get_doc(doctype, doc.name) - current_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + for entry in frappe.get_all(doctype, filters=filters): + current_doc = frappe.get_doc(doctype, entry.name) + current_time = frappe.flags.current_time or now_datetime(current_doc.get("owner")) - if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer - variance = round(time_diff_in_seconds(doc.response_by, current_time), 2) - frappe.db.set_value(doc.doctype, doc.name, "response_by_variance", variance, update_modified=False) + if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer + variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2) + frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) if variance < 0: - frappe.db.set_value(doc.doctype, doc.name, "agreement_status", "Failed", update_modified=False) + frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) - if not doc.get("resolution_date"): # resolution_date set when issue has been closed - variance = round(time_diff_in_seconds(doc.resolution_by, current_time), 2) - frappe.db.set_value(doc.doctype, doc.name, "resolution_by_variance", variance, update_modified=False) + if not current_doc.get("resolution_date"): # resolution_date set when issue has been closed + variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2) + frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) if variance < 0: - frappe.db.set_value(doc.doctype, doc.name, "agreement_status", "Failed", update_modified=False) + frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) def set_user_resolution_time(doc, meta): @@ -512,6 +513,25 @@ def set_resolution_time(doc, meta): doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) +# called via hooks on communication update +def update_hold_time(doc, status): + parent = get_parent_doc(doc) + if not parent: + return + + if doc.communication_type == "Comment": + return + + status_field = parent.meta.get_field("status") + if status_field: + options = (status_field.options or "").splitlines() + + # if status has a "Replied" option, then handle hold time + if ("Replied" in options) and doc.sent_or_received == "Received": + meta = frappe.get_meta(parent.doctype) + handle_hold_time(parent, meta, 'Replied') + + def handle_hold_time(doc, meta, status): if meta.has_field("service_level_agreement") and doc.service_level_agreement: # set response and resolution variance as None as the issue is on Hold for status as Replied @@ -680,16 +700,17 @@ def update_agreement_status_on_custom_status(doc): doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" -def update_agreement_status(doc, from_db, meta): +def update_agreement_status(doc, meta): if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \ doc.service_level_agreement and doc.agreement_status == "Ongoing": - if (meta.has_field("response_by_variance") and from_db.response_by_variance < 0) or \ - (meta.has_field("resolution_by_variance") and from_db.resolution_by_variance < 0): + if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"): + if frappe.db.get_value(doc.doctype, doc.name, "response_by_variance") < 0 or \ + frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance") < 0: - doc.agreement_status = "Failed" - else: - doc.agreement_status = "Fulfilled" + doc.agreement_status = "Failed" + else: + doc.agreement_status = "Fulfilled" def is_holiday(date, holidays): From 18762e6e376a9ed1e50bc15b25effe3c2ed8ef6d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 26 May 2021 17:44:04 +0530 Subject: [PATCH 35/49] fix: sider issues --- erpnext/hooks.py | 4 +-- .../service_level_agreement.py | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 264bf26772f4..22458eb4a7b3 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -228,7 +228,7 @@ doc_events = { "*": { - "validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply" + "validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply", "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", "on_update_after_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record", "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record" @@ -337,7 +337,7 @@ "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", - "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance" + "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", 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 020c08bde795..4a51104c3879 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -68,19 +68,23 @@ def validate_doc(self): frappe.throw(_("{0} is not enabled in {1}").format(frappe.bold("Track Service Level Agreement"), get_link_to_form("Support Settings", "Support Settings"))) - if self.default_service_level_agreement and frappe.db.exists("Service Level Agreement", \ - {"document_type": self.document_type, "default_service_level_agreement": "1", "name": ["!=", self.name]}): - + if self.default_service_level_agreement and frappe.db.exists("Service Level Agreement", { + "document_type": self.document_type, + "default_service_level_agreement": "1", + "name": ["!=", self.name] + }): frappe.throw(_("Default Service Level Agreement for {0} already exists.").format(self.document_type)) if self.start_date and self.end_date: self.validate_from_to_dates(self.start_date, self.end_date) - if self.entity_type and self.entity and frappe.db.exists("Service Level Agreement", {"entity_type": self.entity_type, \ - "entity": self.entity, "name": ["!=", self.name]}): - - frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format(frappe.bold(self.entity_type), \ - frappe.bold(self.entity))) + if self.entity_type and self.entity and frappe.db.exists("Service Level Agreement", { + "entity_type": self.entity_type, + "entity": self.entity, + "name": ["!=", self.name] + }): + frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format( + frappe.bold(self.entity_type), frappe.bold(self.entity))) def validate_status_field(self): meta = frappe.get_meta(self.document_type) @@ -381,7 +385,9 @@ def get_expected_time_for(parameter, service_level, start_date_time): time_left_today = time_diff_in_seconds(end_time, start_time) # no time left for support today - if time_left_today <= 0: pass + 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)) @@ -589,7 +595,7 @@ def handle_hold_time(doc, meta, status): def get_service_level_agreement_fields(): - return [ + return [ { "collapsible": 1, "fieldname": "service_level_section", From a350dbe17ba030053663177ad9311218e78a6c3c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 26 May 2021 18:38:04 +0530 Subject: [PATCH 36/49] fix: add code lost in merge conflict resolution --- erpnext/public/js/utils.js | 170 ++++++++---------- erpnext/support/doctype/issue/issue.js | 31 ++++ .../service_level_agreement.js | 4 +- .../service_level_agreement.py | 6 +- 4 files changed, 116 insertions(+), 95 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 2a5f737b1694..0c8582c51b54 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -754,103 +754,91 @@ $(document).on('app_ready', function() { frappe.call({ method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_sla_doctypes', callback: function(r) { - if (r.message) { - $.each(r.message, function(_i, d) { - frappe.ui.form.on(d, { - onload: function(frm) { - if (frm.doc.service_level_agreement) { - frappe.call({ - method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters', - args: { - doctype: frm.doc.doctype, - name: frm.doc.service_level_agreement, - customer: frm.doc.customer - }, - callback: function (r) { - if (r && r.message) { - frm.set_query('priority', function() { - return { - filters: { - 'name': ['in', r.message.priority], - } - }; - }); - frm.set_query('service_level_agreement', function() { - return { - filters: { - 'name': ['in', r.message.service_level_agreements], - } - }; - }); - } - } - }); - } - }, - - refresh: function(frm) { - if (frm.doc.status !== 'Closed' && frm.doc.agreement_status === 'Ongoing') { - if (frm.doc.service_level_agreement) { - frappe.call({ - 'method': 'frappe.client.get', - args: { - doctype: 'Service Level Agreement', - name: frm.doc.service_level_agreement - }, - callback: function(data) { - let statuses = data.message.pause_sla_on; - const hold_statuses = []; - $.each(statuses, (_i, entry) => { - hold_statuses.push(entry.status); - }); - if (hold_statuses.includes(frm.doc.status)) { - frm.dashboard.clear_headline(); - let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])}; - frm.dashboard.set_headline_alert( - '
' + - '
' + - ''+ message.msg +' ' + - '
' + - '
' - ); - } else { - set_time_to_resolve_and_response(frm); + if (!r.message) + return; + + $.each(r.message, function(_i, d) { + frappe.ui.form.on(d, { + onload: function(frm) { + if (!frm.doc.service_level_agreement) + return; + + frappe.call({ + method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters', + args: { + doctype: frm.doc.doctype, + name: frm.doc.service_level_agreement, + customer: frm.doc.customer + }, + callback: function (r) { + if (r && r.message) { + frm.set_query('priority', function() { + return { + filters: { + 'name': ['in', r.message.priority], } - } + }; + }); + frm.set_query('service_level_agreement', function() { + return { + filters: { + 'name': ['in', r.message.service_level_agreements], + } + }; }); } + } + }); + }, - frm.add_custom_button(__('Close'), function () { - frm.set_value('status', 'Closed'); - frm.save(); - }); - - } else { - if (frm.doc.service_level_agreement) { - frm.dashboard.clear_headline(); - - let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ? - {'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} : - {'indicator': 'red', 'msg': 'Service Level Agreement Failed'}; - - frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' - ); + refresh: function(frm) { + if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement + && frm.doc.agreement_status === 'Ongoing') { + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Service Level Agreement', + name: frm.doc.service_level_agreement + }, + callback: function(data) { + let statuses = data.message.pause_sla_on; + const hold_statuses = []; + $.each(statuses, (_i, entry) => { + hold_statuses.push(entry.status); + }); + if (hold_statuses.includes(frm.doc.status)) { + frm.dashboard.clear_headline(); + let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])}; + frm.dashboard.set_headline_alert( + '
' + + '
' + + ''+ message.msg +' ' + + '
' + + '
' + ); + } else { + set_time_to_resolve_and_response(frm); + } } - - frm.add_custom_button(__('Reopen'), function () { - frm.set_value('status', 'Open'); - frm.save(); - }); - } - }, - }); + }); + } else if (frm.doc.service_level_agreement) { + frm.dashboard.clear_headline(); + + let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ? + {'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} : + {'indicator': 'red', 'msg': 'Service Level Agreement Failed'}; + + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, }); - } + }); } }); }); diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 78d1eadfbd45..9ac1efa268d0 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -8,6 +8,37 @@ frappe.ui.form.on("Issue", { } }; }); + + frappe.db.get_value("Support Settings", {name: "Support Settings"}, + ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { + if (r && r.track_service_level_agreement == "0") { + frm.set_df_property("service_level_section", "hidden", 1); + } + if (r && r.allow_resetting_service_level_agreement == "0") { + frm.set_df_property("reset_service_level_agreement", "hidden", 1); + } + }); + + // buttons + if (frm.doc.status !== "Closed") { + frm.add_custom_button(__("Close"), function() { + frm.set_value("status", "Closed"); + frm.save(); + }); + + frm.add_custom_button(__("Task"), function() { + frappe.model.open_mapped_doc({ + method: "erpnext.support.doctype.issue.issue.make_task", + frm: frm + }); + }, __("Create")); + + } else { + frm.add_custom_button(__("Reopen"), function() { + frm.set_value("status", "Open"); + frm.save(); + }); + } }, reset_service_level_agreement: function(frm) { 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 41fab03060e6..f9189856ea37 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -19,7 +19,9 @@ frappe.ui.form.on('Service Level Agreement', { let statuses = frappe.meta.get_docfield(frm.doc.document_type, '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); + frm.fields_dict.pause_sla_on.grid.update_docfield_property( + 'status', 'options', [''].concat(allow_statuses) + ); }); } 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 4a51104c3879..c7c8bf3009dc 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -8,7 +8,7 @@ from frappe import _ from frappe.core.utils import get_parent_doc from frappe.utils import time_diff_in_seconds, getdate, get_weekdays, add_to_date, get_time, get_datetime, \ - get_time_zone, to_timedelta, get_datetime_str, get_link_to_form + get_time_zone, to_timedelta, get_datetime_str, get_link_to_form, cint from datetime import datetime from erpnext.support.doctype.issue.issue import get_holidays @@ -711,8 +711,8 @@ def update_agreement_status(doc, meta): doc.service_level_agreement and doc.agreement_status == "Ongoing": if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"): - if frappe.db.get_value(doc.doctype, doc.name, "response_by_variance") < 0 or \ - frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance") < 0: + if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \ + cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0: doc.agreement_status = "Failed" else: From ce331df2c891871bbd9c1e6b171da9601519af51 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 27 May 2021 00:04:29 +0530 Subject: [PATCH 37/49] feat: Ability to configure SLA Fulfilled On statuses --- erpnext/hooks.py | 1 - erpnext/patches/v13_0/add_doctype_to_sla.py | 6 +++- .../service_level_agreement.js | 10 +++++- .../service_level_agreement.json | 17 +++++++--- .../service_level_agreement.py | 28 ++++++++++------- .../sla_fulfilled_on_status/__init__.py | 0 .../sla_fulfilled_on_status.json | 31 +++++++++++++++++++ .../sla_fulfilled_on_status.py | 8 +++++ 8 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 erpnext/support/doctype/sla_fulfilled_on_status/__init__.py create mode 100644 erpnext/support/doctype/sla_fulfilled_on_status/sla_fulfilled_on_status.json create mode 100644 erpnext/support/doctype/sla_fulfilled_on_status/sla_fulfilled_on_status.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 22458eb4a7b3..b2c40afabff2 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -334,7 +334,6 @@ "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", - "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance" diff --git a/erpnext/patches/v13_0/add_doctype_to_sla.py b/erpnext/patches/v13_0/add_doctype_to_sla.py index a62eea112fa9..8566c78e3215 100644 --- a/erpnext/patches/v13_0/add_doctype_to_sla.py +++ b/erpnext/patches/v13_0/add_doctype_to_sla.py @@ -12,4 +12,8 @@ def execute(): rename_field('Service Level Agreement', 'enable', 'enabled') for sla in frappe.get_all('Service Level Agreement'): - frappe.db.set_value('Service Level Agreement', sla.name, 'document_type', 'Issue') \ No newline at end of file + agreement = frappe.get_doc('Service Level Agreement', sla.name) + agreement.document_type = 'Issue' + agreement.append('sla_fulfilled_on', {'status': 'Resolved'}) + agreement.append('sla_fulfilled_on', {'status': 'Closed'}) + agreement.save() \ No newline at end of file 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 f9189856ea37..66af140e873b 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -22,6 +22,10 @@ frappe.ui.form.on('Service Level Agreement', { frm.fields_dict.pause_sla_on.grid.update_docfield_property( 'status', 'options', [''].concat(allow_statuses) ); + + frm.fields_dict.sla_fulfilled_on.grid.update_docfield_property( + 'status', 'options', [''].concat(statuses) + ); }); } @@ -30,10 +34,14 @@ frappe.ui.form.on('Service Level Agreement', { onload: function(frm) { frm.set_query("document_type", function() { + let invalid_doctypes = frappe.model.core_doctypes_list; + invalid_doctypes.push(frm.doc.doctype, 'Cost Center', 'Company'); + return { filters: [ ['DocType', 'issingle', '=', 0], - ['DocType', 'name', 'not in', frappe.model.core_doctypes_list], + ['DocType', 'istable', '=', 0], + ['DocType', 'name', 'not in', invalid_doctypes], ['DocType', 'module', 'not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] ] }; 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 d88582ecbd6b..04bd8d3302f4 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -23,7 +23,8 @@ "column_break_7", "end_date", "section_break_18", - "pause_sla_on_section", + "status_details", + "sla_fulfilled_on", "pause_sla_on", "response_and_resolution_time_section", "priorities", @@ -147,7 +148,7 @@ { "fieldname": "pause_sla_on", "fieldtype": "Table", - "label": "Pause SLA On", + "label": "SLA Paused On", "options": "Pause SLA On Status" }, { @@ -164,13 +165,19 @@ "label": "Enabled" }, { - "fieldname": "pause_sla_on_section", + "fieldname": "status_details", "fieldtype": "Section Break", - "label": "Pause SLA On" + "label": "Status Details" + }, + { + "fieldname": "sla_fulfilled_on", + "fieldtype": "Table", + "label": "SLA Fulfilled On", + "options": "SLA Fulfilled On Status" } ], "links": [], - "modified": "2020-11-09 23:52:04.126930", + "modified": "2021-05-26 21:14:48.520555", "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 c7c8bf3009dc..d8644bd8206a 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -141,7 +141,7 @@ def create_docfields(self, meta, service_level_agreement_fields): }).insert(ignore_permissions=True) else: existing_field = meta.get_field(field.get("fieldname")) - self.reset_field_properties(existing_field, field) + self.reset_field_properties(existing_field, "DocField", field) def create_custom_fields(self, meta, service_level_agreement_fields): for field in service_level_agreement_fields: @@ -162,9 +162,9 @@ def create_custom_fields(self, meta, service_level_agreement_fields): }).insert(ignore_permissions=True) else: existing_field = meta.get_field(field.get("fieldname")) - self.reset_field_properties(existing_field, field) + self.reset_field_properties(existing_field, "Custom Field", field) - def reset_field_properties(self, field, sla_field): + def reset_field_properties(self, field, field_dt, sla_field): field = frappe.get_doc(field.doctype, field.name) field.label = sla_field.get("label") field.fieldname = sla_field.get("fieldname") @@ -329,16 +329,22 @@ def update_status(doc, from_db, meta): from_db.status == "Open" and not doc.first_responded_on: doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) - if doc.status in ["Resolved", "Closed"] and from_db.status not in ["Resolved", "Closed"]: - if meta.has_field("resolution_date"): - doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) + if meta.has_field("service_level_agreement") and doc.service_level_agreement: + # mark sla status as fulfilled based on the configuration + fulfillment_statuses = [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={ + "parent": doc.service_level_agreement + }, fields=["status"])] + + if doc.status in fulfillment_statuses and from_db.status not in fulfillment_statuses: + if meta.has_field("resolution_date"): + doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": - set_service_level_agreement_variance(doc.doctype, doc.name) - update_agreement_status(doc, meta) + if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": + set_service_level_agreement_variance(doc.doctype, doc.name) + update_agreement_status(doc, meta) - set_resolution_time(doc, meta) - set_user_resolution_time(doc, meta) + set_resolution_time(doc, meta) + set_user_resolution_time(doc, meta) if doc.status == "Open" and from_db.status != "Open": # if no date, it should be set as None and not a blank string "", as per mysql strict config diff --git a/erpnext/support/doctype/sla_fulfilled_on_status/__init__.py b/erpnext/support/doctype/sla_fulfilled_on_status/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/support/doctype/sla_fulfilled_on_status/sla_fulfilled_on_status.json b/erpnext/support/doctype/sla_fulfilled_on_status/sla_fulfilled_on_status.json new file mode 100644 index 000000000000..87124deaf8ba --- /dev/null +++ b/erpnext/support/doctype/sla_fulfilled_on_status/sla_fulfilled_on_status.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-26 21:11:29.176369", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status" + ], + "fields": [ + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2021-05-26 21:11:29.176369", + "modified_by": "Administrator", + "module": "Support", + "name": "SLA Fulfilled 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/sla_fulfilled_on_status/sla_fulfilled_on_status.py b/erpnext/support/doctype/sla_fulfilled_on_status/sla_fulfilled_on_status.py new file mode 100644 index 000000000000..b0b5ffc81651 --- /dev/null +++ b/erpnext/support/doctype/sla_fulfilled_on_status/sla_fulfilled_on_status.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class SLAFulfilledOnStatus(Document): + pass From ab616e2a80a8ef49a36bc9bf15075f4dced1764a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 28 May 2021 11:01:15 +0530 Subject: [PATCH 38/49] fix: tests --- .../service_level_agreement/service_level_agreement.js | 9 +++++++-- .../service_level_agreement/service_level_agreement.json | 8 ++++---- .../service_level_agreement/service_level_agreement.py | 3 ++- .../test_service_level_agreement.py | 8 +++++--- 4 files changed, 18 insertions(+), 10 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 66af140e873b..ed8fe4f652ac 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -12,19 +12,24 @@ frappe.ui.form.on('Service Level Agreement', { fetch_status_fields: function(frm) { let allow_statuses = []; - const exclude_statuses = ['Open', 'Closed']; + let exclude_statuses = []; if (frm.doc.document_type) { frappe.model.with_doctype(frm.doc.document_type, () => { let statuses = frappe.meta.get_docfield(frm.doc.document_type, 'status', frm.doc.name).options; statuses = statuses.split('\n'); + + exclude_statuses = ['Open', 'Closed']; allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status)); + frm.fields_dict.pause_sla_on.grid.update_docfield_property( 'status', 'options', [''].concat(allow_statuses) ); + exclude_statuses = ['Open', 'Replied']; + allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status)); frm.fields_dict.sla_fulfilled_on.grid.update_docfield_property( - 'status', 'options', [''].concat(statuses) + 'status', 'options', [''].concat(allow_statuses) ); }); } 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 04bd8d3302f4..73f5c320e653 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -81,12 +81,11 @@ { "fieldname": "support_and_resolution_section_break", "fieldtype": "Section Break", - "label": "Support Hours" + "label": "Working Hours" }, { "fieldname": "support_and_resolution", "fieldtype": "Table", - "label": "Support and Resolution", "options": "Service Day", "reqd": 1 }, @@ -173,11 +172,12 @@ "fieldname": "sla_fulfilled_on", "fieldtype": "Table", "label": "SLA Fulfilled On", - "options": "SLA Fulfilled On Status" + "options": "SLA Fulfilled On Status", + "reqd": 1 } ], "links": [], - "modified": "2021-05-26 21:14:48.520555", + "modified": "2021-05-28 10:39:09.877198", "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 d8644bd8206a..ce7b5c92dbea 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -64,7 +64,8 @@ def check_support_and_resolution(self): 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") and self.enabled: + if self.enabled and self.document_type == "Issue" \ + and not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): frappe.throw(_("{0} is not enabled in {1}").format(frappe.bold("Track Service Level Agreement"), get_link_to_form("Support Settings", "Support Settings"))) 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 dab49c77ad82..7886e80a334d 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 @@ -111,10 +111,12 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "resolution_time_period": "Hour", } ], + "sla_fulfilled_on": [ + {"status": "Resolved"}, + {"status": "Closed"} + ], "pause_sla_on": [ - { - "status": "Replied" - } + {"status": "Replied"} ], "support_and_resolution": [ { From 352f0cde724221e4012156927dc96d62915cd2f6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 29 May 2021 12:20:49 +0530 Subject: [PATCH 39/49] test: added more tests for SLA - custom field creation for SLA on standard dt - docfield creation for SLA on custom dt - SLA application - hold time calculation --- .../service_level_agreement.py | 14 +- .../test_service_level_agreement.py | 175 +++++++++++++++++- 2 files changed, 174 insertions(+), 15 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 ce7b5c92dbea..70e7d5805240 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -166,7 +166,7 @@ def create_custom_fields(self, meta, service_level_agreement_fields): self.reset_field_properties(existing_field, "Custom Field", field) def reset_field_properties(self, field, field_dt, sla_field): - field = frappe.get_doc(field.doctype, field.name) + field = frappe.get_doc(field_dt, {"fieldname": field.fieldname}) field.label = sla_field.get("label") field.fieldname = sla_field.get("fieldname") field.fieldtype = sla_field.get("fieldtype") @@ -193,7 +193,7 @@ def check_agreement_status(): def get_active_service_level_agreement_for(doctype, priority, customer=None, service_level_agreement=None): - if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): + if doctype == "Issue" and not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): return filters = [ @@ -294,6 +294,11 @@ def apply(doc, method=None): if not service_level_agreement: return + if frappe.db.exists(doc.doctype, doc.name): + from_db = frappe.get_doc(doc.doctype, doc.name) + else: + from_db = frappe._dict({}) + meta = frappe.get_meta(doc.doctype) if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \ @@ -316,11 +321,6 @@ def apply(doc, method=None): set_response_by_and_variance(doc, meta, start_date_time, priority) set_resolution_by_and_variance(doc, meta, start_date_time, priority) - if frappe.db.exists(doc.doctype, doc.name): - from_db = frappe.get_doc(doc.doctype, doc.name) - else: - from_db = frappe._dict({}) - update_status(doc, from_db, meta) 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 7886e80a334d..dddce75b75f5 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,12 +5,16 @@ import frappe import unittest +import datetime +from frappe.utils import flt from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities +from erpnext.support.doctype.service_level_agreement.service_level_agreement import get_service_level_agreement_fields class TestServiceLevelAgreement(unittest.TestCase): def setUp(self): frappe.db.sql("delete from `tabService Level Agreement`") frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) + frappe.db.sql("delete from `tabLead`") def test_service_level_agreement(self): # Default Service Level Agreement @@ -60,9 +64,111 @@ def test_service_level_agreement(self): self.assertEqual(create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity) self.assertEqual(create_territory_service_level_agreement.default_service_level_agreement, get_territory_service_level_agreement.default_service_level_agreement) -def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None): + def test_custom_field_creation_for_sla_on_standard_dt(self): + # Default Service Level Agreement + doctype = "Lead" + lead_sla = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, entity=None, + response_time=14400, resolution_time=21600, + doctype=doctype + ) + + # check default SLA for lead + default_sla = get_service_level_agreement(default_service_level_agreement=1, doctype=doctype) + self.assertEqual(lead_sla.name, default_sla.name) + + # check SLA custom fields created for leads + sla_fields = get_service_level_agreement_fields() + meta = frappe.get_meta(doctype) + + for field in sla_fields: + self.assertTrue(meta.has_field(field.get("fieldname"))) + + def test_docfield_creation_for_sla_on_custom_dt(self): + doctype = create_custom_doctype() + sla = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, entity=None, + response_time=14400, resolution_time=21600, + doctype=doctype.name + ) + + # check default SLA for custom dt + default_sla = get_service_level_agreement(default_service_level_agreement=1, doctype=doctype.name) + self.assertEqual(sla.name, sla.name) + + # check SLA docfields created + sla_fields = get_service_level_agreement_fields() + meta = frappe.get_meta(doctype.name) + + for field in sla_fields: + self.assertTrue(meta.has_field(field.get("fieldname"))) + + def test_sla_application(self): + # Default Service Level Agreement + doctype = "Lead" + lead_sla = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, entity=None, + response_time=14400, resolution_time=21600, + doctype=doctype, + sla_fulfilled_on=[{"status": "Converted"}] + ) + + # make lead with default SLA + creation = datetime.datetime(2019, 3, 4, 12, 0) + lead = make_lead(creation=creation, index=1) + + self.assertEqual(lead.service_level_agreement, lead_sla.name) + self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) + self.assertEqual(lead.resolution_by, datetime.datetime(2019, 3, 4, 18, 0)) + + frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 0) + lead.reload() + lead.status = 'Converted' + lead.save() + + self.assertEqual(lead.agreement_status, 'Fulfilled') + + def test_hold_time(self): + doctype = "Lead" + lead_sla = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, entity=None, + response_time=14400, resolution_time=21600, + doctype=doctype, + sla_fulfilled_on=[{"status": "Converted"}] + ) + + creation = datetime.datetime(2020, 3, 4, 4, 0) + lead = make_lead(creation, index=2) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15) + lead.reload() + lead.status = 'Replied' + lead.save() + + lead.reload() + self.assertEqual(lead.on_hold_since, frappe.flags.current_time) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) + lead.reload() + lead.status = 'Converted' + lead.save() + + lead.reload() + self.assertEqual(flt(lead.total_hold_time, 2), 3000) + self.assertEqual(lead.resolution_by, datetime.datetime(2020, 3, 4, 16, 50)) + + +def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None, doctype="Issue"): if default_service_level_agreement: - filters = {"default_service_level_agreement": default_service_level_agreement} + filters = {"default_service_level_agreement": default_service_level_agreement, "document_type": doctype} else: filters = {"entity_type": entity_type, "entity": entity} @@ -70,15 +176,21 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ return service_level_agreement def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type, - entity, resolution_time): + entity, resolution_time, doctype="Issue", sla_fulfilled_on=[]): make_holiday_list() make_priorities() + if not sla_fulfilled_on: + sla_fulfilled_on = [ + {"status": "Resolved"}, + {"status": "Closed"} + ] + service_level_agreement = frappe.get_doc({ "doctype": "Service Level Agreement", "enabled": 1, - "document_type": "Issue", + "document_type": doctype, "service_level": "__Test Service Level", "default_service_level_agreement": default_service_level_agreement, "default_priority": "Medium", @@ -111,10 +223,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "resolution_time_period": "Hour", } ], - "sla_fulfilled_on": [ - {"status": "Resolved"}, - {"status": "Closed"} - ], + "sla_fulfilled_on": sla_fulfilled_on, "pause_sla_on": [ {"status": "Replied"} ], @@ -254,3 +363,53 @@ def make_holiday_list(): }, ] }).insert() + +def create_custom_doctype(): + if not frappe.db.exists("DocType", "Test SLA on Custom Dt"): + doc = frappe.get_doc({ + "doctype": "DocType", + "module": "Support", + "custom": 1, + "fields": [{ + "label": "Date", + "fieldname": "date", + "fieldtype": "Date" + }, + { + "label": "Description", + "fieldname": "desc", + "fieldtype": "Long Text" + }, + { + "label": "Email ID", + "fieldname": "email_id", + "fieldtype": "Link", + "options": "Customer" + }, + { + "label": "Status", + "fieldname": "status", + "fieldtype": "Select", + "options": "Open\nReplied\nClosed" + }], + "permissions": [{ + "role": "System Manager", + "read": 1, + "write": 1 + }], + "name": "Test SLA on Custom Dt", + }) + doc.insert() + return doc + else: + return frappe.get_doc("DocType", "Test SLA on Custom Dt") + +def make_lead(creation=None, index=0): + return frappe.get_doc({ + "doctype": "Lead", + "email_id": "test_lead1@example{0}.com".format(index), + "lead_name": "_Test Lead {0}".format(index), + "status": "Open", + "creation": creation, + "service_level_agreement_creation": creation + }).insert(ignore_permissions=True) \ No newline at end of file From 4db87cda412b30b1c078fe622d7ac374225086ce Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 29 May 2021 12:27:12 +0530 Subject: [PATCH 40/49] fix: sider --- .../test_service_level_agreement.py | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 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 dddce75b75f5..720a1158c0df 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 @@ -98,7 +98,7 @@ def test_docfield_creation_for_sla_on_custom_dt(self): # check default SLA for custom dt default_sla = get_service_level_agreement(default_service_level_agreement=1, doctype=doctype.name) - self.assertEqual(sla.name, sla.name) + self.assertEqual(sla.name, default_sla.name) # check SLA docfields created sla_fields = get_service_level_agreement_fields() @@ -136,7 +136,7 @@ def test_sla_application(self): def test_hold_time(self): doctype = "Lead" - lead_sla = create_service_level_agreement( + create_service_level_agreement( default_service_level_agreement=1, holiday_list="__Test Holiday List", entity_type=None, entity=None, @@ -370,28 +370,30 @@ def create_custom_doctype(): "doctype": "DocType", "module": "Support", "custom": 1, - "fields": [{ - "label": "Date", - "fieldname": "date", - "fieldtype": "Date" - }, - { - "label": "Description", - "fieldname": "desc", - "fieldtype": "Long Text" - }, - { - "label": "Email ID", - "fieldname": "email_id", - "fieldtype": "Link", - "options": "Customer" - }, - { - "label": "Status", - "fieldname": "status", - "fieldtype": "Select", - "options": "Open\nReplied\nClosed" - }], + "fields": [ + { + "label": "Date", + "fieldname": "date", + "fieldtype": "Date" + }, + { + "label": "Description", + "fieldname": "desc", + "fieldtype": "Long Text" + }, + { + "label": "Email ID", + "fieldname": "email_id", + "fieldtype": "Link", + "options": "Customer" + }, + { + "label": "Status", + "fieldname": "status", + "fieldtype": "Select", + "options": "Open\nReplied\nClosed" + } + ], "permissions": [{ "role": "System Manager", "read": 1, From ee5c60a30fa08b2ed7eb9218e02739ad85ce13b0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 29 May 2021 13:19:51 +0530 Subject: [PATCH 41/49] refactor: shorter functions for sla application --- .../service_level_agreement.py | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 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 70e7d5805240..225f535532bb 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -103,6 +103,10 @@ def get_service_level_agreement_priority(self, priority): }) def before_insert(self): + # no need to set up SLA fields for Issue dt as they are standard fields in Issue + if self.document_type == "Issue": + return + service_level_agreement_fields = get_service_level_agreement_fields() meta = frappe.get_meta(self.document_type) @@ -200,22 +204,19 @@ def get_active_service_level_agreement_for(doctype, priority, customer=None, ser ["Service Level Agreement", "document_type", "=", doctype], ["Service Level Agreement", "enabled", "=", 1] ] - if priority: filters.append(["Service Level Priority", "priority", "=", priority]) or_filters = [] - - if customer: - or_filters.append( - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] - ) - if service_level_agreement: or_filters = [ ["Service Level Agreement", "name", "=", service_level_agreement], ] + if customer: + or_filters.append( + ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] + ) or_filters.append(["Service Level Agreement", "default_service_level_agreement", "=", 1]) agreement = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters, @@ -294,6 +295,10 @@ def apply(doc, method=None): if not service_level_agreement: return + set_sla_properties(doc, service_level_agreement) + + +def set_sla_properties(doc, service_level_agreement): if frappe.db.exists(doc.doctype, doc.name): from_db = frappe.get_doc(doc.doctype, doc.name) else: @@ -359,25 +364,11 @@ def update_status(doc, from_db, meta): def get_expected_time_for(parameter, service_level, start_date_time): current_date_time = start_date_time expected_time = current_date_time - start_time = None - end_time = None - - if parameter == "response": - allotted_seconds = service_level.get("response_time") - elif parameter == "resolution": - allotted_seconds = service_level.get("resolution_time") - else: - frappe.throw(_("{0} parameter is invalid").format(parameter)) - + start_time = end_time = None expected_time_is_set = 0 - support_days = {} - for service in service_level.get("support_and_resolution"): - support_days[service.workday] = frappe._dict({ - "start_time": service.start_time, - "end_time": service.end_time, - }) - + allotted_seconds = get_allotted_seconds(parameter, service_level) + support_days = get_support_days(service_level) holidays = get_holidays(service_level.get("holiday_list")) weekdays = get_weekdays() @@ -385,12 +376,14 @@ def get_expected_time_for(parameter, service_level, start_date_time): current_weekday = weekdays[current_date_time.weekday()] if not is_holiday(current_date_time, holidays) and current_weekday in support_days: - start_time = current_date_time - datetime(current_date_time.year, current_date_time.month, current_date_time.day) \ - 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 + if getdate(current_date_time) == getdate(start_date_time) \ + and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time: + start_time = current_date_time - datetime(current_date_time.year, current_date_time.month, current_date_time.day) + else: + start_time = support_days[current_weekday].start_time + end_time = support_days[current_weekday].end_time time_left_today = time_diff_in_seconds(end_time, start_time) - # no time left for support today if time_left_today <= 0: pass @@ -414,6 +407,28 @@ def get_expected_time_for(parameter, service_level, start_date_time): return current_date_time +def get_allotted_seconds(parameter, service_level): + allotted_seconds = 0 + if parameter == "response": + allotted_seconds = service_level.get("response_time") + elif parameter == "resolution": + allotted_seconds = service_level.get("resolution_time") + else: + frappe.throw(_("{0} parameter is invalid").format(parameter)) + + return allotted_seconds + + +def get_support_days(service_level): + 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, + }) + return support_days + + def set_service_level_agreement_variance(doctype, doc=None): filters = {"status": "Open", "agreement_status": "Ongoing"} From a37b0aaa085791026184b50e255710f4526959dc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 29 May 2021 17:15:13 +0530 Subject: [PATCH 42/49] feat: Apply SLA for Resolution configuration --- erpnext/patches/v13_0/add_doctype_to_sla.py | 1 + erpnext/public/js/utils.js | 43 +++++++++------ .../service_level_agreement.js | 13 ++++- .../service_level_agreement.json | 19 ++++--- .../service_level_agreement.py | 55 +++++++++++++------ .../test_service_level_agreement.py | 5 +- .../service_level_priority.json | 5 +- 7 files changed, 93 insertions(+), 48 deletions(-) diff --git a/erpnext/patches/v13_0/add_doctype_to_sla.py b/erpnext/patches/v13_0/add_doctype_to_sla.py index 8566c78e3215..35407785ca9a 100644 --- a/erpnext/patches/v13_0/add_doctype_to_sla.py +++ b/erpnext/patches/v13_0/add_doctype_to_sla.py @@ -14,6 +14,7 @@ def execute(): for sla in frappe.get_all('Service Level Agreement'): agreement = frappe.get_doc('Service Level Agreement', sla.name) agreement.document_type = 'Issue' + agreement.apply_sla_for_resolution = 1 agreement.append('sla_fulfilled_on', {'status': 'Resolved'}) agreement.append('sla_fulfilled_on', {'status': 'Closed'}) agreement.save() \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 0c8582c51b54..d8e46957aa3e 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -817,7 +817,7 @@ $(document).on('app_ready', function() { '' ); } else { - set_time_to_resolve_and_response(frm); + set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution); } } }); @@ -843,29 +843,40 @@ $(document).on('app_ready', function() { }); }); -function set_time_to_resolve_and_response(frm) { +function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { frm.dashboard.clear_headline(); - var time_to_respond = get_status(frm.doc.response_by_variance); + let time_to_respond = get_status(frm.doc.response_by_variance); if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') { time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status); } - var time_to_resolve = get_status(frm.doc.resolution_by_variance); - if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') { - time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); + let alert = ` +
+
+ + Time to Respond: ${time_to_respond.diff_display} + +
` + + + if (apply_sla_for_resolution) { + let time_to_resolve = get_status(frm.doc.resolution_by_variance); + if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') { + time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); + } + + alert += ` +
+ + Time to Resolve: ${time_to_resolve.diff_display} + +
` } - frm.dashboard.set_headline_alert( - '
' + - '
' + - 'Time to Respond: '+ time_to_respond.diff_display +' ' + - '
' + - '
' + - 'Time to Resolve: '+ time_to_resolve.diff_display +' ' + - '
' + - '
' - ); + alert += '
' + + frm.dashboard.set_headline_alert(alert); } function get_time_left(timestamp, agreement_status) { 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 ed8fe4f652ac..5808d443f672 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -4,6 +4,7 @@ frappe.ui.form.on('Service Level Agreement', { refresh: function(frm) { frm.trigger('fetch_status_fields'); + frm.trigger('toggle_resolution_fields'); }, document_type: function(frm) { @@ -26,7 +27,7 @@ frappe.ui.form.on('Service Level Agreement', { 'status', 'options', [''].concat(allow_statuses) ); - exclude_statuses = ['Open', 'Replied']; + exclude_statuses = ['Open']; allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status)); frm.fields_dict.sla_fulfilled_on.grid.update_docfield_property( 'status', 'options', [''].concat(allow_statuses) @@ -37,6 +38,16 @@ frappe.ui.form.on('Service Level Agreement', { frm.refresh_field('pause_sla_on'); }, + apply_sla_for_resolution: function(frm) { + frm.trigger('toggle_resolution_fields'); + }, + + toggle_resolution_fields: function(frm) { + frm.fields_dict.priorities.grid.set_column_disp('resolution_time', frm.doc.apply_sla_for_resolution); + frm.fields_dict.priorities.grid.toggle_reqd('resolution_time', frm.doc.apply_sla_for_resolution); + frm.fields_dict.priorities.grid.refresh(); + }, + onload: function(frm) { frm.set_query("document_type", function() { let invalid_doctypes = frappe.model.core_doctypes_list; 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 73f5c320e653..61ca3a334e4a 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -22,12 +22,12 @@ "start_date", "column_break_7", "end_date", - "section_break_18", + "response_and_resolution_time_section", + "apply_sla_for_resolution", + "priorities", "status_details", "sla_fulfilled_on", "pause_sla_on", - "response_and_resolution_time_section", - "priorities", "support_and_resolution_section_break", "support_and_resolution" ], @@ -139,11 +139,6 @@ "options": "Issue Priority", "read_only": 1 }, - { - "fieldname": "section_break_18", - "fieldtype": "Section Break", - "hide_border": 1 - }, { "fieldname": "pause_sla_on", "fieldtype": "Table", @@ -174,10 +169,16 @@ "label": "SLA Fulfilled On", "options": "SLA Fulfilled On Status", "reqd": 1 + }, + { + "default": "1", + "fieldname": "apply_sla_for_resolution", + "fieldtype": "Check", + "label": "Apply SLA for Resolution Time" } ], "links": [], - "modified": "2021-05-28 10:39:09.877198", + "modified": "2021-05-29 13:35:41.956849", "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 225f535532bb..9a3dfdbbeb13 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -24,16 +24,19 @@ def check_priorities(self): for priority in self.priorities: # Check if response and resolution time is set for every priority - if not priority.response_time or not priority.resolution_time: - frappe.throw(_("Set Response Time and Resolution Time for Priority {0} in row {1}.").format(priority.priority, priority.idx)) + if not priority.response_time: + frappe.throw(_("Set Response Time for Priority {0} in row {1}.").format(priority.priority, priority.idx)) - priorities.append(priority.priority) + if self.apply_sla_for_resolution: + if not priority.resolution_time: + frappe.throw(_("Set Response Time for Priority {0} in row {1}.").format(priority.priority, priority.idx)) - response = priority.response_time - resolution = priority.resolution_time + response = priority.response_time + resolution = priority.resolution_time + if response > resolution: + frappe.throw(_("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) - if response > resolution: - frappe.throw(_("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) + priorities.append(priority.priority) # Check if repeated priority if not len(set(priorities)) == len(priorities): @@ -108,7 +111,7 @@ def before_insert(self): return service_level_agreement_fields = get_service_level_agreement_fields() - meta = frappe.get_meta(self.document_type) + meta = frappe.get_meta(self.document_type, cached=False) if meta.custom: self.create_docfields(meta, service_level_agreement_fields) @@ -220,7 +223,7 @@ def get_active_service_level_agreement_for(doctype, priority, customer=None, ser or_filters.append(["Service Level Agreement", "default_service_level_agreement", "=", 1]) agreement = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters, - fields=["name", "default_priority"]) + fields=["name", "default_priority", "apply_sla_for_resolution"]) return agreement[0] if agreement else None @@ -324,7 +327,8 @@ def set_sla_properties(doc, service_level_agreement): start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) set_response_by_and_variance(doc, meta, start_date_time, priority) - set_resolution_by_and_variance(doc, meta, start_date_time, priority) + if service_level_agreement.apply_sla_for_resolution: + set_resolution_by_and_variance(doc, meta, start_date_time, priority) update_status(doc, from_db, meta) @@ -342,15 +346,19 @@ def update_status(doc, from_db, meta): }, fields=["status"])] if doc.status in fulfillment_statuses and from_db.status not in fulfillment_statuses: - if meta.has_field("resolution_date"): + apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, + "apply_sla_for_resolution") + + if apply_sla_for_resolution and meta.has_field("resolution_date"): doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": set_service_level_agreement_variance(doc.doctype, doc.name) update_agreement_status(doc, meta) - set_resolution_time(doc, meta) - set_user_resolution_time(doc, meta) + if apply_sla_for_resolution: + set_resolution_time(doc, meta) + set_user_resolution_time(doc, meta) if doc.status == "Open" and from_db.status != "Open": # if no date, it should be set as None and not a blank string "", as per mysql strict config @@ -439,6 +447,8 @@ def set_service_level_agreement_variance(doctype, doc=None): for entry in frappe.get_all(doctype, filters=filters): current_doc = frappe.get_doc(doctype, entry.name) current_time = frappe.flags.current_time or now_datetime(current_doc.get("owner")) + apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", current_doc.service_level_agreement, + "apply_sla_for_resolution") if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2) @@ -447,7 +457,7 @@ def set_service_level_agreement_variance(doctype, doc=None): if variance < 0: frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) - if not current_doc.get("resolution_date"): # resolution_date set when issue has been closed + if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2) frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) @@ -732,10 +742,21 @@ def update_agreement_status(doc, meta): if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \ doc.service_level_agreement and doc.agreement_status == "Ongoing": - if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"): - if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \ - cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0: + apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, + "apply_sla_for_resolution") + # if SLA is applied for resolution check for response and resolution, else only response + if apply_sla_for_resolution: + if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"): + if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \ + cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0: + + doc.agreement_status = "Failed" + else: + doc.agreement_status = "Fulfilled" + else: + if meta.has_field("response_by_variance") and \ + cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0: doc.agreement_status = "Failed" else: doc.agreement_status = "Fulfilled" 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 720a1158c0df..664dc122d531 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 @@ -102,7 +102,7 @@ def test_docfield_creation_for_sla_on_custom_dt(self): # check SLA docfields created sla_fields = get_service_level_agreement_fields() - meta = frappe.get_meta(doctype.name) + meta = frappe.get_meta(doctype.name, cached=False) for field in sla_fields: self.assertTrue(meta.has_field(field.get("fieldname"))) @@ -176,7 +176,7 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ return service_level_agreement def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type, - entity, resolution_time, doctype="Issue", sla_fulfilled_on=[]): + entity, resolution_time, doctype="Issue", sla_fulfilled_on=[], apply_sla_for_resolution=1): make_holiday_list() make_priorities() @@ -199,6 +199,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "entity": entity, "start_date": frappe.utils.getdate(), "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), + "apply_sla_for_resolution": 1, "priorities": [ { "priority": "Low", 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 c166ebbc7fe9..b415a3618a85 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -34,8 +34,7 @@ "hide_days": 1, "hide_seconds": 1, "in_list_view": 1, - "label": "Resolution Time", - "reqd": 1 + "label": "Resolution Time" }, { "fieldname": "cb_00", @@ -66,7 +65,7 @@ ], "istable": 1, "links": [], - "modified": "2020-07-06 13:29:20.183505", + "modified": "2021-05-29 14:24:28.643920", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", From 7bce2fee5f23d795f4440c05ce70fad4ce4eb84a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 29 May 2021 17:21:23 +0530 Subject: [PATCH 43/49] fix: sider --- erpnext/public/js/utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index d8e46957aa3e..db7c034596a6 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -857,7 +857,7 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { Time to Respond: ${time_to_respond.diff_display} - ` + `; if (apply_sla_for_resolution) { @@ -871,10 +871,10 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { Time to Resolve: ${time_to_resolve.diff_display} - ` + `; } - alert += '' + alert += ''; frm.dashboard.set_headline_alert(alert); } From 981c23dbb320b0fb253b4d06d0ff37efbbdab290 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 29 May 2021 19:46:38 +0530 Subject: [PATCH 44/49] test: apply SLA for resolution disabled --- .../service_level_agreement.py | 15 ++- .../test_service_level_agreement.py | 91 +++++++++++++++---- 2 files changed, 83 insertions(+), 23 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 9a3dfdbbeb13..920cfea418a3 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -124,6 +124,9 @@ def on_trash(self): def after_insert(self): set_documents_with_active_service_level_agreement() + def on_update(self): + set_documents_with_active_service_level_agreement() + def create_docfields(self, meta, service_level_agreement_fields): last_index = len(meta.fields) @@ -726,13 +729,14 @@ def update_agreement_status_on_custom_status(doc): # Update Agreement Fulfilled status using Custom Scripts for Custom Status meta = frappe.get_meta(doc.doctype) + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) if meta.has_field("first_responded_on") and not doc.first_responded_on: # first_responded_on set when first reply is sent to customer - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_datetime(doc.get("owner"))), 2) + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) if meta.has_field("resolution_date") and not doc.resolution_date: # resolution_date set when issue has been closed - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_datetime(doc.get("owner"))), 2) + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) if meta.has_field("agreement_status"): doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" @@ -777,15 +781,16 @@ def set_response_by_and_variance(doc, meta, start_date_time, priority): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) if meta.has_field("response_by_variance"): - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_datetime(doc.get("owner")))) - + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) def set_resolution_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) if meta.has_field("resolution_by_variance"): - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_datetime(doc.get("owner")))) + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) def now_datetime(user): 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 664dc122d531..966b65474180 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 @@ -12,7 +12,6 @@ class TestServiceLevelAgreement(unittest.TestCase): def setUp(self): - frappe.db.sql("delete from `tabService Level Agreement`") frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) frappe.db.sql("delete from `tabLead`") @@ -72,7 +71,8 @@ def test_custom_field_creation_for_sla_on_standard_dt(self): holiday_list="__Test Holiday List", entity_type=None, entity=None, response_time=14400, resolution_time=21600, - doctype=doctype + doctype=doctype, + sla_fulfilled_on=[{"status": "Converted"}] ) # check default SLA for lead @@ -142,7 +142,8 @@ def test_hold_time(self): entity_type=None, entity=None, response_time=14400, resolution_time=21600, doctype=doctype, - sla_fulfilled_on=[{"status": "Converted"}] + sla_fulfilled_on=[{"status": "Converted"}], + pause_sla_on=[{"status": "Replied"}] ) creation = datetime.datetime(2020, 3, 4, 4, 0) @@ -165,6 +166,63 @@ def test_hold_time(self): self.assertEqual(flt(lead.total_hold_time, 2), 3000) self.assertEqual(lead.resolution_by, datetime.datetime(2020, 3, 4, 16, 50)) + def test_failed_sla_for_response_only(self): + doctype = "Lead" + create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, entity=None, + response_time=14400, + doctype=doctype, + sla_fulfilled_on=[{"status": "Replied"}], + pause_sla_on=[], + apply_sla_for_resolution=0 + ) + + creation = datetime.datetime(2019, 3, 4, 12, 0) + lead = make_lead(creation=creation, index=1) + self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) + + # failed with response time only + frappe.flags.current_time = datetime.datetime(2019, 3, 4, 16, 5) + lead.reload() + lead.status = 'Replied' + lead.save() + + lead.reload() + self.assertEqual(lead.agreement_status, 'Failed') + + def test_fulfilled_sla_for_response_only(self): + doctype = "Lead" + lead_sla = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, entity=None, + response_time=14400, + doctype=doctype, + sla_fulfilled_on=[{"status": "Replied"}], + apply_sla_for_resolution=0 + ) + + # fulfilled with response time only + creation = datetime.datetime(2019, 3, 4, 12, 0) + lead = make_lead(creation=creation, index=2) + + self.assertEqual(lead.service_level_agreement, lead_sla.name) + self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) + + frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30) + lead.reload() + lead.status = 'Replied' + lead.save() + + lead.reload() + self.assertEqual(lead.agreement_status, 'Fulfilled') + + def tearDown(self): + for d in frappe.get_all("Service Level Agreement"): + frappe.delete_doc("Service Level Agreement", d.name, force=1) + def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None, doctype="Issue"): if default_service_level_agreement: @@ -176,7 +234,7 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ return service_level_agreement def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type, - entity, resolution_time, doctype="Issue", sla_fulfilled_on=[], apply_sla_for_resolution=1): + entity, resolution_time=0, doctype="Issue", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1): make_holiday_list() make_priorities() @@ -187,7 +245,9 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list {"status": "Closed"} ] - service_level_agreement = frappe.get_doc({ + pause_sla_on = [{"status": "Replied"}] if doctype == "Issue" else pause_sla_on + + service_level_agreement = frappe._dict({ "doctype": "Service Level Agreement", "enabled": 1, "document_type": doctype, @@ -199,35 +259,27 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "entity": entity, "start_date": frappe.utils.getdate(), "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), - "apply_sla_for_resolution": 1, + "apply_sla_for_resolution": apply_sla_for_resolution, "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", } ], "sla_fulfilled_on": sla_fulfilled_on, - "pause_sla_on": [ - {"status": "Replied"} - ], + "pause_sla_on": pause_sla_on, "support_and_resolution": [ { "workday": "Monday", @@ -281,10 +333,13 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list service_level_agreement_exists = frappe.db.exists("Service Level Agreement", filters) if not service_level_agreement_exists: - service_level_agreement.insert(ignore_permissions=True) - return service_level_agreement + doc = frappe.get_doc(service_level_agreement).insert(ignore_permissions=True) else: - return frappe.get_doc("Service Level Agreement", service_level_agreement_exists) + doc = frappe.get_doc("Service Level Agreement", service_level_agreement_exists) + doc.update(service_level_agreement) + doc.save() + + return doc def create_customer(): From bff1b1f112b003bcbe82069ed35357770188b757 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 29 May 2021 19:59:42 +0530 Subject: [PATCH 45/49] fix: show resolution time in gridview only if 'apply sla on resolution' is enabled --- .../service_level_agreement.js | 17 +++++++++++++++++ .../service_level_priority.json | 3 +-- 2 files changed, 18 insertions(+), 2 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 5808d443f672..f839533f75fc 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -2,6 +2,23 @@ // For license information, please see license.txt frappe.ui.form.on('Service Level Agreement', { + setup: function(frm) { + if (frm.doc.apply_sla_for_resolution) { + frm.get_field('priorities').grid.editable_fields = [ + {fieldname: 'priority', columns: 1}, + {fieldname: 'default_priority', columns: 1}, + {fieldname: 'response_time', columns: 2}, + {fieldname: 'resolution_time', columns: 2} + ]; + } else { + frm.get_field('priorities').grid.editable_fields = [ + {fieldname: 'priority', columns: 1}, + {fieldname: 'default_priority', columns: 1}, + {fieldname: 'response_time', columns: 3}, + ]; + } + }, + refresh: function(frm) { frm.trigger('fetch_status_fields'); frm.trigger('toggle_resolution_fields'); 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 b415a3618a85..0367fc6d8875 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -33,7 +33,6 @@ "fieldtype": "Duration", "hide_days": 1, "hide_seconds": 1, - "in_list_view": 1, "label": "Resolution Time" }, { @@ -65,7 +64,7 @@ ], "istable": 1, "links": [], - "modified": "2021-05-29 14:24:28.643920", + "modified": "2021-05-29 19:52:51.733248", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", From d645cf55e1d025476104075fa96f8eacf3ea75ed Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 29 May 2021 23:17:55 +0530 Subject: [PATCH 46/49] fix: update custom dt after adding SLA fields --- .../doctype/service_level_agreement/service_level_agreement.py | 3 +++ .../service_level_agreement/test_service_level_agreement.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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 920cfea418a3..cad737d693a7 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -154,6 +154,9 @@ def create_docfields(self, meta, service_level_agreement_fields): existing_field = meta.get_field(field.get("fieldname")) self.reset_field_properties(existing_field, "DocField", field) + # to update meta and modified timestamp + frappe.get_doc('DocType', self.document_type).save(ignore_permissions=True) + def create_custom_fields(self, meta, service_level_agreement_fields): for field in service_level_agreement_fields: if not meta.has_field(field.get("fieldname")): 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 966b65474180..2a8446d29f9e 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 @@ -81,7 +81,7 @@ def test_custom_field_creation_for_sla_on_standard_dt(self): # check SLA custom fields created for leads sla_fields = get_service_level_agreement_fields() - meta = frappe.get_meta(doctype) + meta = frappe.get_meta(doctype, cached=False) for field in sla_fields: self.assertTrue(meta.has_field(field.get("fieldname"))) From d793a1bd613431cdc20d2319298000ebd790689a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Jun 2021 11:41:20 +0530 Subject: [PATCH 47/49] fix: update resolution while updating hold time only if 'apply SLA for resolution' is enabled --- .../service_level_agreement.py | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 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 cad737d693a7..c236aa95a1e2 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -585,51 +585,57 @@ def handle_hold_time(doc, meta, status): update_values = {} - if hold_statuses: - if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses: - update_values['on_hold_since'] = frappe.flags.current_time or now_datetime(doc.get("owner")) - - if meta.has_field("first_responded_on") and not doc.first_responded_on: - update_values['response_by'] = None - update_values['response_by_variance'] = 0 - - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 - # calculate hold time when status is changed from any hold status to any non-hold status - - if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses: - hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - last_hold_time = 0 - - if meta.has_field("on_hold_since") and doc.on_hold_since: - # last_hold_time will be added to the sla variables - last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) - update_values['total_hold_time'] = hold_time + last_hold_time - - # re-calculate SLA variables after issue changes from any hold status to any non-hold status - # add hold time to SLA variables - start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - priority = get_priority(doc) - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - - if meta.has_field("first_responded_on") and not doc.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_seconds(response_by, now_time)) - - update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + last_hold_time + if not hold_statuses: + return + + if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses: + update_values['on_hold_since'] = frappe.flags.current_time or now_datetime(doc.get("owner")) + + if meta.has_field("first_responded_on") and not doc.first_responded_on: + update_values['response_by'] = None + update_values['response_by_variance'] = 0 + + update_values['resolution_by'] = None + update_values['resolution_by_variance'] = 0 + + # calculate hold time when status is changed from any hold status to any non-hold status + if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses: + hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0 + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + last_hold_time = 0 + if meta.has_field("on_hold_since") and doc.on_hold_since: + # last_hold_time will be added to the sla variables + last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) + update_values['total_hold_time'] = hold_time + last_hold_time + + # re-calculate SLA variables after issue changes from any hold status to any non-hold status + # add hold time to SLA variables + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + priority = get_priority(doc) + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + + # add hold time to response by variance + if meta.has_field("first_responded_on") and not doc.first_responded_on: + response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + response_by = add_to_date(response_by, seconds=round(last_hold_time)) + response_by_variance = round(time_diff_in_seconds(response_by, now_time)) + + update_values['response_by'] = response_by + update_values['response_by_variance'] = response_by_variance + last_hold_time + + # add hold time to resolution by variance + if frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, "apply_sla_for_resolution"): resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) update_values['resolution_by'] = resolution_by update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time - update_values['on_hold_since'] = None - doc.db_set(update_values) + update_values['on_hold_since'] = None + + doc.db_set(update_values) def get_service_level_agreement_fields(): From d7c0081bada3ab5e3d1547ff7cd76c8384a45d95 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Jun 2021 12:25:21 +0530 Subject: [PATCH 48/49] chore: breakdown handle_hold_time function into smaller functions --- .../service_level_agreement.py | 100 ++++++++++-------- 1 file changed, 54 insertions(+), 46 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 c236aa95a1e2..60e5fbe80eed 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -583,59 +583,67 @@ def handle_hold_time(doc, meta, status): "parent": doc.service_level_agreement }, fields=["status"])] - update_values = {} - if not hold_statuses: return if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses: - update_values['on_hold_since'] = frappe.flags.current_time or now_datetime(doc.get("owner")) - - if meta.has_field("first_responded_on") and not doc.first_responded_on: - update_values['response_by'] = None - update_values['response_by_variance'] = 0 - - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 + apply_hold_status(doc, meta) # calculate hold time when status is changed from any hold status to any non-hold status if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses: - hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - last_hold_time = 0 - - if meta.has_field("on_hold_since") and doc.on_hold_since: - # last_hold_time will be added to the sla variables - last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) - update_values['total_hold_time'] = hold_time + last_hold_time - - # re-calculate SLA variables after issue changes from any hold status to any non-hold status - # add hold time to SLA variables - start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - priority = get_priority(doc) - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - - # add hold time to response by variance - if meta.has_field("first_responded_on") and not doc.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_seconds(response_by, now_time)) - - update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + last_hold_time - - # add hold time to resolution by variance - if frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, "apply_sla_for_resolution"): - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) - resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) - - update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time - - update_values['on_hold_since'] = None - - doc.db_set(update_values) + reset_hold_status_and_update_hold_time(doc, meta) + + +def apply_hold_status(doc, meta): + update_values = {'on_hold_since': frappe.flags.current_time or now_datetime(doc.get("owner"))} + + if meta.has_field("first_responded_on") and not doc.first_responded_on: + update_values['response_by'] = None + update_values['response_by_variance'] = 0 + + update_values['resolution_by'] = None + update_values['resolution_by_variance'] = 0 + + doc.db_set(update_values) + + +def reset_hold_status_and_update_hold_time(doc, meta): + hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0 + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + last_hold_time = 0 + update_values = {} + + if meta.has_field("on_hold_since") and doc.on_hold_since: + # last_hold_time will be added to the sla variables + last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) + update_values['total_hold_time'] = hold_time + last_hold_time + + # re-calculate SLA variables after issue changes from any hold status to any non-hold status + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + priority = get_priority(doc) + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + + # add hold time to response by variance + if meta.has_field("first_responded_on") and not doc.first_responded_on: + response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + response_by = add_to_date(response_by, seconds=round(last_hold_time)) + response_by_variance = round(time_diff_in_seconds(response_by, now_time)) + + update_values['response_by'] = response_by + update_values['response_by_variance'] = response_by_variance + last_hold_time + + # add hold time to resolution by variance + if frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, "apply_sla_for_resolution"): + resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) + resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) + + update_values['resolution_by'] = resolution_by + update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time + + update_values['on_hold_since'] = None + + doc.db_set(update_values) def get_service_level_agreement_fields(): From 70a649ef8c58e5b5b2c2908ffc55c36154d200d3 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Jun 2021 13:20:59 +0530 Subject: [PATCH 49/49] fix(ux): resolution time field not visible on enabling resolution application --- .../service_level_agreement.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 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 f839533f75fc..308bce48dfb8 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Service Level Agreement', { setup: function(frm) { - if (frm.doc.apply_sla_for_resolution) { + if (cint(frm.doc.apply_sla_for_resolution) === 1) { frm.get_field('priorities').grid.editable_fields = [ {fieldname: 'priority', columns: 1}, {fieldname: 'default_priority', columns: 1}, @@ -60,9 +60,15 @@ frappe.ui.form.on('Service Level Agreement', { }, toggle_resolution_fields: function(frm) { - frm.fields_dict.priorities.grid.set_column_disp('resolution_time', frm.doc.apply_sla_for_resolution); - frm.fields_dict.priorities.grid.toggle_reqd('resolution_time', frm.doc.apply_sla_for_resolution); - frm.fields_dict.priorities.grid.refresh(); + if (cint(frm.doc.apply_sla_for_resolution) === 1) { + frm.fields_dict.priorities.grid.update_docfield_property('resolution_time', 'hidden', 0); + frm.fields_dict.priorities.grid.update_docfield_property('resolution_time', 'reqd', 1); + } else { + frm.fields_dict.priorities.grid.update_docfield_property('resolution_time', 'hidden', 1); + frm.fields_dict.priorities.grid.update_docfield_property('resolution_time', 'reqd', 0); + } + + frm.refresh_field('priorities'); }, onload: function(frm) {