Merge pull request #17492 from hrwX/sla_fix

feat(Service Level Agreement): Service Level Agreement based on Customer, Group and Territory
This commit is contained in:
Himanshu 2019-06-27 01:32:21 +05:30 committed by GitHub
commit 0b39c4ecff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1423 additions and 2818 deletions

View File

@ -18,6 +18,11 @@ def get_data():
"onboard": 1,
"dependencies": ["Employee"]
},
{
"type": "doctype",
"name": "Employee Group",
"dependencies": ["Employee"]
},
{
"type": "doctype",
"name": "Attendance",

View File

@ -12,6 +12,16 @@ def get_data():
"description": _("Support queries from customers."),
"onboard": 1,
},
{
"type": "doctype",
"name": "Issue Type",
"description": _("Issue Type."),
},
{
"type": "doctype",
"name": "Issue Priority",
"description": _("Issue Priority."),
},
{
"type": "doctype",
"name": "Communication",
@ -38,11 +48,6 @@ def get_data():
{
"label": _("Service Level Agreement"),
"items": [
{
"type": "doctype",
"name": "Employee Group",
"description": _("Support Team."),
},
{
"type": "doctype",
"name": "Service Level",

View File

@ -242,7 +242,8 @@ scheduler_events = {
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
"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.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
],
"daily": [
"erpnext.stock.reorder_item.reorder_item",
@ -264,7 +265,7 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.update_project_sales_billing",
"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.service_level_agreement.service_level_agreement.check_agreement_status",
],
"daily_long": [
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms"

View File

@ -605,4 +605,5 @@ erpnext.patches.v11_1.delete_scheduling_tool
erpnext.patches.v12_0.make_custom_fields_for_bank_remittance #14-06-2019
execute:frappe.delete_doc_if_exists("Page", "support-analytics")
erpnext.patches.v12_0.make_item_manufacturer
erpnext.patches.v12_0.set_quotation_status
erpnext.patches.v12_0.set_quotation_status
erpnext.patches.v12_0.set_priority_for_support

View File

@ -0,0 +1,78 @@
import frappe
def execute():
frappe.reload_doc("support", "doctype", "issue_priority")
frappe.reload_doc("support", "doctype", "service_level_priority")
set_issue_priority()
set_priority_for_issue()
set_priorities_service_level()
set_priorities_service_level_agreement()
def set_issue_priority():
# Adds priority from issue to Issue Priority DocType as Priority is a new DocType.
for priority in frappe.get_meta("Issue").get_field("priority").options.split("\n"):
if priority and not frappe.db.exists("Issue Priority", priority):
frappe.get_doc({
"doctype": "Issue Priority",
"name": priority
}).insert(ignore_permissions=True)
def set_priority_for_issue():
# Sets priority for Issues as Select field is changed to Link field.
issue_priority = frappe.get_list("Issue", fields=["name", "priority"])
frappe.reload_doc("support", "doctype", "issue")
for issue in issue_priority:
frappe.db.set_value("Issue", issue.name, "priority", issue.priority)
def set_priorities_service_level():
# Migrates "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period" to Child Table
# as a Service Level can have multiple priorities
try:
service_level_priorities = frappe.get_list("Service Level", fields=["name", "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period"])
frappe.reload_doc("support", "doctype", "service_level")
for service_level in service_level_priorities:
if service_level:
doc = frappe.get_doc("Service Level", service_level.name)
doc.append("priorities", {
"priority": service_level.priority,
"default_priority": 1,
"response_time": service_level.response_time,
"response_time_period": service_level.response_time_period,
"resolution_time": service_level.resolution_time,
"resolution_time_period": service_level.resolution_time_period
})
doc.save(ignore_permissions=True)
except frappe.db.TableMissingError:
frappe.reload_doc("support", "doctype", "service_level")
def set_priorities_service_level_agreement():
# Migrates "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period" to Child Table
# as a Service Level Agreement can have multiple priorities
try:
service_level_agreement_priorities = frappe.get_list("Service Level Agreement", fields=["name", "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period"])
frappe.reload_doc("support", "doctype", "service_level_agreement")
for service_level_agreement in service_level_agreement_priorities:
if service_level_agreement:
doc = frappe.get_doc("Service Level Agreement", service_level_agreement.name)
if doc.customer:
doc.entity_type = "Customer"
doc.entity = doc.customer
doc.append("priorities", {
"priority": service_level_agreement.priority,
"default_priority": 1,
"response_time": service_level_agreement.response_time,
"response_time_period": service_level_agreement.response_time_period,
"resolution_time": service_level_agreement.resolution_time,
"resolution_time_period": service_level_agreement.resolution_time_period
})
doc.save(ignore_permissions=True)
except frappe.db.TableMissingError:
frappe.reload_doc("support", "doctype", "service_level_agreement")

View File

@ -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()

View File

@ -173,6 +173,11 @@ def install(country=None):
{"attribute_value": _("White"), "abbr": "WHI"}
]},
# Issue Priority
{'doctype': 'Issue Priority', 'name': _('Low')},
{'doctype': 'Issue Priority', 'name': _('Medium')},
{'doctype': 'Issue Priority', 'name': _('High')},
#Job Applicant Source
{'doctype': 'Job Applicant Source', 'source_name': _('Website Listing')},
{'doctype': 'Job Applicant Source', 'source_name': _('Walk In')},

View File

@ -1,13 +1,43 @@
frappe.ui.form.on("Issue", {
onload: function(frm) {
frm.email_field = "raised_by";
if (frm.doc.service_level_agreement) {
set_time_to_resolve_and_response(frm);
frappe.call({
method: "erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters",
args: {
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") {
if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") {
if (frm.doc.service_level_agreement) {
set_time_to_resolve_and_response(frm);
}
frm.add_custom_button(__("Close"), function () {
frm.set_value("status", "Closed");
frm.save();
@ -20,6 +50,22 @@ frappe.ui.form.on("Issue", {
});
}, __("Make"));
} else {
if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();
let agreement_fulfilled = (frm.doc.agreement_fulfilled == "Fulfilled") ?
{"indicator": "green", "msg": "Service Level Agreement has been fulfilled"} :
{"indicator": "red", "msg": "Service Level Agreement Failed"};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ agreement_fulfilled.indicator +'"><span class="hidden-xs">'+ agreement_fulfilled.msg +'</span></span> ' +
'</div>' +
'</div>'
);
}
frm.add_custom_button(__("Reopen"), function () {
frm.set_value("status", "Open");
frm.save();
@ -27,6 +73,27 @@ frappe.ui.form.on("Issue", {
}
},
priority: function(frm) {
if (frm.doc.service_level_agreement) {
frm.call('change_service_level_agreement_and_priority', {
"priority": frm.doc.priority,
"service_level_agreement": frm.doc.service_level_agreement
}).then(() => {
frappe.msgprint(__("Issue Priority changed to {0}.", [frm.doc.priority]));
frm.refresh();
});
}
},
service_level_agreement: function(frm) {
frm.call('change_service_level_agreement_and_priority', {
"service_level_agreement": frm.doc.service_level_agreement
}).then(() => {
frappe.msgprint(__("Service Level Agreement changed to {0}.", [frm.doc.service_level_agreement]));
frm.refresh();
});
},
timeline_refresh: function(frm) {
// create button for "Help Article"
if(frappe.model.can_create('Help Article')) {
@ -81,36 +148,26 @@ frappe.ui.form.on("Issue", {
});
function set_time_to_resolve_and_response(frm) {
frm.dashboard.clear_headline();
const customer = frm.fields_dict['customer'].$wrapper;
const email_account = frm.fields_dict['email_account'].$wrapper;
var time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_fulfilled);
var time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_fulfilled);
const time_to_respond = $(get_time_left_element(__('Time To Respond'), frm.doc.response_by));
const time_to_resolve = $(get_time_left_element(__('Time To Resolve'), frm.doc.resolution_by));
time_to_respond.insertAfter(customer);
time_to_resolve.insertAfter(email_account);
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-6">' +
'<span class="indicator whitespace-nowrap '+ time_to_respond.indicator +'"><span class="hidden-xs">Time to Respond: '+ time_to_respond.diff_display +'</span></span> ' +
'</div>' +
'<div class="col-xs-6">' +
'<span class="indicator whitespace-nowrap '+ time_to_resolve.indicator +'"><span class="hidden-xs">Time to Resolve: '+ time_to_resolve.diff_display +'</span></span> ' +
'</div>' +
'</div>'
);
}
function get_time_left_element(label, timestamp) {
$('.'+ frappe.scrub(label) +'').remove();
return `
<div class="frappe-control input-max-width `+ frappe.scrub(label) +`" data-field_name="`+ frappe.scrub(label) +`">
<div class="form-group">
<div class="clearfix">
<label class="control-label" style="padding-right: 0px;">
${label}
</label>
</div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input">${get_time_left(timestamp)}</div>
</div>
</div>
</div>
`;
}
function get_time_left(timestamp) {
function get_time_left(timestamp, agreement_fulfilled) {
const diff = moment(timestamp).diff(moment());
return diff >= 44500 ? moment.duration(diff).humanize() : 0;
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : moment(0, 'seconds').format('HH:mm');
let indicator = (diff_display == '00:00' && agreement_fulfilled != "Fulfilled") ? "red" : "green";
return {"diff_display": diff_display, "indicator": indicator};
}

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ from frappe.utils import now, time_diff_in_hours, now_datetime, getdate, get_wee
from datetime import datetime, timedelta
from frappe.model.mapper import get_mapped_doc
from frappe.utils.user import is_website_user
from ..service_level_agreement.service_level_agreement import get_active_service_level_agreement_for
from erpnext.support.doctype.service_level_agreement.service_level_agreement import get_active_service_level_agreement_for
from erpnext.crm.doctype.opportunity.opportunity import assign_to_user
from frappe.email.inbox import link_communication_to_document
@ -63,22 +63,39 @@ class Issue(Document):
def update_status(self):
status = frappe.db.get_value("Issue", self.name, "status")
if self.status!="Open" and status =="Open" and not self.first_responded_on:
self.first_responded_on = now()
self.first_responded_on = frappe.flags.current_time or now_datetime()
if self.status=="Closed" and status !="Closed":
self.resolution_date = now()
self.update_agreement_status()
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()
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
def update_agreement_status(self):
current_time = frappe.flags.current_time or now_datetime()
if self.service_level_agreement:
if (round(time_diff_in_hours(self.response_by, current_time), 2) < 0
or round(time_diff_in_hours(self.resolution_by, current_time), 2) < 0):
self.agreement_status = "Failed"
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_status = "Fulfilled"
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"
self.save(ignore_permissions=True)
def create_communication(self):
communication = frappe.new_doc("Communication")
@ -124,23 +141,42 @@ class Issue(Document):
def before_insert(self):
self.set_response_and_resolution_time()
def set_response_and_resolution_time(self):
service_level_agreement = get_active_service_level_agreement_for(self.customer)
if service_level_agreement:
self.service_level_agreement = service_level_agreement.name
self.priority = service_level_agreement.priority
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 self.service_level_agreement: return
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
service_level = frappe.get_doc("Service Level", service_level_agreement.service_level)
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
service_level_agreement = frappe.get_doc("Service Level Agreement", service_level_agreement.name)
priority = service_level_agreement.get_service_level_agreement_priority(self.priority)
priority.update({
"support_and_resolution": service_level_agreement.support_and_resolution,
"holiday_list": service_level_agreement.holiday_list
})
if not self.creation:
self.creation = now_datetime()
start_date_time = get_datetime(self.creation)
self.response_by = get_expected_time_for(parameter='response', service_level=priority, start_date_time=start_date_time)
self.resolution_by = get_expected_time_for(parameter='resolution', service_level=priority, start_date_time=start_date_time)
self.response_by = get_expected_time_for('response', service_level, start_date_time)
self.resolution_by = get_expected_time_for('resolution', service_level, 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()))
@frappe.whitelist()
def change_service_level_agreement_and_priority(self, priority=None, service_level_agreement=None):
self.set_response_and_resolution_time(priority=priority, service_level_agreement=service_level_agreement)
self.save(ignore_permissions=True)
def get_expected_time_for(parameter, service_level, start_date_time):
current_date_time = start_date_time
@ -150,11 +186,11 @@ def get_expected_time_for(parameter, service_level, start_date_time):
# lets assume response time is in days by default
if parameter == 'response':
allotted_days = service_level.response_time
time_period = service_level.response_time_period
allotted_days = service_level.get("response_time")
time_period = service_level.get("response_time_period")
elif parameter == 'resolution':
allotted_days = service_level.resolution_time
time_period = service_level.resolution_time_period
allotted_days = service_level.get("resolution_time")
time_period = service_level.get("resolution_time_period")
else:
frappe.throw(_("{0} parameter is invalid".format(parameter)))
@ -168,20 +204,22 @@ def get_expected_time_for(parameter, service_level, start_date_time):
expected_time_is_set = 1 if allotted_days == 0 and time_period in ['Day', 'Week'] else 0
support_days = {}
for service in service_level.support_and_resolution:
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.holiday_list)
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) else support_days[current_weekday].start_time
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_hours(end_time, start_time)
@ -207,6 +245,28 @@ def get_expected_time_for(parameter, service_level, start_date_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("Issue", doc.name, "response_by_variance", variance)
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 {
"title": _("Issues"),
@ -244,14 +304,12 @@ def set_multiple_status(names, status):
for name in names:
set_status(name, status)
@frappe.whitelist()
def set_status(name, status):
st = frappe.get_doc("Issue", name)
st.status = status
st.save()
def auto_close_tickets():
"""Auto-close replied support tickets after 7 days"""
auto_close_after_days = frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7
@ -291,6 +349,7 @@ def make_task(source_name, target_doc=None):
"doctype": "Task"
}
}, target_doc)
@frappe.whitelist()
def make_issue_from_communication(communication, ignore_communication_links=False):
""" raise a issue from email """
@ -307,3 +366,10 @@ 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)

View File

@ -4,8 +4,8 @@ from __future__ import unicode_literals
import frappe
import unittest
from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import make_service_level_agreement
from frappe.utils import now_datetime
from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues
from frappe.utils import now_datetime, get_datetime
import datetime
from datetime import timedelta
from frappe.desk.form import assign_to
@ -18,56 +18,104 @@ class TestIssue(unittest.TestCase):
def test_response_time_and_resolution_time_based_on_different_sla(self):
make_service_level_agreement()
create_service_level_agreements_for_issues()
creation = "2019-03-04 12:00:00"
creation = datetime.datetime(2019, 3, 4, 12, 0)
# make issue with customer specific SLA
issue = make_issue(creation, '_Test Customer')
customer = create_customer("_Test Customer", "__Test SLA Customer Group", "__Test SLA Territory")
issue = make_issue(creation, "_Test Customer", 1)
self.assertEquals(issue.response_by, datetime.datetime(2019, 3, 7, 18, 0))
self.assertEquals(issue.resolution_by, datetime.datetime(2019, 3, 9, 18, 0))
self.assertEquals(issue.response_by, datetime.datetime(2019, 3, 4, 14, 0))
self.assertEquals(issue.resolution_by, datetime.datetime(2019, 3, 4, 15, 0))
# make issue with customer_group specific SLA
customer = create_customer("__Test Customer", "_Test SLA Customer Group", "__Test SLA Territory")
issue = make_issue(creation, "__Test Customer", 2)
self.assertEquals(issue.response_by, datetime.datetime(2019, 3, 4, 14, 0))
self.assertEquals(issue.resolution_by, datetime.datetime(2019, 3, 4, 15, 0))
# make issue with territory specific SLA
customer = create_customer("___Test Customer", "__Test SLA Customer Group", "_Test SLA Territory")
issue = make_issue(creation, "___Test Customer", 3)
self.assertEquals(issue.response_by, datetime.datetime(2019, 3, 4, 14, 0))
self.assertEquals(issue.resolution_by, datetime.datetime(2019, 3, 4, 15, 0))
# make issue with default SLA
issue = make_issue(creation)
issue = make_issue(creation=creation, index=4)
self.assertEquals(issue.response_by, datetime.datetime(2019, 3, 4, 16, 0))
self.assertEquals(issue.resolution_by, datetime.datetime(2019, 3, 4, 18, 0))
creation = "2019-03-04 14:00:00"
# make issue with default SLA before working hours
creation = datetime.datetime(2019, 3, 4, 7, 0)
issue = make_issue(creation=creation, index=5)
self.assertEquals(issue.response_by, datetime.datetime(2019, 3, 4, 14, 0))
self.assertEquals(issue.resolution_by, datetime.datetime(2019, 3, 4, 16, 0))
# make issue with default SLA after working hours
creation = datetime.datetime(2019, 3, 4, 20, 0)
issue = make_issue(creation, index=6)
self.assertEquals(issue.response_by, datetime.datetime(2019, 3, 6, 14, 0))
self.assertEquals(issue.resolution_by, datetime.datetime(2019, 3, 6, 16, 0))
# make issue with default SLA next day
issue = make_issue(creation)
creation = datetime.datetime(2019, 3, 4, 14, 0)
issue = make_issue(creation=creation, index=7)
self.assertEquals(issue.response_by, datetime.datetime(2019, 3, 4, 18, 0))
self.assertEquals(issue.resolution_by, datetime.datetime(2019, 3, 6, 12, 0))
frappe.flags.current_time = datetime.datetime(2019, 3, 3, 12, 0)
frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 0)
issue.status = 'Closed'
issue.save()
self.assertEqual(issue.agreement_status, 'Fulfilled')
self.assertEqual(issue.agreement_fulfilled, 'Fulfilled')
issue.status = 'Open'
issue.save()
frappe.flags.current_time = datetime.datetime(2019, 3, 5, 12, 0)
issue.status = 'Closed'
issue.save()
self.assertEqual(issue.agreement_status, 'Failed')
def make_issue(creation=None, customer=None):
def make_issue(creation=None, customer=None, index=0):
issue = frappe.get_doc({
"doctype": "Issue",
"subject": "Issue 1",
"subject": "Service Level Agreement Issue {0}".format(index),
"customer": customer,
"raised_by": "test@example.com",
"description": "Service Level Agreement Issue",
"creation": creation
}).insert()
}).insert(ignore_permissions=True)
return issue
return issue
def create_customer(name, customer_group, territory):
create_customer_group(customer_group)
create_territory(territory)
if not frappe.db.exists("Customer", {"customer_name": name}):
frappe.get_doc({
"doctype": "Customer",
"customer_name": name,
"customer_group": customer_group,
"territory": territory
}).insert(ignore_permissions=True)
def create_customer_group(customer_group):
if not frappe.db.exists("Customer Group", {"customer_group_name": customer_group}):
frappe.get_doc({
"doctype": "Customer Group",
"customer_group_name": customer_group
}).insert(ignore_permissions=True)
def create_territory(territory):
if not frappe.db.exists("Territory", {"territory_name": territory}):
frappe.get_doc({
"doctype": "Territory",
"territory_name": territory,
}).insert(ignore_permissions=True)

View File

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Issue Priority', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,39 @@
{
"autoname": "Prompt",
"creation": "2019-05-20 15:14:21.604447",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"description"
],
"fields": [
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
}
],
"modified": "2019-05-20 17:06:38.095647",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue Priority",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class IssuePriority(Document):
def validate(self):
if frappe.db.exists("Issue Priority", {"name": self.name}):
frappe.throw(_("Issue Priority Already Exists"))

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestIssuePriority(unittest.TestCase):
def test_priorities(self):
make_priorities()
priorities = frappe.get_list("Issue Priority")
for priority in priorities:
self.assertIn(priority.name, ["Low", "Medium", "High"])
def make_priorities():
insert_priority("Low")
insert_priority("Medium")
insert_priority("High")
def insert_priority(name):
if not frappe.db.exists("Issue Priority", name):
frappe.get_doc({
"doctype": "Issue Priority",
"name": name
}).insert(ignore_permissions=True)

View File

@ -1,203 +1,53 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-03-04 12:55:36.403035",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"workday",
"section_break_2",
"start_time",
"column_break_3",
"end_time"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "workday",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Workday",
"length": 0,
"no_copy": 0,
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "start_time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Start Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Start Time"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "end_time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "End Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "End Time"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-03-04 12:55:36.403035",
"modified": "2019-05-05 19:15:08.999579",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Day",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View File

@ -1,488 +1,111 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:service_level",
"beta": 0,
"creation": "2018-11-19 12:44:30.407502",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"service_level",
"employee_group",
"column_break_2",
"holiday_list",
"default_priority",
"response_and_resoution_time",
"priorities",
"section_break_01",
"support_and_resolution"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "service_level",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Level",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "priority",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Priority",
"length": 0,
"no_copy": 0,
"options": "Low\nMedium\nHigh",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "holiday_list",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Holiday List (ignored during SLA calculation)",
"length": 0,
"no_copy": 0,
"options": "Holiday List",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "employee_group",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Employee Group",
"length": 0,
"no_copy": 0,
"options": "Employee Group",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Employee Group"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "response_and_resoution_time",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Response and Resoution Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Response and Resoution Time"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "response_time",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Response Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "resolution_time",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Resolution Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "column_break_9",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "response_time_period",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Response Time Period",
"length": 0,
"no_copy": 0,
"options": "Hour\nDay\nWeek",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "resolution_time_period",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Resolution Time Period",
"length": 0,
"no_copy": 0,
"options": "Hour\nDay\nWeek",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "section_break_01",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Support and Resolution",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Support Hours"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "support_and_resolution",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Support and Resolution",
"length": 0,
"no_copy": 0,
"options": "Service Day",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"fieldname": "priorities",
"fieldtype": "Table",
"label": "Priorities",
"options": "Service Level Priority",
"reqd": 1
},
{
"fieldname": "default_priority",
"fieldtype": "Link",
"label": "Default Priority",
"options": "Issue Priority",
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-03-04 12:55:53.215841",
"modified": "2019-06-06 12:58:03.464056",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
"sort_order": "DESC"
}

