Merge branch 'develop' into better-logging-for-membership
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 01d3903..c12e006d 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -320,7 +320,6 @@
entry['remarks'] = "On cancellation of " + entry['voucher_no']
entry['is_cancelled'] = 1
- entry['posting_date'] = today()
if entry['debit'] or entry['credit']:
make_entry(entry, adv_adj, "Yes")
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 2563b66..84c7454 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -268,9 +268,9 @@
def get_last_purchase_rate(self, item_code, row):
condition = ''
if row.project:
- condition += " AND a.project='%s'" % (row.project)
+ condition += " AND a.project=%s" % (frappe.db.escape(row.project))
elif row.cost_center:
- condition += " AND a.cost_center='%s'" % (row.cost_center)
+ condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center))
if self.filters.to_date:
condition += " AND modified='%s'" % (self.filters.to_date)
diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
index af109ba..e956afd 100644
--- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
+++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
@@ -178,6 +178,7 @@
period_card = make_supplier_scorecard(docname, None)
period_card.start_date = start_date
period_card.end_date = end_date
+ period_card.insert(ignore_permissions=True)
period_card.submit()
scp_count = scp_count + 1
if start_date < first_start_date:
diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py
index 87f1033..9938710 100644
--- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py
+++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py
@@ -106,7 +106,7 @@
"doctype": "Supplier Scorecard Scoring Criteria",
"postprocess": update_criteria_fields,
}
- }, target_doc, post_process)
+ }, target_doc, post_process, ignore_permissions=True)
return doc
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 046fb2c..90c466b 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1264,7 +1264,10 @@
transitions.append(transition.as_dict())
if not transitions:
- frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions"))
+ frappe.throw(
+ _("You are not allowed to update as per the conditions set in {} Workflow.").format(get_link_to_form("Workflow", workflow)),
+ title=_("Insufficient Permissions")
+ )
def get_new_child_item(item_row):
new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json
index eb69dc0..d974beb 100644
--- a/erpnext/crm/desk_page/crm/crm.json
+++ b/erpnext/crm/desk_page/crm/crm.json
@@ -8,7 +8,7 @@
{
"hidden": 0,
"label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Opportunity\",\n \"name\": \"Minutes to First Response for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]"
+ "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Opportunity\",\n \"name\": \"First Response Time for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
@@ -42,7 +42,7 @@
"idx": 0,
"is_standard": 1,
"label": "CRM",
- "modified": "2020-05-28 13:33:52.906750",
+ "modified": "2020-08-11 18:55:18.238900",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM",
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index b61cad3..eee13f7 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -24,7 +24,7 @@
"converted_by",
"sales_stage",
"order_lost_reason",
- "mins_to_first_response",
+ "first_response_time",
"expected_closing",
"next_contact",
"contact_by",
@@ -153,13 +153,6 @@
"read_only": 1
},
{
- "bold": 1,
- "fieldname": "mins_to_first_response",
- "fieldtype": "Float",
- "label": "Mins to first response",
- "read_only": 1
- },
- {
"fieldname": "expected_closing",
"fieldtype": "Date",
"label": "Expected Closing Date"
@@ -419,12 +412,19 @@
"fieldtype": "Link",
"label": "Converted By",
"options": "User"
+ },
+ {
+ "bold": 1,
+ "fieldname": "first_response_time",
+ "fieldtype": "Duration",
+ "label": "First Response Time",
+ "read_only": 1
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "modified": "2020-08-11 17:34:35.066961",
+ "modified": "2020-08-12 17:34:35.066961",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/__init__.py b/erpnext/crm/report/first_response_time_for_opportunity/__init__.py
similarity index 100%
rename from erpnext/crm/report/minutes_to_first_response_for_opportunity/__init__.py
rename to erpnext/crm/report/first_response_time_for_opportunity/__init__.py
diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
new file mode 100644
index 0000000..3f5c95a
--- /dev/null
+++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
@@ -0,0 +1,44 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["First Response Time for Opportunity"] = {
+ "filters": [
+ {
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
+ },
+ {
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.nowdate()
+ },
+ ],
+ get_chart_data: function (_columns, result) {
+ return {
+ data: {
+ labels: result.map(d => d[0]),
+ datasets: [{
+ name: "First Response Time",
+ values: result.map(d => d[1])
+ }]
+ },
+ type: "line",
+ tooltipOptions: {
+ formatTooltipY: d => {
+ let duration_options = {
+ hide_days: 0,
+ hide_seconds: 0
+ };
+ value = frappe.utils.get_formatted_duration(d, duration_options);
+ return value;
+ }
+ }
+ }
+ }
+};
diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json
new file mode 100644
index 0000000..1b3184f
--- /dev/null
+++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json
@@ -0,0 +1,28 @@
+{
+ "add_total_row": 0,
+ "creation": "2020-08-10 18:34:19.083872",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "Test 2",
+ "modified": "2020-08-10 18:34:19.083872",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "First Response Time for Opportunity",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Opportunity",
+ "report_name": "First Response Time for Opportunity",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Sales User"
+ },
+ {
+ "role": "Sales Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py
new file mode 100644
index 0000000..2ffbc3e
--- /dev/null
+++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute(filters=None):
+ columns = [
+ {
+ 'fieldname': 'creation_date',
+ 'label': 'Date',
+ 'fieldtype': 'Date',
+ 'width': 300
+ },
+ {
+ 'fieldname': 'first_response_time',
+ 'fieldtype': 'Duration',
+ 'label': 'First Response Time',
+ 'width': 300
+ },
+ ]
+
+ data = frappe.db.sql('''
+ SELECT
+ date(creation) as creation_date,
+ avg(first_response_time) as avg_response_time
+ FROM tabOpportunity
+ WHERE
+ date(creation) between %s and %s
+ and first_response_time > 0
+ GROUP BY creation_date
+ ORDER BY creation_date desc
+ ''', (filters.from_date, filters.to_date))
+
+ return columns, data
diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js b/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js
deleted file mode 100644
index 92d026a..0000000
--- a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.query_reports["Minutes to First Response for Opportunity"] = {
- "filters": [
- {
- "fieldname": "from_date",
- "label": __("From Date"),
- "fieldtype": "Date",
- 'reqd': 1,
- "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
- },
- {
- "fieldname": "to_date",
- "label": __("To Date"),
- "fieldtype": "Date",
- 'reqd': 1,
- "default": frappe.datetime.nowdate()
- },
- ],
- get_chart_data: function (columns, result) {
- return {
- data: {
- labels: result.map(d => d[0]),
- datasets: [{
- name: 'Mins to first response',
- values: result.map(d => d[1])
- }]
- },
- type: 'line',
- }
- }
-}
diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json b/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json
deleted file mode 100644
index bcd092b..0000000
--- a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "add_total_row": 0,
- "apply_user_permissions": 0,
- "creation": "2016-06-17 11:28:25.867258",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 2,
- "is_standard": "Yes",
- "modified": "2017-02-24 20:06:08.801109",
- "modified_by": "Administrator",
- "module": "CRM",
- "name": "Minutes to First Response for Opportunity",
- "owner": "Administrator",
- "ref_doctype": "Opportunity",
- "report_name": "Minutes to First Response for Opportunity",
- "report_type": "Script Report",
- "roles": [
- {
- "role": "Sales User"
- },
- {
- "role": "Sales Manager"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py b/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py
deleted file mode 100644
index 54e3a60..0000000
--- a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute(filters=None):
- columns = [
- {
- 'fieldname': 'creation_date',
- 'label': 'Date',
- 'fieldtype': 'Date'
- },
- {
- 'fieldname': 'mins',
- 'fieldtype': 'Float',
- 'label': 'Mins to First Response'
- },
- ]
-
- data = frappe.db.sql('''select date(creation) as creation_date,
- avg(mins_to_first_response) as mins
- from tabOpportunity
- where date(creation) between %s and %s
- and mins_to_first_response > 0
- group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date))
-
- return columns, data
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index c1aa4a6..6087ce2 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -726,5 +726,6 @@
erpnext.patches.v13_0.drop_razorpay_payload_column
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment
erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports
+erpnext.patches.v13_0.rename_issue_doctype_fields
erpnext.patches.v13_0.change_default_pos_print_format
erpnext.patches.v13_0.set_youtube_video_id
diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
new file mode 100644
index 0000000..5bd6596
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2020, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ if frappe.db.exists('DocType', 'Issue'):
+ issues = frappe.db.get_all('Issue', fields=['name', 'response_by_variance', 'resolution_by_variance', 'mins_to_first_response'],
+ order_by='creation desc')
+ frappe.reload_doc('support', 'doctype', 'issue')
+
+ # rename fields
+ rename_map = {
+ 'agreement_fulfilled': 'agreement_status',
+ 'mins_to_first_response': 'first_response_time'
+ }
+ for old, new in rename_map.items():
+ rename_field('Issue', old, new)
+
+ # change fieldtype to duration
+ count = 0
+ for entry in issues:
+ response_by_variance = convert_to_seconds(entry.response_by_variance, 'Hours')
+ resolution_by_variance = convert_to_seconds(entry.resolution_by_variance, 'Hours')
+ mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes')
+ frappe.db.set_value('Issue', entry.name, {
+ 'response_by_variance': response_by_variance,
+ 'resolution_by_variance': resolution_by_variance,
+ 'first_response_time': mins_to_first_response
+ })
+ # commit after every 100 updates
+ count += 1
+ if count%100 == 0:
+ frappe.db.commit()
+
+ if frappe.db.exists('DocType', 'Opportunity'):
+ opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc')
+ frappe.reload_doc('crm', 'doctype', 'opportunity')
+ rename_field('Opportunity', 'mins_to_first_response', 'first_response_time')
+
+ # change fieldtype to duration
+ count = 0
+ for entry in opportunities:
+ mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes')
+ frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response)
+ # commit after every 100 updates
+ count += 1
+ if count%100 == 0:
+ frappe.db.commit()
+
+ # renamed reports from "Minutes to First Response for Issues" to "First Response Time for Issues". Same for Opportunity
+ for report in ['Minutes to First Response for Issues', 'Minutes to First Response for Opportunity']:
+ if frappe.db.exists('Report', report):
+ frappe.delete_doc('Report', report)
+
+
+def convert_to_seconds(value, unit):
+ seconds = 0
+ if unit == 'Hours':
+ seconds = value * 3600
+ if unit == 'Minutes':
+ seconds = value * 60
+ return seconds
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 38e5fe5..faeeb57 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -26,19 +26,19 @@
refresh: function(frm) {
if (frm.doc.is_stock_item) {
- frm.add_custom_button(__("Balance"), function() {
+ frm.add_custom_button(__("Stock Balance"), function() {
frappe.route_options = {
"item_code": frm.doc.name
}
frappe.set_route("query-report", "Stock Balance");
}, __("View"));
- frm.add_custom_button(__("Ledger"), function() {
+ frm.add_custom_button(__("Stock Ledger"), function() {
frappe.route_options = {
"item_code": frm.doc.name
}
frappe.set_route("query-report", "Stock Ledger");
}, __("View"));
- frm.add_custom_button(__("Projected"), function() {
+ frm.add_custom_button(__("Stock Projected Qty"), function() {
frappe.route_options = {
"item_code": frm.doc.name
}
diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json
index b1ad7c8..28410f3 100644
--- a/erpnext/support/desk_page/support/support.json
+++ b/erpnext/support/desk_page/support/support.json
@@ -28,7 +28,7 @@
{
"hidden": 0,
"label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Issues\",\n \"name\": \"Minutes to First Response for Issues\",\n \"type\": \"report\"\n }\n]"
+ "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n }\n]"
}
],
"category": "Modules",
@@ -43,7 +43,7 @@
"idx": 0,
"is_standard": 1,
"label": "Support",
- "modified": "2020-06-04 11:54:56.124219",
+ "modified": "2020-08-11 15:49:34.307341",
"modified_by": "Administrator",
"module": "Support",
"name": "Support",
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index 858564a..fe01d4b 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -2,10 +2,14 @@
onload: function(frm) {
frm.email_field = "raised_by";
- frappe.db.get_value("Support Settings", {name: "Support Settings"}, "allow_resetting_service_level_agreement", (r) => {
- if (!r.allow_resetting_service_level_agreement) {
- frm.set_df_property("reset_service_level_agreement", "hidden", 1) ;
- }
+ frappe.db.get_value("Support Settings", {name: "Support Settings"},
+ ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
+ if (r && r.track_service_level_agreement == "0") {
+ frm.set_df_property("service_level_section", "hidden", 1);
+ }
+ if (r && r.allow_resetting_service_level_agreement == "0") {
+ frm.set_df_property("reset_service_level_agreement", "hidden", 1);
+ }
});
if (frm.doc.service_level_agreement) {
@@ -38,7 +42,7 @@
},
refresh: function (frm) {
- if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") {
+ if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") {
if (frm.doc.service_level_agreement) {
frappe.call({
'method': 'frappe.client.get',
@@ -85,14 +89,14 @@
if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();
- let agreement_fulfilled = (frm.doc.agreement_fulfilled == "Fulfilled") ?
+ let agreement_status = (frm.doc.agreement_status == "Fulfilled") ?
{"indicator": "green", "msg": "Service Level Agreement has been fulfilled"} :
{"indicator": "red", "msg": "Service Level Agreement Failed"};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
- '<span class="indicator whitespace-nowrap '+ agreement_fulfilled.indicator +'"><span class="hidden-xs">'+ agreement_fulfilled.msg +'</span></span> ' +
+ '<span class="indicator whitespace-nowrap '+ agreement_status.indicator +'"><span class="hidden-xs">'+ agreement_status.msg +'</span></span> ' +
'</div>' +
'</div>'
);
@@ -198,13 +202,13 @@
frm.dashboard.clear_headline();
var time_to_respond = get_status(frm.doc.response_by_variance);
- if (!frm.doc.first_responded_on && frm.doc.agreement_fulfilled === "Ongoing") {
- time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_fulfilled);
+ if (!frm.doc.first_responded_on && frm.doc.agreement_status === "Ongoing") {
+ time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
}
var time_to_resolve = get_status(frm.doc.resolution_by_variance);
- if (!frm.doc.resolution_date && frm.doc.agreement_fulfilled === "Ongoing") {
- time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_fulfilled);
+ if (!frm.doc.resolution_date && frm.doc.agreement_status === "Ongoing") {
+ time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
}
frm.dashboard.set_headline_alert(
@@ -219,10 +223,10 @@
);
}
-function get_time_left(timestamp, agreement_fulfilled) {
+function get_time_left(timestamp, agreement_status) {
const diff = moment(timestamp).diff(moment());
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed";
- let indicator = (diff_display == 'Failed' && agreement_fulfilled != "Fulfilled") ? "red" : "green";
+ let indicator = (diff_display == 'Failed' && agreement_status != "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 6525ab2..a43381c 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -27,17 +27,25 @@
"response_by_variance",
"reset_service_level_agreement",
"cb",
- "agreement_fulfilled",
+ "agreement_status",
"resolution_by",
"resolution_by_variance",
"service_level_agreement_creation",
"on_hold_since",
"total_hold_time",
"response",
- "mins_to_first_response",
+ "first_response_time",
"first_responded_on",
"column_break_26",
"avg_response_time",
+ "section_break_19",
+ "resolution_details",
+ "column_break1",
+ "opening_date",
+ "opening_time",
+ "resolution_date",
+ "resolution_time",
+ "user_resolution_time",
"additional_info",
"lead",
"contact",
@@ -46,23 +54,14 @@
"customer_name",
"project",
"company",
- "section_break_19",
- "resolution_details",
- "column_break1",
- "opening_date",
- "opening_time",
- "resolution_date",
- "content_type",
- "attachment",
"via_customer_portal",
- "resolution_time",
- "user_resolution_time"
+ "attachment",
+ "content_type"
],
"fields": [
{
"fieldname": "subject_section",
"fieldtype": "Section Break",
- "label": "Subject",
"options": "fa fa-flag"
},
{
@@ -158,7 +157,7 @@
"collapsible": 1,
"fieldname": "service_level_section",
"fieldtype": "Section Break",
- "label": "Service Level"
+ "label": "Service Level Agreement Details"
},
{
"fieldname": "service_level_agreement",
@@ -191,14 +190,7 @@
"collapsible": 1,
"fieldname": "response",
"fieldtype": "Section Break",
- "label": "Response"
- },
- {
- "bold": 1,
- "fieldname": "mins_to_first_response",
- "fieldtype": "Float",
- "label": "Mins to First Response",
- "read_only": 1
+ "label": "Response Details"
},
{
"fieldname": "first_responded_on",
@@ -261,7 +253,7 @@
"collapsible": 1,
"fieldname": "section_break_19",
"fieldtype": "Section Break",
- "label": "Resolution"
+ "label": "Resolution Details"
},
{
"depends_on": "eval:!doc.__islocal",
@@ -327,27 +319,18 @@
"label": "Via Customer Portal"
},
{
- "default": "Ongoing",
- "depends_on": "eval: doc.service_level_agreement",
- "fieldname": "agreement_fulfilled",
- "fieldtype": "Select",
- "label": "Service Level Agreement Fulfilled",
- "options": "Ongoing\nFulfilled\nFailed",
- "read_only": 1
- },
- {
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
- "description": "in hours",
"fieldname": "response_by_variance",
- "fieldtype": "Float",
+ "fieldtype": "Duration",
+ "hide_seconds": 1,
"label": "Response By Variance",
"read_only": 1
},
{
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
- "description": "in hours",
"fieldname": "resolution_by_variance",
- "fieldtype": "Float",
+ "fieldtype": "Duration",
+ "hide_seconds": 1,
"label": "Resolution By Variance",
"read_only": 1
},
@@ -406,12 +389,28 @@
"fieldtype": "Duration",
"label": "Total Hold Time",
"read_only": 1
+ },
+ {
+ "default": "Ongoing",
+ "depends_on": "eval: doc.service_level_agreement",
+ "fieldname": "agreement_status",
+ "fieldtype": "Select",
+ "label": "Service Level Agreement Status",
+ "options": "Ongoing\nFulfilled\nFailed",
+ "read_only": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "first_response_time",
+ "fieldtype": "Duration",
+ "label": "First Response Time",
+ "read_only": 1
}
],
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
- "modified": "2020-06-10 12:47:37.146914",
+ "modified": "2020-08-11 18:49:07.574769",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 87168e1..920c13c 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -61,7 +61,7 @@
if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
self.resolution_date = frappe.flags.current_time or now_datetime()
- if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing":
+ if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing":
set_service_level_agreement_variance(issue=self.name)
self.update_agreement_status()
set_resolution_time(issue=self)
@@ -72,7 +72,7 @@
self.resolution_date = None
self.reset_issue_metrics()
# enable SLA and variance on Reopen
- self.agreement_fulfilled = "Ongoing"
+ self.agreement_status = "Ongoing"
set_service_level_agreement_variance(issue=self.name)
self.handle_hold_time(status)
@@ -113,39 +113,39 @@
if not self.first_responded_on:
response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
response_by = add_to_date(response_by, seconds=round(last_hold_time))
- response_by_variance = round(time_diff_in_hours(response_by, now_time))
+ response_by_variance = round(time_diff_in_seconds(response_by, now_time))
update_values['response_by'] = response_by
- update_values['response_by_variance'] = response_by_variance + (last_hold_time // 3600)
+ update_values['response_by_variance'] = response_by_variance + last_hold_time
resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time))
- resolution_by_variance = round(time_diff_in_hours(resolution_by, now_time))
+ resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
update_values['resolution_by'] = resolution_by
- update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600)
+ update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
update_values['on_hold_since'] = None
self.db_set(update_values)
def update_agreement_status(self):
- if self.service_level_agreement and self.agreement_fulfilled == "Ongoing":
+ if self.service_level_agreement and self.agreement_status == "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"
+ self.agreement_status = "Failed"
else:
- self.agreement_fulfilled = "Fulfilled"
+ self.agreement_status = "Fulfilled"
- def update_agreement_fulfilled_on_custom_status(self):
+ def update_agreement_status_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)
+ self.response_by_variance = round(time_diff_in_seconds(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.resolution_by_variance = round(time_diff_in_seconds(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.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
def create_communication(self):
communication = frappe.new_doc("Communication")
@@ -172,7 +172,7 @@
replicated_issue = deepcopy(self)
replicated_issue.subject = subject
replicated_issue.issue_split_from = self.name
- replicated_issue.mins_to_first_response = 0
+ replicated_issue.first_response_time = 0
replicated_issue.first_responded_on = None
replicated_issue.creation = now_datetime()
@@ -180,7 +180,7 @@
if replicated_issue.service_level_agreement:
replicated_issue.service_level_agreement_creation = now_datetime()
replicated_issue.service_level_agreement = None
- replicated_issue.agreement_fulfilled = "Ongoing"
+ replicated_issue.agreement_status = "Ongoing"
replicated_issue.response_by = None
replicated_issue.response_by_variance = None
replicated_issue.resolution_by = None
@@ -241,8 +241,8 @@
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_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()))
+ self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()))
+ self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()))
def change_service_level_agreement_and_priority(self):
if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \
@@ -271,7 +271,7 @@
self.service_level_agreement_creation = now_datetime()
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
- self.agreement_fulfilled = "Ongoing"
+ self.agreement_status = "Ongoing"
self.save()
def reset_issue_metrics(self):
@@ -347,7 +347,7 @@
def set_service_level_agreement_variance(issue=None):
current_time = frappe.flags.current_time or now_datetime()
- filters = {"status": "Open", "agreement_fulfilled": "Ongoing"}
+ filters = {"status": "Open", "agreement_status": "Ongoing"}
if issue:
filters = {"name": issue}
@@ -358,13 +358,13 @@
variance = round(time_diff_in_hours(doc.response_by, current_time), 2)
frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False)
if variance < 0:
- frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
+ frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False)
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(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False)
if variance < 0:
- frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
+ frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False)
def set_resolution_time(issue):
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index fb8ceb5..c962dc6 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -73,7 +73,7 @@
issue.status = 'Closed'
issue.save()
- self.assertEqual(issue.agreement_fulfilled, 'Fulfilled')
+ self.assertEqual(issue.agreement_status, 'Fulfilled')
def test_issue_metrics(self):
creation = datetime.datetime(2020, 3, 4, 4, 0)
diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/__init__.py b/erpnext/support/report/first_response_time_for_issues/__init__.py
similarity index 100%
rename from erpnext/support/report/minutes_to_first_response_for_issues/__init__.py
rename to erpnext/support/report/first_response_time_for_issues/__init__.py
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
new file mode 100644
index 0000000..576e0b7
--- /dev/null
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
@@ -0,0 +1,44 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["First Response Time for Issues"] = {
+ "filters": [
+ {
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
+ },
+ {
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default":frappe.datetime.nowdate()
+ }
+ ],
+ get_chart_data: function(_columns, result) {
+ return {
+ data: {
+ labels: result.map(d => d[0]),
+ datasets: [{
+ name: 'First Response Time',
+ values: result.map(d => d[1])
+ }]
+ },
+ type: "line",
+ tooltipOptions: {
+ formatTooltipY: d => {
+ let duration_options = {
+ hide_days: 0,
+ hide_seconds: 0
+ };
+ value = frappe.utils.get_formatted_duration(d, duration_options);
+ return value;
+ }
+ }
+ }
+ }
+};
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json
new file mode 100644
index 0000000..c4fe6f5
--- /dev/null
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "creation": "2020-08-10 18:12:42.391224",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "Test 2",
+ "modified": "2020-08-10 18:12:42.391224",
+ "modified_by": "Administrator",
+ "module": "Support",
+ "name": "First Response Time for Issues",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;",
+ "ref_doctype": "Issue",
+ "report_name": "First Response Time for Issues",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Support Team"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
new file mode 100644
index 0000000..922da2b
--- /dev/null
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute(filters=None):
+ columns = [
+ {
+ 'fieldname': 'creation_date',
+ 'label': 'Date',
+ 'fieldtype': 'Date',
+ 'width': 300
+ },
+ {
+ 'fieldname': 'first_response_time',
+ 'fieldtype': 'Duration',
+ 'label': 'First Response Time',
+ 'width': 300
+ },
+ ]
+
+ data = frappe.db.sql('''
+ SELECT
+ date(creation) as creation_date,
+ avg(first_response_time) as avg_response_time
+ FROM tabIssue
+ WHERE
+ date(creation) between %s and %s
+ and first_response_time > 0
+ GROUP BY creation_date
+ ORDER BY creation_date desc
+ ''', (filters.from_date, filters.to_date))
+
+ return columns, data
\ No newline at end of file
diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js
deleted file mode 100644
index 034e777..0000000
--- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js
+++ /dev/null
@@ -1,30 +0,0 @@
-frappe.query_reports["Minutes to First Response for Issues"] = {
- "filters": [
- {
- "fieldname":"from_date",
- "label": __("From Date"),
- "fieldtype": "Date",
- 'reqd': 1,
- "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
- },
- {
- "fieldname":"to_date",
- "label": __("To Date"),
- "fieldtype": "Date",
- 'reqd': 1,
- "default":frappe.datetime.nowdate()
- },
- ],
- get_chart_data: function(columns, result) {
- return {
- data: {
- labels: result.map(d => d[0]),
- datasets: [{
- name: 'Mins to first response',
- values: result.map(d => d[1])
- }]
- },
- type: 'line',
- }
- }
-}
diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json
deleted file mode 100644
index 539d3d9..0000000
--- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "add_total_row": 0,
- "apply_user_permissions": 1,
- "creation": "2016-06-14 17:44:26.034112",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 2,
- "is_standard": "Yes",
- "modified": "2017-02-24 20:06:18.391100",
- "modified_by": "Administrator",
- "module": "Support",
- "name": "Minutes to First Response for Issues",
- "owner": "Administrator",
- "query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;",
- "ref_doctype": "Issue",
- "report_name": "Minutes to First Response for Issues",
- "report_type": "Script Report",
- "roles": [
- {
- "role": "Support Team"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py
deleted file mode 100644
index 57c2d44..0000000
--- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute(filters=None):
- columns = [
- {
- 'fieldname': 'creation_date',
- 'label': 'Date',
- 'fieldtype': 'Date'
- },
- {
- 'fieldname': 'mins',
- 'fieldtype': 'Float',
- 'label': 'Mins to First Response'
- },
- ]
-
- data = frappe.db.sql('''select date(creation) as creation_date,
- avg(mins_to_first_response) as mins
- from tabIssue
- where date(creation) between %s and %s
- and mins_to_first_response > 0
- group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date))
-
- return columns, data