Skip to content

Commit

Permalink
feat(SLA): Apply SLA to any document (frappe#22449)
Browse files Browse the repository at this point in the history
  • Loading branch information
hrwX authored Jun 14, 2021
1 parent f83c5d9 commit cbb66be
Show file tree
Hide file tree
Showing 16 changed files with 1,358 additions and 603 deletions.
8 changes: 6 additions & 2 deletions erpnext/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@

doc_events = {
"*": {
"validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
"on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record",
"on_update_after_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record",
"on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record"
Expand All @@ -242,6 +243,9 @@
"on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions",
"erpnext.portal.utils.set_default_role"]
},
"Communication": {
"on_update": "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time"
},
("Sales Taxes and Charges Template", 'Price List'): {
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
},
Expand Down Expand Up @@ -332,8 +336,8 @@
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders"
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
"erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance"
],
"hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
Expand Down
1 change: 1 addition & 0 deletions erpnext/patches.txt
Original file line number Diff line number Diff line change
Expand Up @@ -285,4 +285,5 @@ erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed
erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.add_doctype_to_sla
erpnext.patches.v13_0.set_training_event_attendance
20 changes: 20 additions & 0 deletions erpnext/patches/v13_0/add_doctype_to_sla.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt

from __future__ import unicode_literals

import frappe
from frappe.model.utils.rename_field import rename_field

def execute():
frappe.reload_doc('support', 'doctype', 'service_level_agreement')
if frappe.db.has_column('Service Level Agreement', 'enable'):
rename_field('Service Level Agreement', 'enable', 'enabled')

for sla in frappe.get_all('Service Level Agreement'):
agreement = frappe.get_doc('Service Level Agreement', sla.name)
agreement.document_type = 'Issue'
agreement.apply_sla_for_resolution = 1
agreement.append('sla_fulfilled_on', {'status': 'Resolved'})
agreement.append('sla_fulfilled_on', {'status': 'Closed'})
agreement.save()
145 changes: 145 additions & 0 deletions erpnext/public/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,151 @@ $(document).on('app_ready', function() {
}
});

// Show SLA dashboard
$(document).on('app_ready', function() {
frappe.call({
method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_sla_doctypes',
callback: function(r) {
if (!r.message)
return;

$.each(r.message, function(_i, d) {
frappe.ui.form.on(d, {
onload: function(frm) {
if (!frm.doc.service_level_agreement)
return;

frappe.call({
method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters',
args: {
doctype: frm.doc.doctype,
name: frm.doc.service_level_agreement,
customer: frm.doc.customer
},
callback: function (r) {
if (r && r.message) {
frm.set_query('priority', function() {
return {
filters: {
'name': ['in', r.message.priority],
}
};
});
frm.set_query('service_level_agreement', function() {
return {
filters: {
'name': ['in', r.message.service_level_agreements],
}
};
});
}
}
});
},

refresh: function(frm) {
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
&& frm.doc.agreement_status === 'Ongoing') {
frappe.call({
'method': 'frappe.client.get',
args: {
doctype: 'Service Level Agreement',
name: frm.doc.service_level_agreement
},
callback: function(data) {
let statuses = data.message.pause_sla_on;
const hold_statuses = [];
$.each(statuses, (_i, entry) => {
hold_statuses.push(entry.status);
});
if (hold_statuses.includes(frm.doc.status)) {
frm.dashboard.clear_headline();
let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ message.indicator +'"><span>'+ message.msg +'</span></span> ' +
'</div>' +
'</div>'
);
} else {
set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution);
}
}
});
} else if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();

let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ?
{'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} :
{'indicator': 'red', 'msg': 'Service Level Agreement Failed'};

frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ agreement_status.indicator +'"><span class="hidden-xs">'+ agreement_status.msg +'</span></span> ' +
'</div>' +
'</div>'
);
}
},
});
});
}
});
});

function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
frm.dashboard.clear_headline();

let time_to_respond = get_status(frm.doc.response_by_variance);
if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') {
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
}

let alert = `
<div class="row">
<div class="col-xs-12 col-sm-6">
<span class="indicator whitespace-nowrap ${time_to_respond.indicator}">
<span>Time to Respond: ${time_to_respond.diff_display}</span>
</span>
</div>`;


if (apply_sla_for_resolution) {
let time_to_resolve = get_status(frm.doc.resolution_by_variance);
if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') {
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
}

alert += `
<div class="col-xs-12 col-sm-6">
<span class="indicator whitespace-nowrap ${time_to_resolve.indicator}">
<span>Time to Resolve: ${time_to_resolve.diff_display}</span>
</span>
</div>`;
}

alert += '</div>';

frm.dashboard.set_headline_alert(alert);
}

function get_time_left(timestamp, agreement_status) {
const diff = moment(timestamp).diff(moment());
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed';
let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green';
return {'diff_display': diff_display, 'indicator': indicator};
}