View File

@ -12,35 +12,84 @@ from frappe.utils import get_weekdays
class ServiceLevel(Document):
def validate(self):
week = get_weekdays()
indexes = []
self.check_priorities()
self.check_support_and_resolution()
self.check_response_and_resolution_time()
def check_priorities(self):
default_priority = []
priorities = []
for priority in self.priorities:
# Check if response and resolution time is set for every priority
if not (priority.response_time or priority.resolution_time):
frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.".format(priority.priority, priority.idx)))
priorities.append(priority.priority)
if priority.default_priority:
default_priority.append(priority.default_priority)
if priority.response_time_period == "Hour":
response = priority.response_time * 0.0416667
elif priority.response_time_period == "Day":
response = priority.response_time
elif priority.response_time_period == "Week":
response = priority.response_time * 7
if priority.resolution_time_period == "Hour":
resolution = priority.resolution_time * 0.0416667
elif priority.resolution_time_period == "Day":
resolution = priority.resolution_time
elif priority.resolution_time_period == "Week":
resolution = priority.resolution_time * 7
if response > resolution:
frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.".format(priority.priority, priority.idx)))
# Check if repeated priority
if not len(set(priorities)) == len(priorities):
repeated_priority = get_repeated(priorities)
frappe.throw(_("Priority {0} has been repeated.".format(repeated_priority)))
# Check if repeated default priority
if not len(set(default_priority)) == len(default_priority):
frappe.throw(_("Select only one Priority as Default."))
# set default priority from priorities
try:
self.default_priority = next(d.priority for d in self.priorities if d.default_priority)
except Exception:
frappe.throw(_("Select a Default Priority."))
def check_support_and_resolution(self):
week = get_weekdays()
support_days = []
for support_and_resolution in self.support_and_resolution:
indexes.append(week.index(support_and_resolution.workday))
# 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
start_time, end_time = (datetime.strptime(support_and_resolution.start_time, '%H:%M:%S').time(),
datetime.strptime(support_and_resolution.end_time, '%H:%M:%S').time())
if start_time > end_time:
frappe.throw(_("Start Time can't be greater than End Time for {0}.".format(support_and_resolution.workday)))
if not len(set(indexes)) == len(indexes):
frappe.throw(_("Workday has been repeated twice"))
def check_response_and_resolution_time(self):
if self.response_time_period == "Hour":
response = self.response_time * 0.0416667
elif self.response_time_period == "Day":
response = self.response_time
elif self.response_time_period == "Week":
response = self.response_time * 7
if support_and_resolution.start_time >= support_and_resolution.end_time:
frappe.throw(_("Start Time can't be greater than or equal to End Time \
for {0}.".format(support_and_resolution.workday)))
if self.resolution_time_period == "Hour":
resolution = self.resolution_time * 0.0416667
elif self.resolution_time_period == "Day":
resolution = self.resolution_time
elif self.resolution_time_period == "Week":
resolution = self.resolution_time * 7
# Check for repeated workday
if not len(set(support_days)) == len(support_days):
repeated_days = get_repeated(support_days)
frappe.throw(_("Workday {0} has been repeated.".format(repeated_days)))
if response > resolution:
frappe.throw(_("Response Time can't be greater than Resolution Time"))
def get_repeated(values):
unique_list = []
diff = []
for value in values:
if value not in unique_list:
unique_list.append(str(value))
else:
if value not in diff:
diff.append(str(value))
return " ".join(diff)

