From 0674d16fee33d08c4b69a15f90f6255fae453304 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 7 Jun 2019 15:28:42 +0530 Subject: [PATCH] feat: sla based on customer/group/territory --- erpnext/hooks.py | 1 - .../patches/v12_0/set_priority_for_support.py | 9 +- .../doctype/customer_group/customer_group.py | 2 +- erpnext/support/doctype/issue/issue.js | 37 +-- erpnext/support/doctype/issue/issue.py | 38 ++- .../service_level_agreement.json | 42 +++- .../service_level_agreement.py | 16 +- .../test_service_level_agreement.py | 226 +++++++++--------- 8 files changed, 194 insertions(+), 177 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c72dacfd62..b272f60860 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -264,7 +264,6 @@ scheduler_events = { "erpnext.projects.doctype.project.project.send_project_status_email_to_users", "erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", - "erpnext.support.doctype.issue.issue.set_service_level_agreement_status" ], "daily_long": [ "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" diff --git a/erpnext/patches/v12_0/set_priority_for_support.py b/erpnext/patches/v12_0/set_priority_for_support.py index f04b45a751..53aed92f17 100644 --- a/erpnext/patches/v12_0/set_priority_for_support.py +++ b/erpnext/patches/v12_0/set_priority_for_support.py @@ -6,10 +6,11 @@ def execute(): priorities = frappe.get_meta("Issue").get_field("priority").options.split("\n") for priority in priorities: - frappe.get_doc({ - "doctype": "Issue Priority", - "name": priority - }).insert(ignore_permissions=True) + if not frappe.db.exists("Issue Priority", priority): + frappe.get_doc({ + "doctype": "Issue Priority", + "name": priority + }).insert(ignore_permissions=True) frappe.reload_doc("support", "doctype", "issue") frappe.reload_doc("support", "doctype", "service_level") diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index 388ddcaada..f62613ea1b 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -8,7 +8,7 @@ from frappe import _ from frappe.utils.nestedset import NestedSet class CustomerGroup(NestedSet): - nsm_parent_field = 'parent_customer_group'; + nsm_parent_field = 'parent_customer_group' def on_update(self): self.validate_name_with_customer() diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 9ee981dea6..7c939737ed 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -1,6 +1,25 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; + if (frm.doc.service_level_agreement) { + frappe.call({ + method: "erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_priorities", + args: { + name: frm.doc.service_level_agreement, + }, + callback: function (r) { + if (r && r.message) { + frm.set_query('priority', function() { + return { + filters: { + "name": ["in", r.message], + } + }; + }); + } + } + }); + } }, refresh: function (frm) { @@ -43,24 +62,6 @@ frappe.ui.form.on("Issue", { frm.save(); }); } - - frappe.call({ - method: "erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_priorities", - args: { - name: frm.doc.service_level_agreement, - }, - callback: function (r) { - if (r && r.message) { - frm.set_query('priority', function() { - return { - filters: { - "name": ["in", r.message], - } - }; - }); - } - } - }); }, priority: function(frm) { diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 734b4433ba..525f8293f6 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -77,11 +77,9 @@ class Issue(Document): def update_agreement_status(self): current_time = frappe.flags.current_time or now_datetime() - if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": - response_time_diff = round(time_diff_in_hours(self.response_by, current_time), 2) - resolution_time_diff = round(time_diff_in_hours(self.resolution_by, current_time), 2) - if response_time_diff < 0 or resolution_time_diff < 0: + if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": + if self.response_by_variance < 0 or self.resolution_by_variance < 0: self.agreement_fulfilled = "Failed" else: self.agreement_fulfilled = "Fulfilled" @@ -232,33 +230,27 @@ def get_expected_time_for(parameter, service_level, start_date_time): return current_date_time -def set_service_level_agreement_status(): - issues = frappe.get_list("Issue", filters={"status": "Open", "agreement_fulfilled": "Ongoing"}) - for issue in issues: - doc = frappe.get_doc("Issue", issue.name) - if doc.service_level_agreement and doc.agreement_fulfilled == "Ongoing": - response_time_diff = round(time_diff_in_hours(doc.response_by, now_datetime()), 2) - resolution_time_diff = round(time_diff_in_hours(doc.resolution_by, now_datetime()), 2) - if response_time_diff < 0 or resolution_time_diff < 0: - frappe.db.set_value("Issue", doc.name, "agreement_fulfilled", "Failed") - else: - frappe.db.set_value("Issue", doc.name, "agreement_fulfilled", "Fulfilled") - def set_service_level_agreement_variance(issue=None): - filters = {"status": "Open", "agreement_fulfilled": "Ongoing"} + current_time = frappe.flags.current_time or now_datetime() + filters = {"status": "Open", "agreement_fulfilled": "Ongoing"} if issue: filters = {"name": issue} - issues = frappe.get_list("Issue", filters=filters) - for issue in issues: + for issue in frappe.get_list("Issue", filters=filters): doc = frappe.get_doc("Issue", issue.name) - if not doc.first_responded_on: - variance = round(time_diff_in_hours(doc.response_by, now_datetime()), 2) + + 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("Issue", doc.name, "response_by_variance", variance) - if not doc.resolution_date: - variance = round(time_diff_in_hours(doc.resolution_by, now_datetime()), 2) + if variance < 0: + frappe.db.set_value("Issue", doc.name, "agreement_fulfilled", "Failed") + + 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("Issue", doc.name, "resolution_by_variance", variance) + if variance < 0: + frappe.db.set_value("Issue", doc.name, "agreement_fulfilled", "Failed") def get_list_context(context=None): return { 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 c68d385345..5b6b7cc239 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -6,12 +6,15 @@ "engine": "InnoDB", "field_order": [ "service_level", - "customer", "default_service_level_agreement", "holiday_list", "column_break_2", "employee_group", "default_priority", + "apply_to_section", + "apply_to", + "column_break_10", + "entity", "agreement_details_section", "start_date", "active", @@ -23,16 +26,6 @@ "support_and_resolution" ], "fields": [ - { - "depends_on": "eval: !doc.default_service_level_agreement;", - "fieldname": "customer", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Customer", - "options": "Customer", - "set_only_once": 1 - }, { "default": "0", "depends_on": "eval: !doc.customer;", @@ -133,9 +126,34 @@ "fieldtype": "Check", "label": "Active", "read_only": 1 + }, + { + "collapsible_depends_on": "eval: !doc.default_service_level_agreement;", + "fieldname": "apply_to_section", + "fieldtype": "Section Break", + "label": "Apply To" + }, + { + "fieldname": "apply_to", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Apply To", + "options": "\nCustomer\nCustomer Group\nTerritory" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "entity", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Entity", + "options": "apply_to" } ], - "modified": "2019-06-06 12:56:24.545060", + "modified": "2019-06-07 00:30:34.755416", "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 4ff0312b47..ccc287bdc8 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -45,14 +45,14 @@ def check_agreement_status(): def get_active_service_level_agreement_for(priority, customer=None, service_level_agreement=None): filters = [ - ["Service Level Agreement", "active", "=", 1] + ["Service Level Agreement", "active", "=", 1], ] if priority: filters.append(["Service Level Priority", "priority", "=", priority]) or_filters = [ - ["Service Level Agreement", "customer", "=", customer] + ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] ] if service_level_agreement: or_filters = [ @@ -62,10 +62,18 @@ def get_active_service_level_agreement_for(priority, customer=None, service_leve or_filters.append(["Service Level Agreement", "default_service_level_agreement", "=", 1]) agreement = frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters, - fields=["name", "default_priority", "customer"]) + fields=["name", "default_priority"], debug=True) return agreement[0] if agreement else None @frappe.whitelist() def get_service_level_agreement_priorities(name): - return [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])] \ No newline at end of file + return [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])] + +def get_customer_group(customer): + if customer: + return frappe.db.get_value("Customer", customer, "customer_group") + +def get_customer_territory(customer): + if customer: + return frappe.db.get_value("Customer", customer, "territory") \ No newline at end of file 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 2679f97f55..88e1ee4a90 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 @@ -10,136 +10,116 @@ from erpnext.support.doctype.service_level.test_service_level import make_servic class TestServiceLevelAgreement(unittest.TestCase): def test_service_level_agreement(self): - test_make_service_level_agreement = make_service_level_agreement() - test_get_service_level_agreement = get_service_level_agreement() + make_service_level() - self.assertEqual(test_make_service_level_agreement.name, test_get_service_level_agreement.name) - self.assertEqual(test_make_service_level_agreement.customer, test_get_service_level_agreement.customer) - self.assertEqual(test_make_service_level_agreement.default_service_level_agreement, test_get_service_level_agreement.default_service_level_agreement) -def make_service_level_agreement(): - make_service_level() + # Default Service Level Agreement + create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1, + service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + apply_to=None, entity=None, response_time=4, resolution_time=6) + get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1) - # Default Service Level Agreement - default_service_level_agreement = frappe.get_doc({ - "doctype": "Service Level Agreement", - "service_level_agreement_name": "Default Service Level Agreement", - "default_service_level_agreement": 1, - "service_level": "__Test Service Level", - "holiday_list": "__Test Holiday List", - "employee_group": "_Test Employee Group", - "start_date": frappe.utils.getdate(), - "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), - "priorities": [ - { - "priority": "Low", - "response_time": 4, - "response_time_period": "Hour", - "resolution_time": 6, - "resolution_time_period": "Hour", - }, - { - "priority": "Medium", - "response_time": 4, - "default_priority": 1, - "response_time_period": "Hour", - "resolution_time": 6, - "resolution_time_period": "Hour", - }, - { - "priority": "High", - "response_time": 4, - "response_time_period": "Hour", - "resolution_time": 6, - "resolution_time_period": "Hour", - } - ], - "support_and_resolution": [ - { - "workday": "Monday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Tuesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Wednesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Thursday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Friday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Saturday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Sunday", - "start_time": "10:00:00", - "end_time": "18:00:00", - } - ] - }) + self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name) + self.assertEqual(create_default_service_level_agreement.apply_to, get_default_service_level_agreement.apply_to) + self.assertEqual(create_default_service_level_agreement.entity, get_default_service_level_agreement.entity) + self.assertEqual(create_default_service_level_agreement.default_service_level_agreement, get_default_service_level_agreement.default_service_level_agreement) - default_service_level_agreement_exists = frappe.db.exists("Service Level Agreement", "SLA-Default Service Level Agreement") - if not default_service_level_agreement_exists: - default_service_level_agreement.insert(ignore_permissions=True) + # Service Level Agreement for Customer + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_name": "_Test Customer", + "customer_group": "Commercial", + "customer_type": "Individual", + "territory": "Rest Of The World" + }) + if not frappe.db.exists("Customer", "_Test Customer"): + customer.insert(ignore_permissions=True) + else: + customer = frappe.get_doc("Customer", "_Test Customer") - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test Customer", - "customer_group": "Commercial", - "customer_type": "Individual", - "territory": "Rest Of The World" - }) - if not frappe.db.exists("Customer", "_Test Customer"): - customer.insert(ignore_permissions=True) + create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, + service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + apply_to="Customer", entity=customer.name, response_time=2, resolution_time=3) + get_customer_service_level_agreement = get_service_level_agreement(apply_to="Customer", entity=customer.name) + + self.assertEqual(create_customer_service_level_agreement.name, get_customer_service_level_agreement.name) + self.assertEqual(create_customer_service_level_agreement.apply_to, get_customer_service_level_agreement.apply_to) + self.assertEqual(create_customer_service_level_agreement.entity, get_customer_service_level_agreement.entity) + self.assertEqual(create_customer_service_level_agreement.default_service_level_agreement, get_customer_service_level_agreement.default_service_level_agreement) + + + # Service Level Agreement for Customer Group + customer_group = create_customer_group() + create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, + service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + apply_to="Customer Group", entity=customer_group.name, response_time=4, resolution_time=6) + get_customer_group_service_level_agreement = get_service_level_agreement(apply_to="Customer Group", entity=customer_group.name) + + self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name) + self.assertEqual(create_customer_group_service_level_agreement.apply_to, get_customer_group_service_level_agreement.apply_to) + self.assertEqual(create_customer_group_service_level_agreement.entity, get_customer_group_service_level_agreement.entity) + self.assertEqual(create_customer_group_service_level_agreement.default_service_level_agreement, get_customer_group_service_level_agreement.default_service_level_agreement) + + + # Service Level Agreement for Territory + territory = create_territory() + create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, + service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + apply_to="Territory", entity=territory.name, response_time=2, resolution_time=3) + get_territory_service_level_agreement = get_service_level_agreement(apply_to="Territory", entity=territory.name) + + self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name) + self.assertEqual(create_territory_service_level_agreement.apply_to, get_territory_service_level_agreement.apply_to) + 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, apply_to=None, entity=None): + if default_service_level_agreement: + filters = {"default_service_level_agreement": default_service_level_agreement} else: - customer = frappe.get_doc("Customer", "_Test Customer") + filters = {"apply_to": apply_to, "entity": entity} + + service_level_agreement = frappe.get_doc("Service Level Agreement", filters) + print(service_level_agreement) + return service_level_agreement + +def create_service_level_agreement(default_service_level_agreement, service_level, holiday_list, employee_group, + response_time, apply_to, entity, resolution_time): service_level_agreement = frappe.get_doc({ "doctype": "Service Level Agreement", - "service_level_agreement_name": "_Test Service Level Agreement", - "customer": customer.customer_name, - "service_level": "_Test Service Level", - "holiday_list": "__Test Holiday List", - "employee_group": "_Test Employee Group", + "default_service_level_agreement": default_service_level_agreement, + "service_level": service_level, + "holiday_list": holiday_list, + "employee_group": employee_group, + "apply_to": apply_to, + "entity": entity, "start_date": frappe.utils.getdate(), "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), "priorities": [ { "priority": "Low", - "response_time": 2, - "response_time_period": "Day", - "resolution_time": 3, - "resolution_time_period": "Day", + "response_time": response_time, + "response_time_period": "Hour", + "resolution_time": resolution_time, + "resolution_time_period": "Hour", }, { "priority": "Medium", - "response_time": 2, - "response_time_period": "Day", - "resolution_time": 3, - "resolution_time_period": "Day", + "response_time": response_time, + "default_priority": 1, + "response_time_period": "Hour", + "resolution_time": resolution_time, + "resolution_time_period": "Hour", }, { "priority": "High", - "response_time": 2, - "response_time_period": "Day", - "resolution_time": 3, - "resolution_time_period": "Day", + "response_time": response_time, + "response_time_period": "Hour", + "resolution_time": resolution_time, + "resolution_time_period": "Hour", } ], "support_and_resolution": [ @@ -181,14 +161,32 @@ def make_service_level_agreement(): ] }) - service_level_agreement_exists = frappe.db.exists("Service Level Agreement", {"service_level_agreement_name": "_Test Service Level Agreement"}) + service_level_agreement_exists = frappe.db.exists("Service Level Agreement", service_level_agreement.name) if not service_level_agreement_exists: service_level_agreement.insert(ignore_permissions=True) return service_level_agreement else: - return frappe.get_doc("Service Level Agreement", "SLA-_Test Service Level Agreement") + return frappe.get_doc("Service Level Agreement", service_level_agreement.name) -def get_service_level_agreement(): - service_level_agreement = frappe.get_doc("Service Level Agreement", "SLA-_Test Service Level Agreement") - return service_level_agreement \ No newline at end of file +def create_customer_group(): + customer_group = frappe.get_doc({ + "doctype": "Customer Group", + "customer_group_name": "_Test SLA Customer Group" + }) + + if not frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"}): + customer_group.insert() + + return customer_group.name + +def create_territory(): + territory = frappe.get_doc({ + "doctype": "Territory", + "territory_name": "_Test SLA Territory", + }) + + if not frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}): + territory.insert() + + return territory.name \ No newline at end of file