feat: sla based on customer/group/territory
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index c72dacf..b272f60 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -264,7 +264,6 @@
"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 f04b45a..53aed92 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 @@
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 388ddca..f62613e 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.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 9ee981d..7c93973 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 @@
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 734b443..525f829 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -77,11 +77,9 @@
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 @@
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 c68d385..5b6b7cc 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",
@@ -24,16 +27,6 @@
],
"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;",
"fieldname": "default_service_level_agreement",
@@ -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 4ff0312..ccc287b 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 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 @@
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 2679f97..88e1ee4 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 @@
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 @@
]
})
- 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