View File

@ -0,0 +1,12 @@
from frappe import _
def get_data():
return {
'fieldname': 'service_level',
'transactions': [
{
'label': _('Service Level Agreement'),
'items': ['Service Level Agreement']
}
]
}

View File

@ -3,31 +3,65 @@
# See license.txt
from __future__ import unicode_literals
from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group
from frappe.utils import now_datetime
import datetime
from datetime import timedelta
from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities
import frappe
import unittest
class TestServiceLevel(unittest.TestCase):
pass
def make_service_level():
employee_group = make_employee_group()
make_holiday_list()
def test_service_level(self):
employee_group = make_employee_group()
make_holiday_list()
make_priorities()
# Default Service Level Agreement
default_service_level = frappe.get_doc({
# Default Service Level
test_make_service_level = create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6)
get_make_service_level = get_service_level("__Test Service Level")
self.assertEqual(test_make_service_level.name, get_make_service_level.name)
self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list)
self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group)
# Service Level
test_make_service_level = create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3)
get_make_service_level = get_service_level("_Test Service Level")
self.assertEqual(test_make_service_level.name, get_make_service_level.name)
self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list)
self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group)
def create_service_level(service_level, holiday_list, employee_group, response_time, resolution_time):
sl = frappe.get_doc({
"doctype": "Service Level",
"service_level": "__Test Service Level",
"holiday_list": "__Test Holiday List",
"priority": "Medium",
"service_level": service_level,
"holiday_list": holiday_list,
"employee_group": employee_group,
"response_time": 4,
"response_time_period": "Hour",
"resolution_time": 6,
"resolution_time_period": "Hour",
"priorities": [
{
"priority": "Low",
"response_time": response_time,
"response_time_period": "Hour",
"resolution_time": resolution_time,
"resolution_time_period": "Hour",
},
{
"priority": "Medium",
"response_time": response_time,
"default_priority": 1,
"response_time_period": "Hour",
"resolution_time": resolution_time,
"resolution_time_period": "Hour",
},
{
"priority": "High",
"response_time": response_time,
"response_time_period": "Hour",
"resolution_time": resolution_time,
"resolution_time_period": "Hour",
}
],
"support_and_resolution": [
{
"workday": "Monday",
@ -67,73 +101,21 @@ def make_service_level():
]
})
default_service_level_exists = frappe.db.exists("Service Level", "__Test Service Level")
if not default_service_level_exists:
default_service_level.insert()
sl_exists = frappe.db.exists("Service Level", {"service_level": service_level})
service_level = frappe.get_doc({
"doctype": "Service Level",
"service_level": "_Test Service Level",
"holiday_list": "__Test Holiday List",
"priority": "Medium",
"employee_group": employee_group,
"response_time": 2,
"response_time_period": "Day",
"resolution_time": 3,
"resolution_time_period": "Day",
"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",
}
]
})
service_level_exist = frappe.db.exists("Service Level", "_Test Service Level")
if not service_level_exist:
service_level.insert()
return service_level.service_level
if not sl_exists:
sl.insert()
return sl
else:
return service_level_exist
return frappe.get_doc("Service Level", {"service_level": service_level})
def get_service_level():
service_level = frappe.db.exists("Service Level", "_Test Service Level")
return service_level
def get_service_level(service_level):
return frappe.get_doc("Service Level", service_level)
def make_holiday_list():
holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
if not holiday_list:
now = datetime.datetime.now()
now = frappe.utils.now_datetime()
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
"holiday_list_name": "__Test Holiday List",
@ -153,4 +135,15 @@ def make_holiday_list():
"holiday_date": "2019-02-11"
},
]
}).insert()
}).insert()
def create_service_level_for_sla():
employee_group = make_employee_group()
make_holiday_list()
make_priorities()
# Default Service Level
create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6)
# Service Level
create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3)

