diff --git a/erpnext/config/hr.py b/erpnext/config/hr.py index 607a5fd135e..21e6a505a86 100644 --- a/erpnext/config/hr.py +++ b/erpnext/config/hr.py @@ -18,6 +18,11 @@ def get_data(): "onboard": 1, "dependencies": ["Employee"] }, + { + "type": "doctype", + "name": "Employee Group", + "dependencies": ["Employee"] + }, { "type": "doctype", "name": "Attendance", diff --git a/erpnext/config/support.py b/erpnext/config/support.py index 47dc53e4488..0301bb3e196 100644 --- a/erpnext/config/support.py +++ b/erpnext/config/support.py @@ -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", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 1d1dc627122..6ce75bbac36 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -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" diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9c1f3eaf728..dcf91a4a697 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -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 \ No newline at end of file +erpnext.patches.v12_0.set_quotation_status +erpnext.patches.v12_0.set_priority_for_support diff --git a/erpnext/patches/v12_0/set_priority_for_support.py b/erpnext/patches/v12_0/set_priority_for_support.py new file mode 100644 index 00000000000..cc290396f84 --- /dev/null +++ b/erpnext/patches/v12_0/set_priority_for_support.py @@ -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") \ No newline at end of file diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index 388ddcaadab..f62613ea1bb 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -8,7 +8,7 @@ from frappe import _ from frappe.utils.nestedset import NestedSet class CustomerGroup(NestedSet): - nsm_parent_field = 'parent_customer_group'; + nsm_parent_field = 'parent_customer_group' def on_update(self): self.validate_name_with_customer() diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 89f4e3036e8..4b734df61d5 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -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')}, diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 97b34e94018..ba54edcf399 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -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( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + 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( + '
' + + '
' + + ' ' + + '
' + + '
' + + ' ' + + '
' + + '
' + ); } -function get_time_left_element(label, timestamp) { - $('.'+ frappe.scrub(label) +'').remove(); - return ` -
-
-
- -
-
- -
-
-
- `; -} - -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}; } diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 91b718e86f2..3ad5153a6b5 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -1,1309 +1,364 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", - "beta": 0, - "creation": "2013-02-01 10:36:25", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "allow_import": 1, + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2013-02-01 10:36:25", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "subject_section", + "naming_series", + "subject", + "customer", + "raised_by", + "cb00", + "status", + "priority", + "issue_type", + "sb_details", + "description", + "service_level_section", + "service_level_agreement", + "response_by", + "response_by_variance", + "cb", + "agreement_fulfilled", + "resolution_by", + "resolution_by_variance", + "response", + "mins_to_first_response", + "first_responded_on", + "additional_info", + "lead", + "contact", + "email_account", + "column_break_16", + "customer_name", + "project", + "company", + "section_break_19", + "resolution_details", + "column_break1", + "opening_date", + "opening_time", + "resolution_date", + "content_type", + "attachment", + "via_customer_portal" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "subject_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": "Subject", - "length": 0, - "no_copy": 0, - "options": "fa fa-flag", - "permlevel": 0, - "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 - }, + "fieldname": "subject_section", + "fieldtype": "Section Break", + "label": "Subject", + "options": "fa fa-flag" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fetch_if_empty": 0, - "fieldname": "naming_series", - "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": "Series", - "length": 0, - "no_copy": 1, - "options": "ISS-.YYYY.-", - "permlevel": 0, - "print_hide": 1, - "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": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "ISS-.YYYY.-", + "print_hide": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "subject", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Subject", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "bold": 1, + "fieldname": "subject", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Subject", + "reqd": 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": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer", - "oldfieldtype": "Link", - "options": "Customer", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer", + "fieldtype": "Link", + "in_global_search": 1, + "label": "Customer", + "oldfieldname": "customer", + "oldfieldtype": "Link", + "options": "Customer", + "print_hide": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.__islocal", - "fetch_if_empty": 0, - "fieldname": "raised_by", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Raised By (Email)", - "length": 0, - "no_copy": 0, - "oldfieldname": "raised_by", - "oldfieldtype": "Data", - "options": "Email", - "permlevel": 0, - "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 - }, + "bold": 1, + "depends_on": "eval:doc.__islocal", + "fieldname": "raised_by", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Raised By (Email)", + "oldfieldname": "raised_by", + "oldfieldtype": "Data", + "options": "Email" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "cb00", - "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, - "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 - }, + "fieldname": "cb00", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Open", - "fetch_if_empty": 0, - "fieldname": "status", - "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": 1, - "label": "Status", - "length": 0, - "no_copy": 1, - "oldfieldname": "status", - "oldfieldtype": "Select", - "options": "Open\nReplied\nHold\nClosed", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "oldfieldname": "status", + "oldfieldtype": "Select", + "options": "Open\nReplied\nHold\nClosed", + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Medium", - "depends_on": "", - "fetch_if_empty": 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": 0, - "in_standard_filter": 1, - "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 - }, + "fieldname": "priority", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Priority", + "options": "Issue Priority" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "issue_type", - "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": "Issue Type", - "length": 0, - "no_copy": 0, - "options": "Issue Type", - "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 - }, + "fieldname": "issue_type", + "fieldtype": "Link", + "label": "Issue Type", + "options": "Issue Type" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval:doc.status!=\"Closed\"", - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "sb_details", - "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": "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 - }, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.status!=\"Closed\"", + "fieldname": "sb_details", + "fieldtype": "Section Break", + "label": "Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "problem_description", - "oldfieldtype": "Text", - "permlevel": 0, - "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 - }, + "bold": 1, + "fieldname": "description", + "fieldtype": "Text Editor", + "in_global_search": 1, + "label": "Description", + "oldfieldname": "problem_description", + "oldfieldtype": "Text" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "depends_on": "eval: doc.service_level_agreement", - "fetch_if_empty": 0, - "fieldname": "service_level_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": "Service 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "depends_on": "eval: doc.service_level_agreement", + "fieldname": "service_level_section", + "fieldtype": "Section Break", + "label": "Service Level" + }, { - "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_agreement", - "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": "Service Level Agreement", - "length": 0, - "no_copy": 0, - "options": "Service Level Agreement", - "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 - }, + "fieldname": "service_level_agreement", + "fieldtype": "Link", + "label": "Service Level Agreement", + "options": "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": "response_by", - "fieldtype": "Datetime", - "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 By", - "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 - }, + "fieldname": "response_by", + "fieldtype": "Datetime", + "label": "Response By", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "cb", - "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, - "label": "", - "length": 0, - "no_copy": 0, - "options": "fa fa-pushpin", - "permlevel": 0, - "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 - }, + "collapsible": 1, + "fieldname": "cb", + "fieldtype": "Column Break", + "options": "fa fa-pushpin", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Ongoing", - "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": "Ongoing\nFulfilled\nFailed", - "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 - }, + "fieldname": "resolution_by", + "fieldtype": "Datetime", + "label": "Resolution By", + "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": "resolution_by", - "fieldtype": "Datetime", - "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 By", - "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 - }, + "collapsible": 1, + "fieldname": "response", + "fieldtype": "Section Break", + "label": "Response" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "response", - "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", - "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 - }, + "bold": 1, + "fieldname": "mins_to_first_response", + "fieldtype": "Float", + "label": "Mins to First Response", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "mins_to_first_response", - "fieldtype": "Float", - "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": "Mins to First Response", - "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 - }, + "fieldname": "first_responded_on", + "fieldtype": "Datetime", + "label": "First Responded On" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "first_responded_on", - "fieldtype": "Datetime", - "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": "First Responded On", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "collapsible": 1, + "fieldname": "additional_info", + "fieldtype": "Section Break", + "label": "Reference", + "options": "fa fa-pushpin", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "additional_info", - "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": "Reference", - "length": 0, - "no_copy": 0, - "options": "fa fa-pushpin", - "permlevel": 0, - "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 - }, + "fieldname": "lead", + "fieldtype": "Link", + "label": "Lead", + "options": "Lead" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "lead", - "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": "Lead", - "length": 0, - "no_copy": 0, - "options": "Lead", - "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 - }, + "fieldname": "contact", + "fieldtype": "Link", + "label": "Contact", + "options": "Contact" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "contact", - "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": "Contact", - "length": 0, - "no_copy": 0, - "options": "Contact", - "permlevel": 0, - "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 - }, + "fieldname": "email_account", + "fieldtype": "Link", + "label": "Email Account", + "options": "Email Account" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "email_account", - "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": "Email Account", - "length": 0, - "no_copy": 0, - "options": "Email Account", - "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 - }, + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, { - "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 - }, + "bold": 1, + "fieldname": "customer_name", + "fieldtype": "Data", + "label": "Customer Name", + "oldfieldname": "customer_name", + "oldfieldtype": "Data", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "customer_name", - "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": "Customer Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer_name", - "oldfieldtype": "Data", - "permlevel": 0, - "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 - }, + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "project", - "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": "Project", - "length": 0, - "no_copy": 0, - "options": "Project", - "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 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "print_hide": 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": "company", - "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": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "print_hide": 1, - "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 - }, + "collapsible": 1, + "fieldname": "section_break_19", + "fieldtype": "Section Break", + "label": "Resolution" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_19", - "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": "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 - }, + "depends_on": "eval:!doc.__islocal", + "fieldname": "resolution_details", + "fieldtype": "Text Editor", + "label": "Resolution Details", + "no_copy": 1, + "oldfieldname": "resolution_details", + "oldfieldtype": "Text" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.__islocal", - "fetch_if_empty": 0, - "fieldname": "resolution_details", - "fieldtype": "Text Editor", - "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 Details", - "length": 0, - "no_copy": 1, - "oldfieldname": "resolution_details", - "oldfieldtype": "Text", - "permlevel": 0, - "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 - }, + "depends_on": "eval:!doc.__islocal", + "fieldname": "column_break1", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.__islocal", - "fetch_if_empty": 0, - "fieldname": "column_break1", - "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, - "oldfieldtype": "Column Break", - "permlevel": 0, - "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 - }, + "default": "Today", + "fieldname": "opening_date", + "fieldtype": "Date", + "label": "Opening Date", + "no_copy": 1, + "oldfieldname": "opening_date", + "oldfieldtype": "Date", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fetch_if_empty": 0, - "fieldname": "opening_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": "Opening Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "opening_date", - "oldfieldtype": "Date", - "permlevel": 0, - "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 - }, + "fieldname": "opening_time", + "fieldtype": "Time", + "label": "Opening Time", + "no_copy": 1, + "oldfieldname": "opening_time", + "oldfieldtype": "Time", + "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": "opening_time", - "fieldtype": "Time", - "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": "Opening Time", - "length": 0, - "no_copy": 1, - "oldfieldname": "opening_time", - "oldfieldtype": "Time", - "permlevel": 0, - "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 - }, + "depends_on": "eval:!doc.__islocal", + "fieldname": "resolution_date", + "fieldtype": "Datetime", + "label": "Resolution Date", + "no_copy": 1, + "oldfieldname": "resolution_date", + "oldfieldtype": "Date", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.__islocal", - "fetch_if_empty": 0, - "fieldname": "resolution_date", - "fieldtype": "Datetime", - "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 Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "resolution_date", - "oldfieldtype": "Date", - "permlevel": 0, - "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 - }, + "fieldname": "content_type", + "fieldtype": "Data", + "hidden": 1, + "label": "Content Type" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "content_type", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Content Type", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "attachment", + "fieldtype": "Attach", + "hidden": 1, + "label": "Attachment" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "attachment", - "fieldtype": "Attach", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Attachment", - "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 - }, + "default": "0", + "fieldname": "via_customer_portal", + "fieldtype": "Check", + "label": "Via Customer Portal" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "via_customer_portal", - "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": "Via Customer Portal", - "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 + "default": "Ongoing", + "fieldname": "agreement_fulfilled", + "fieldtype": "Select", + "label": "Service Level Agreement Fulfilled", + "options": "Ongoing\nFulfilled\nFailed", + "read_only": 1 + }, + { + "description": "in hours", + "fieldname": "response_by_variance", + "fieldtype": "Float", + "label": "Response By Variance", + "read_only": 1 + }, + { + "description": "in hours", + "fieldname": "resolution_by_variance", + "fieldtype": "Float", + "label": "Resolution By Variance", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "icon": "fa fa-ticket", - "idx": 7, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-23 18:04:42.039620", + ], + "icon": "fa fa-ticket", + "idx": 7, + "modified": "2019-05-20 15:19:00.771333", "modified_by": "Administrator", - "module": "Support", - "name": "Issue", - "owner": "Administrator", + "module": "Support", + "name": "Issue", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Support Team", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Support Team", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "search_fields": "status,customer,subject,raised_by", - "show_name_in_global_search": 0, - "sort_order": "ASC", - "timeline_field": "customer", - "title_field": "subject", - "track_changes": 0, - "track_seen": 1, - "track_views": 0 + ], + "quick_entry": 1, + "search_fields": "status,customer,subject,raised_by", + "sort_field": "modified", + "sort_order": "ASC", + "timeline_field": "customer", + "title_field": "subject", + "track_seen": 1 } \ No newline at end of file diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index dca06d45373..0f6dd72f04d 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -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) \ No newline at end of file diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 48ba1f6d3fb..1296b3609de 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -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 \ No newline at end of file + 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) diff --git a/erpnext/support/doctype/issue_priority/__init__.py b/erpnext/support/doctype/issue_priority/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/support/doctype/issue_priority/issue_priority.js b/erpnext/support/doctype/issue_priority/issue_priority.js new file mode 100644 index 00000000000..37ce6a54bf7 --- /dev/null +++ b/erpnext/support/doctype/issue_priority/issue_priority.js @@ -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) { + + // } +}); diff --git a/erpnext/support/doctype/issue_priority/issue_priority.json b/erpnext/support/doctype/issue_priority/issue_priority.json new file mode 100644 index 00000000000..38a187b56ff --- /dev/null +++ b/erpnext/support/doctype/issue_priority/issue_priority.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/support/doctype/issue_priority/issue_priority.py b/erpnext/support/doctype/issue_priority/issue_priority.py new file mode 100644 index 00000000000..cecaaaab299 --- /dev/null +++ b/erpnext/support/doctype/issue_priority/issue_priority.py @@ -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")) diff --git a/erpnext/support/doctype/issue_priority/test_issue_priority.py b/erpnext/support/doctype/issue_priority/test_issue_priority.py new file mode 100644 index 00000000000..a7b55f8a74c --- /dev/null +++ b/erpnext/support/doctype/issue_priority/test_issue_priority.py @@ -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) \ No newline at end of file diff --git a/erpnext/support/doctype/service_day/service_day.json b/erpnext/support/doctype/service_day/service_day.json index 8ed006dc406..68614b18072 100644 --- a/erpnext/support/doctype/service_day/service_day.json +++ b/erpnext/support/doctype/service_day/service_day.json @@ -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 } \ No newline at end of file diff --git a/erpnext/support/doctype/service_level/service_level.json b/erpnext/support/doctype/service_level/service_level.json index 2dd335e2cca..dced3aa9e90 100644 --- a/erpnext/support/doctype/service_level/service_level.json +++ b/erpnext/support/doctype/service_level/service_level.json @@ -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" } \ No newline at end of file diff --git a/erpnext/support/doctype/service_level/service_level.py b/erpnext/support/doctype/service_level/service_level.py index 4b41e535608..4e70a01c7a9 100644 --- a/erpnext/support/doctype/service_level/service_level.py +++ b/erpnext/support/doctype/service_level/service_level.py @@ -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")) \ No newline at end of file +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) diff --git a/erpnext/support/doctype/service_level/service_level_dashboard.py b/erpnext/support/doctype/service_level/service_level_dashboard.py new file mode 100644 index 00000000000..393095e1179 --- /dev/null +++ b/erpnext/support/doctype/service_level/service_level_dashboard.py @@ -0,0 +1,12 @@ +from frappe import _ + +def get_data(): + return { + 'fieldname': 'service_level', + 'transactions': [ + { + 'label': _('Service Level Agreement'), + 'items': ['Service Level Agreement'] + } + ] + } \ No newline at end of file diff --git a/erpnext/support/doctype/service_level/test_service_level.py b/erpnext/support/doctype/service_level/test_service_level.py index 3843e31c0f1..09577df1663 100644 --- a/erpnext/support/doctype/service_level/test_service_level.py +++ b/erpnext/support/doctype/service_level/test_service_level.py @@ -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() \ No newline at end of file + }).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) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index 884e3eb52fe..1d486f48346 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -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(); } }); - } + }, }); diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 27d7ad5ae2f..f91a80c60d1 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -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 } \ No newline at end of file diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 911d06905b3..332bf63580a 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -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 \ No newline at end of file + 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)] + } \ No newline at end of file diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py new file mode 100644 index 00000000000..f2bd6813965 --- /dev/null +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py @@ -0,0 +1,12 @@ +from frappe import _ + +def get_data(): + return { + 'fieldname': 'service_level_agreement', + 'transactions': [ + { + 'label': _('Issue'), + 'items': ['Issue'] + } + ] + } \ No newline at end of file diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index e5737e03038..6aa53941921 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -5,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 \ No newline at end of file +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) diff --git a/erpnext/support/doctype/service_level_priority/__init__.py b/erpnext/support/doctype/service_level_priority/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/support/doctype/service_level_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json new file mode 100644 index 00000000000..cd87a1c1131 --- /dev/null +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/support/doctype/service_level_priority/service_level_priority.py b/erpnext/support/doctype/service_level_priority/service_level_priority.py new file mode 100644 index 00000000000..0c0fe4a0669 --- /dev/null +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.py @@ -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