function get_status(variance) {
if (variance > 0) {
return {'diff_display': 'Fulfilled', 'indicator': 'green'};
} else {
return {'diff_display': 'Failed', 'indicator': 'red'};
}
}

function attach_selector_button(inner_text, append_loction, context, grid_row) {
let $btn_div = $("<div>").css({"margin-bottom": "10px", "margin-top": "10px"})
.appendTo(append_loction);
Expand Down
135 changes: 8 additions & 127 deletions erpnext/support/doctype/issue/issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,94 +9,15 @@ frappe.ui.form.on("Issue", {
};
});

if (frappe.model.can_read("Support Settings")) {
frappe.db.get_value("Support Settings", {name: "Support Settings"},
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
if (r && r.track_service_level_agreement == "0") {
frm.set_df_property("service_level_section", "hidden", 1);
}
if (r && r.allow_resetting_service_level_agreement == "0") {
frm.set_df_property("reset_service_level_agreement", "hidden", 1);
}
});
}

if (frm.doc.service_level_agreement) {
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],
}
};
});
}
frappe.db.get_value("Support Settings", {name: "Support Settings"},
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
if (r && r.track_service_level_agreement == "0") {
frm.set_df_property("service_level_section", "hidden", 1);
}
});
}
},

refresh: function(frm) {

// alert messages
if (frm.doc.status !== "Closed" && frm.doc.service_level_agreement
&& frm.doc.agreement_status === "Ongoing") {
frappe.call({
"method": "frappe.client.get",
args: {
doctype: "Service Level Agreement",
name: frm.doc.service_level_agreement
},
callback: function(data) {
let statuses = data.message.pause_sla_on;
const hold_statuses = [];
$.each(statuses, (_i, entry) => {
hold_statuses.push(entry.status);
});
if (hold_statuses.includes(frm.doc.status)) {
frm.dashboard.clear_headline();
let message = { "indicator": "orange", "msg": __("SLA is on hold since {0}", [moment(frm.doc.on_hold_since).fromNow(true)]) };
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap ' + message.indicator + '"><span>' + message.msg + '</span></span> ' +
'</div>' +
'</div>'
);
} else {
set_time_to_resolve_and_response(frm);
}
if (r && r.allow_resetting_service_level_agreement == "0") {
frm.set_df_property("reset_service_level_agreement", "hidden", 1);
}
});
} else if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();

let agreement_status = (frm.doc.agreement_status == "Fulfilled") ?
{ "indicator": "green", "msg": "Service Level Agreement has been fulfilled" } :
{ "indicator": "red", "msg": "Service Level Agreement Failed" };

frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap ' + agreement_status.indicator + '"><span class="hidden-xs">' + agreement_status.msg + '</span></span> ' +
'</div>' +
'</div>'
);
}

// buttons
if (frm.doc.status !== "Closed") {
Expand Down Expand Up @@ -142,7 +63,7 @@ frappe.ui.form.on("Issue", {
message: __("Resetting Service Level Agreement.")
});

frm.call("reset_service_level_agreement", {
frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", {
reason: values.reason,
user: frappe.session.user_email
}, () => {
Expand Down Expand Up @@ -224,44 +145,4 @@ frappe.ui.form.on("Issue", {
// frm.timeline.wrapper.data("help-article-event-attached", true);
// }
},
});

function set_time_to_resolve_and_response(frm) {
frm.dashboard.clear_headline();

var time_to_respond = get_status(frm.doc.response_by_variance);
if (!frm.doc.first_responded_on && frm.doc.agreement_status === "Ongoing") {
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
}

var time_to_resolve = get_status(frm.doc.resolution_by_variance);
if (!frm.doc.resolution_date && frm.doc.agreement_status === "Ongoing") {
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
}

frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12 col-sm-6">' +
'<span class="indicator whitespace-nowrap '+ time_to_respond.indicator +'"><span>Time to Respond: '+ time_to_respond.diff_display +'</span></span> ' +
'</div>' +
'<div class="col-xs-12 col-sm-6">' +
'<span class="indicator whitespace-nowrap '+ time_to_resolve.indicator +'"><span>Time to Resolve: '+ time_to_resolve.diff_display +'</span></span> ' +
'</div>' +
'</div>'
);
}

function get_time_left(timestamp, agreement_status) {
const diff = moment(timestamp).diff(moment());
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed";
let indicator = (diff_display == "Failed" && agreement_status != "Fulfilled") ? "red" : "green";
return {"diff_display": diff_display, "indicator": indicator};
}

function get_status(variance) {
if (variance > 0) {
return {"diff_display": "Fulfilled", "indicator": "green"};
} else {
return {"diff_display": "Failed", "indicator": "red"};
}
}
});
Loading

0 comments on commit cbb66be

Please sign in to comment.