View File

@ -11,11 +11,19 @@ frappe.ui.form.on('Service Level Agreement', {
name: frm.doc.service_level
},
callback: function(data){
for (var i = 0; i < data.message.support_and_resolution.length; i++){
frm.add_child("support_and_resolution", data.message.support_and_resolution[i]);
let count = Math.max(data.message.priorities.length, data.message.support_and_resolution.length);
let i = 0;
while (i < count){
if (data.message.priorities[i]) {
frm.add_child("priorities", data.message.priorities[i]);
}
if (data.message.support_and_resolution[i]) {
frm.add_child("support_and_resolution", data.message.support_and_resolution[i]);
}
i++;
}
frm.refresh();
}
});
}
},
});

View File

@ -1,764 +1,188 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "Prompt",
"beta": 0,
"autoname": "format:SLA-{service_level}-{####}",
"creation": "2018-12-26 21:08:15.448812",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"service_level",
"default_service_level_agreement",
"holiday_list",
"column_break_2",
"employee_group",
"default_priority",
"entity_section",
"entity_type",
"column_break_10",
"entity",
"agreement_details_section",
"start_date",
"active",
"column_break_7",
"end_date",
"response_and_resolution_time_section",
"priorities",
"support_and_resolution_section_break",
"support_and_resolution"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"depends_on": "eval: !doc.default_service_level_agreement",
"fetch_if_empty": 0,
"fieldname": "customer",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Customer",
"length": 0,
"no_copy": 0,
"options": "Customer",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: !doc.customer",
"fetch_if_empty": 0,
"default": "0",
"depends_on": "eval: !doc.customer;",
"fieldname": "default_service_level_agreement",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Service Level Agreement",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Default Service Level Agreement"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "service_level",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"in_standard_filter": 1,
"label": "Service Level",
"length": 0,
"no_copy": 0,
"options": "Service Level",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "service_level.holiday_list",
"fetch_if_empty": 0,
"fieldname": "holiday_list",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holiday List",
"length": 0,
"no_copy": 0,
"options": "Holiday List",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fetch_from": "service_level.priority",
"fetch_if_empty": 0,
"fieldname": "priority",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Priority",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "service_level.employee_group",
"fetch_if_empty": 0,
"fieldname": "employee_group",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee Group",
"length": 0,
"no_copy": 0,
"options": "Employee Group",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": "",
"columns": 0,
"depends_on": "eval: !doc.default_service_level_agreement",
"fetch_if_empty": 0,
"fieldname": "agreement_details_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Agreement Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Agreement Details"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: !doc.default_service_level_agreement",
"fetch_if_empty": 0,
"fieldname": "start_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Start Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Start Date"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Active",
"depends_on": "eval: !doc.default_service_level_agreement",
"fetch_if_empty": 0,
"fieldname": "agreement_status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Agreement Status",
"length": 0,
"no_copy": 0,
"options": "Active\nExpired",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: !doc.default_contract",
"fetch_if_empty": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: !doc.default_service_level_agreement",
"fetch_if_empty": 0,
"fieldname": "end_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "End Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "End Date"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"collapsible": 1,
"fieldname": "response_and_resolution_time_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Response and Resolution Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Response and Resolution Time"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "service_level.response_time",
"fetch_if_empty": 0,
"fieldname": "response_time",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Response Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "service_level.resolution_time",
"fetch_if_empty": 0,
"fieldname": "resolution_time",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Resolution Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_16",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "service_level.response_time_period",
"fetch_if_empty": 0,
"fieldname": "response_time_period",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Response Time Period",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "service_level.resolution_time_period",
"fetch_if_empty": 0,
"fieldname": "resolution_time_period",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Resolution Time Period",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "support_and_resolution_section_break",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Support and Resolution",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Support Hours"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "support_and_resolution",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Support and Resolution",
"length": 0,
"no_copy": 0,
"options": "Service Day",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Service Day"
},
{
"fieldname": "priorities",
"fieldtype": "Table",
"label": "Priorities",
"options": "Service Level Priority"
},
{
"fetch_from": "service_level.default_priority",
"fieldname": "default_priority",
"fieldtype": "Link",
"label": "Default Priority",
"options": "Issue Priority",
"read_only": 1
},
{
"default": "1",
"fieldname": "active",
"fieldtype": "Check",
"label": "Active",
"read_only": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "entity",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Entity",
"options": "entity_type"
},
{
"depends_on": "eval: !doc.default_service_level_agreement",
"fieldname": "entity_section",
"fieldtype": "Section Break",
"label": "Entity"
},
{
"fieldname": "entity_type",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Entity Type",
"options": "\nCustomer\nCustomer Group\nTerritory"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-03-17 22:36:53.576464",
"modified": "2019-06-20 18:04:14.293378",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Agreement",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View File

@ -9,37 +9,88 @@ from frappe import _
class ServiceLevelAgreement(Document):
def before_insert(self):
if self.default_service_level_agreement:
doc = frappe.get_list("Service Level Agreement", filters=[{"default_service_level_agreement": "1"}])
if doc:
frappe.throw(_("A Default Service Level Agreement already exists."))
def validate(self):
if not self.default_service_level_agreement:
if not (self.start_date and self.end_date):
frappe.throw(_("Enter Start and End Date for the Agreement."))
if self.start_date >= 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:
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 self.start_date >= self.end_date:
frappe.throw(_("Start Date of Agreement can't be greater than or equal to End Date."))
if self.end_date < frappe.utils.getdate():
frappe.throw(_("End Date of Agreement can't be less than today."))
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))
def get_service_level_agreement_priority(self, priority):
priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name})
return frappe._dict({
"priority": priority.priority,
"response_time": priority.response_time,
"response_time_period": priority.response_time_period,
"resolution_time": priority.resolution_time,
"resolution_time_period": priority.resolution_time_period
})
def check_agreement_status():
service_level_agreements = frappe.get_list("Service Level Agreement", filters=[
{"agreement_status": "Active"},
{"active": 1},
{"default_service_level_agreement": 0}
])
service_level_agreements.reverse()
], fields=["name"])
for service_level_agreement in service_level_agreements:
service_level_agreement = frappe.get_doc("Service Level Agreement", service_level_agreement)
if service_level_agreement.end_date < frappe.utils.getdate():
service_level_agreement.agreement_status = "Expired"
service_level_agreement.save()
doc = frappe.get_doc("Service Level Agreement", service_level_agreement.name)
if doc.end_date and doc.end_date < frappe.utils.getdate():
frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0)
def get_active_service_level_agreement_for(customer):
agreement = frappe.get_list("Service Level Agreement",
filters=[{"agreement_status": "Active"}],
or_filters=[{'customer': customer},{"default_service_level_agreement": "1"}],
fields=["name", "service_level", "holiday_list", "priority"],
order_by='customer DESC',
limit=1)
def get_active_service_level_agreement_for(priority, customer=None, service_level_agreement=None):
filters = [
["Service Level Agreement", "active", "=", 1],
]
return agreement[0] if agreement else None
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 service_level_agreement:
or_filters = [
["Service Level Agreement", "name", "=", service_level_agreement],
]
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"])
return agreement[0] if agreement else None
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")
@frappe.whitelist()
def get_service_level_agreement_filters(name, customer=None):
if not customer:
or_filters = [
["Service Level Agreement", "default_service_level_agreement", "=", 1]
]
else:
or_filters = [
["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), "IS NULL"]],
["Service Level Agreement", "default_service_level_agreement", "=", 1]
]
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", or_filters=or_filters)]
}

View File

@ -0,0 +1,12 @@
from frappe import _
def get_data():
return {
'fieldname': 'service_level_agreement',
'transactions': [
{
'label': _('Issue'),
'items': ['Issue']
}
]
}

View File

@ -5,29 +5,107 @@ from __future__ import unicode_literals
import frappe
import unittest
from erpnext.support.doctype.service_level.test_service_level import make_service_level
from erpnext.support.doctype.service_level.test_service_level import create_service_level_for_sla
class TestServiceLevelAgreement(unittest.TestCase):
pass
def make_service_level_agreement():
make_service_level()
def test_service_level_agreement(self):
create_service_level_for_sla()
# Default Service Level Agreement
default_service_level_agreement = frappe.get_doc({
# Default Service Level Agreement
create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1,
service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type=None, entity=None, response_time=4, resolution_time=6)
get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1)
self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name)
self.assertEqual(create_default_service_level_agreement.entity_type, get_default_service_level_agreement.entity_type)
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)
# Service Level Agreement for Customer
customer = create_customer()
create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Customer", entity=customer, response_time=2, resolution_time=3)
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)
self.assertEqual(create_customer_service_level_agreement.entity_type, get_customer_service_level_agreement.entity_type)
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",
entity_type="Customer Group", entity=customer_group, response_time=2, resolution_time=3)
get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group)
self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name)
self.assertEqual(create_customer_group_service_level_agreement.entity_type, get_customer_group_service_level_agreement.entity_type)
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",
entity_type="Territory", entity=territory, response_time=2, resolution_time=3)
get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory)
self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name)
self.assertEqual(create_territory_service_level_agreement.entity_type, get_territory_service_level_agreement.entity_type)
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}
else:
filters = {"entity_type": entity_type, "entity": entity}
service_level_agreement = frappe.get_doc("Service Level Agreement", filters)
return service_level_agreement
def create_service_level_agreement(default_service_level_agreement, service_level, holiday_list, employee_group,
response_time, entity_type, entity, resolution_time):
service_level_agreement = frappe.get_doc({
"doctype": "Service Level Agreement",
"name": "__Test Service Level Agreement",
"default_service_level_agreement": 1,
"service_level": "__Test Service Level",
"holiday_list": "__Test Holiday List",
"priority": "Medium",
"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,
"entity_type": entity_type,
"entity": entity,
"start_date": frappe.utils.getdate(),
"end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100),
"response_time": 4,
"response_time_period": "Hour",
"resolution_time": 6,
"resolution_time_period": "Hour",
"priorities": [
{
"priority": "Low",
"response_time": response_time,
"response_time_period": "Hour",
"resolution_time": resolution_time,
"resolution_time_period": "Hour",
},
{
"priority": "Medium",
"response_time": response_time,
"default_priority": 1,
"response_time_period": "Hour",
"resolution_time": resolution_time,
"resolution_time_period": "Hour",
},
{
"priority": "High",
"response_time": response_time,
"response_time_period": "Hour",
"resolution_time": resolution_time,
"resolution_time_period": "Hour",
}
],
"support_and_resolution": [
{
"workday": "Monday",
@ -67,11 +145,26 @@ def make_service_level_agreement():
]
})
default_service_level_agreement_exists = frappe.db.exists("Service Level Agreement", "__Test Service Level Agreement")
if not default_service_level_agreement_exists:
default_service_level_agreement.insert()
filters = {
"default_service_level_agreement": service_level_agreement.default_service_level_agreement,
"service_level": service_level_agreement.service_level
}
if not default_service_level_agreement:
filters.update({
"entity_type": entity_type,
"entity": entity
})
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
else:
return frappe.get_doc("Service Level Agreement", service_level_agreement_exists)
def create_customer():
customer = frappe.get_doc({
"doctype": "Customer",
"customer_name": "_Test Customer",
@ -80,70 +173,53 @@ def make_service_level_agreement():
"territory": "Rest Of The World"
})
if not frappe.db.exists("Customer", "_Test Customer"):
customer.insert()
customer.insert(ignore_permissions=True)
return customer.name
else:
customer = frappe.get_doc("Customer", "_Test Customer")
return frappe.db.exists("Customer", "_Test Customer")
service_level_agreement = frappe.get_doc({
"doctype": "Service Level Agreement",
"name": "_Test Service Level Agreement",
"customer": customer.customer_name,
"service_level": "_Test Service Level",
"holiday_list": "__Test Holiday List",
"priority": "Medium",
"employee_group": "_Test Employee Group",
"start_date": frappe.utils.getdate(),
"end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100),
"response_time": 2,
"response_time_period": "Day",
"resolution_time": 3,
"resolution_time_period": "Day",
"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",
}
]
def create_customer_group():
customer_group = frappe.get_doc({
"doctype": "Customer Group",
"customer_group_name": "_Test SLA Customer Group"
})
service_level_agreement_exists = frappe.db.exists("Service Level Agreement", "_Test Service Level Agreement")
if not service_level_agreement_exists:
service_level_agreement.insert()
return service_level_agreement.name
if not frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"}):
customer_group.insert()
return customer_group.name
else:
return service_level_agreement_exists
return frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"})
def get_service_level_agreement():
service_level_agreement = frappe.db.exists("Service Level Agreement", "_Test Service Level Agreement")
return service_level_agreement
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
else:
return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"})
def create_service_level_agreements_for_issues():
create_service_level_for_sla()
create_service_level_agreement(default_service_level_agreement=1,
service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type=None, entity=None, response_time=4, resolution_time=6)
create_customer()
create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Customer", entity="_Test Customer", response_time=2, resolution_time=3)
create_customer_group()
create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=2, resolution_time=3)
create_territory()
create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Territory", entity="_Test SLA Territory", response_time=2, resolution_time=3)

View File

@ -0,0 +1,87 @@
{
"creation": "2019-05-04 05:54:03.658991",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"priority",
"cb_01",
"default_priority",
"sb_00",
"response_time",
"response_time_period",
"cb_00",
"resolution_time",
"resolution_time_period"
],
"fields": [
{
"columns": 2,
"fieldname": "priority",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Priority",
"options": "Issue Priority"
},
{
"fieldname": "sb_00",
"fieldtype": "Section Break"
},
{
"columns": 1,
"fieldname": "response_time",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Response Time"
},
{
"columns": 1,
"fieldname": "resolution_time",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Resolution Time"
},
{
"fieldname": "cb_00",
"fieldtype": "Column Break"
},
{
"columns": 2,
"fieldname": "response_time_period",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Response Time Period",
"options": "Hour\nDay\nWeek"
},
{
"columns": 2,
"fieldname": "resolution_time_period",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Resolution Time Period",
"options": "Hour\nDay\nWeek"
},
{
"fieldname": "cb_01",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "default_priority",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Default Priority"
}
],
"istable": 1,
"modified": "2019-05-21 06:54:42.674377",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Priority",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class ServiceLevelPriority(Document):
pass