mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-08 15:42:52 +00:00
feat: Issue Summary Script Report (#23603)
* feat: Issue Summary Report * feat: Add Issue Metrics to Issue Summary Report * fix: code clean-up * feat: Added Report Summary * feat: Add SLA status fields * fix: add report link to desk page * fix: sider issues Co-authored-by: Marica <maricadsouza221197@gmail.com> Co-authored-by: Nabin Hait <nabinhait@gmail.com>
This commit is contained in:
@@ -28,7 +28,7 @@
|
|||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"label": "Reports",
|
"label": "Reports",
|
||||||
"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]"
|
"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 {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Summary\",\n \"name\": \"Issue Summary\",\n \"type\": \"report\"\n }\n]"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"category": "Modules",
|
"category": "Modules",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "Support",
|
"label": "Support",
|
||||||
"modified": "2020-08-11 15:49:34.307341",
|
"modified": "2020-10-12 18:40:22.252915",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Support",
|
"module": "Support",
|
||||||
"name": "Support",
|
"name": "Support",
|
||||||
|
|||||||
0
erpnext/support/report/issue_summary/__init__.py
Normal file
0
erpnext/support/report/issue_summary/__init__.py
Normal file
73
erpnext/support/report/issue_summary/issue_summary.js
Normal file
73
erpnext/support/report/issue_summary/issue_summary.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.query_reports["Issue Summary"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
fieldname: "company",
|
||||||
|
label: __("Company"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Company",
|
||||||
|
default: frappe.defaults.get_user_default("Company"),
|
||||||
|
reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "based_on",
|
||||||
|
label: __("Based On"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"],
|
||||||
|
default: "Customer",
|
||||||
|
reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "from_date",
|
||||||
|
label: __("From Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
default: frappe.defaults.get_global_default("year_start_date"),
|
||||||
|
reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname:"to_date",
|
||||||
|
label: __("To Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
default: frappe.defaults.get_global_default("year_end_date"),
|
||||||
|
reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "status",
|
||||||
|
label: __("Status"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options:[
|
||||||
|
{label: __('Open'), value: 'Open'},
|
||||||
|
{label: __('Replied'), value: 'Replied'},
|
||||||
|
{label: __('Resolved'), value: 'Resolved'},
|
||||||
|
{label: __('Closed'), value: 'Closed'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "priority",
|
||||||
|
label: __("Issue Priority"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Issue Priority"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "customer",
|
||||||
|
label: __("Customer"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Customer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "project",
|
||||||
|
label: __("Project"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "assigned_to",
|
||||||
|
label: __("Assigned To"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "User"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
26
erpnext/support/report/issue_summary/issue_summary.json
Normal file
26
erpnext/support/report/issue_summary/issue_summary.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2020-10-12 01:01:55.181777",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"modified": "2020-10-12 14:54:55.655920",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Support",
|
||||||
|
"name": "Issue Summary",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Issue",
|
||||||
|
"report_name": "Issue Summary",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Support Team"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
353
erpnext/support/report/issue_summary/issue_summary.py
Normal file
353
erpnext/support/report/issue_summary/issue_summary.py
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
import json
|
||||||
|
from six import iteritems
|
||||||
|
from frappe import _, scrub
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
return IssueSummary(filters).run()
|
||||||
|
|
||||||
|
class IssueSummary(object):
|
||||||
|
def __init__(self, filters=None):
|
||||||
|
self.filters = frappe._dict(filters or {})
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.get_columns()
|
||||||
|
self.get_data()
|
||||||
|
self.get_chart_data()
|
||||||
|
self.get_report_summary()
|
||||||
|
|
||||||
|
return self.columns, self.data, None, self.chart, self.report_summary
|
||||||
|
|
||||||
|
def get_columns(self):
|
||||||
|
self.columns = []
|
||||||
|
|
||||||
|
if self.filters.based_on == 'Customer':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Customer'),
|
||||||
|
'options': 'Customer',
|
||||||
|
'fieldname': 'customer',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.based_on == 'Assigned To':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('User'),
|
||||||
|
'fieldname': 'user',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'User',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.based_on == 'Issue Type':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Issue Type'),
|
||||||
|
'fieldname': 'issue_type',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'Issue Type',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.based_on == 'Issue Priority':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Issue Priority'),
|
||||||
|
'fieldname': 'priority',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'Issue Priority',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
self.statuses = ['Open', 'Replied', 'Resolved', 'Closed']
|
||||||
|
for status in self.statuses:
|
||||||
|
self.columns.append({
|
||||||
|
'label': _(status),
|
||||||
|
'fieldname': scrub(status),
|
||||||
|
'fieldtype': 'Int',
|
||||||
|
'width': 80
|
||||||
|
})
|
||||||
|
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Total Issues'),
|
||||||
|
'fieldname': 'total_issues',
|
||||||
|
'fieldtype': 'Int',
|
||||||
|
'width': 100
|
||||||
|
})
|
||||||
|
|
||||||
|
self.sla_status_map = {
|
||||||
|
'SLA Failed': 'failed',
|
||||||
|
'SLA Fulfilled': 'fulfilled',
|
||||||
|
'SLA Ongoing': 'ongoing'
|
||||||
|
}
|
||||||
|
|
||||||
|
for label, fieldname in self.sla_status_map.items():
|
||||||
|
self.columns.append({
|
||||||
|
'label': _(label),
|
||||||
|
'fieldname': fieldname,
|
||||||
|
'fieldtype': 'Int',
|
||||||
|
'width': 100
|
||||||
|
})
|
||||||
|
|
||||||
|
self.metrics = ['Avg First Response Time', 'Avg Response Time', 'Avg Hold Time',
|
||||||
|
'Avg Resolution Time', 'Avg User Resolution Time']
|
||||||
|
|
||||||
|
for metric in self.metrics:
|
||||||
|
self.columns.append({
|
||||||
|
'label': _(metric),
|
||||||
|
'fieldname': scrub(metric),
|
||||||
|
'fieldtype': 'Duration',
|
||||||
|
'width': 170
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
self.get_issues()
|
||||||
|
self.get_rows()
|
||||||
|
|
||||||
|
def get_issues(self):
|
||||||
|
filters = self.get_common_filters()
|
||||||
|
self.field_map = {
|
||||||
|
'Customer': 'customer',
|
||||||
|
'Issue Type': 'issue_type',
|
||||||
|
'Issue Priority': 'priority',
|
||||||
|
'Assigned To': '_assign'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.entries = frappe.db.get_all('Issue',
|
||||||
|
fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date', 'status', 'avg_response_time',
|
||||||
|
'first_response_time', 'total_hold_time', 'user_resolution_time', 'resolution_time', 'agreement_status'],
|
||||||
|
filters=filters
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_common_filters(self):
|
||||||
|
filters = {}
|
||||||
|
filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date])
|
||||||
|
|
||||||
|
if self.filters.get('assigned_to'):
|
||||||
|
filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%')
|
||||||
|
|
||||||
|
for entry in ['company', 'status', 'priority', 'customer', 'project']:
|
||||||
|
if self.filters.get(entry):
|
||||||
|
filters[entry] = self.filters.get(entry)
|
||||||
|
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def get_rows(self):
|
||||||
|
self.data = []
|
||||||
|
self.get_summary_data()
|
||||||
|
|
||||||
|
for entity, data in iteritems(self.issue_summary_data):
|
||||||
|
if self.filters.based_on == 'Customer':
|
||||||
|
row = {'customer': entity}
|
||||||
|
elif self.filters.based_on == 'Assigned To':
|
||||||
|
row = {'user': entity}
|
||||||
|
elif self.filters.based_on == 'Issue Type':
|
||||||
|
row = {'issue_type': entity}
|
||||||
|
elif self.filters.based_on == 'Issue Priority':
|
||||||
|
row = {'priority': entity}
|
||||||
|
|
||||||
|
for status in self.statuses:
|
||||||
|
count = flt(data.get(status, 0.0))
|
||||||
|
row[scrub(status)] = count
|
||||||
|
|
||||||
|
row['total_issues'] = data.get('total_issues', 0.0)
|
||||||
|
|
||||||
|
for sla_status in self.sla_status_map.values():
|
||||||
|
value = flt(data.get(sla_status), 0.0)
|
||||||
|
row[sla_status] = value
|
||||||
|
|
||||||
|
for metric in self.metrics:
|
||||||
|
value = flt(data.get(scrub(metric)), 0.0)
|
||||||
|
row[scrub(metric)] = value
|
||||||
|
|
||||||
|
self.data.append(row)
|
||||||
|
|
||||||
|
def get_summary_data(self):
|
||||||
|
self.issue_summary_data = frappe._dict()
|
||||||
|
|
||||||
|
for d in self.entries:
|
||||||
|
status = d.status
|
||||||
|
agreement_status = scrub(d.agreement_status)
|
||||||
|
|
||||||
|
if self.filters.based_on == 'Assigned To':
|
||||||
|
if d._assign:
|
||||||
|
for entry in json.loads(d._assign):
|
||||||
|
self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0)
|
||||||
|
self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0)
|
||||||
|
self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault('total_issues', 0.0)
|
||||||
|
self.issue_summary_data[entry][status] += 1
|
||||||
|
self.issue_summary_data[entry][agreement_status] += 1
|
||||||
|
self.issue_summary_data[entry]['total_issues'] += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
field = self.field_map.get(self.filters.based_on)
|
||||||
|
value = d.get(field)
|
||||||
|
if not value:
|
||||||
|
value = _('Not Specified')
|
||||||
|
|
||||||
|
self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(status, 0.0)
|
||||||
|
self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(agreement_status, 0.0)
|
||||||
|
self.issue_summary_data.setdefault(value, frappe._dict()).setdefault('total_issues', 0.0)
|
||||||
|
self.issue_summary_data[value][status] += 1
|
||||||
|
self.issue_summary_data[value][agreement_status] += 1
|
||||||
|
self.issue_summary_data[value]['total_issues'] += 1
|
||||||
|
|
||||||
|
self.get_metrics_data()
|
||||||
|
|
||||||
|
def get_metrics_data(self):
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
metrics_list = ['avg_response_time', 'avg_first_response_time', 'avg_hold_time',
|
||||||
|
'avg_resolution_time', 'avg_user_resolution_time']
|
||||||
|
|
||||||
|
for entry in self.entries:
|
||||||
|
issues.append(entry.name)
|
||||||
|
|
||||||
|
field = self.field_map.get(self.filters.based_on)
|
||||||
|
|
||||||
|
if issues:
|
||||||
|
if self.filters.based_on == 'Assigned To':
|
||||||
|
assignment_map = frappe._dict()
|
||||||
|
for d in self.entries:
|
||||||
|
if d._assign:
|
||||||
|
for entry in json.loads(d._assign):
|
||||||
|
for metric in metrics_list:
|
||||||
|
self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0)
|
||||||
|
|
||||||
|
self.issue_summary_data[entry]['avg_response_time'] += d.get('avg_response_time') or 0.0
|
||||||
|
self.issue_summary_data[entry]['avg_first_response_time'] += d.get('first_response_time') or 0.0
|
||||||
|
self.issue_summary_data[entry]['avg_hold_time'] += d.get('total_hold_time') or 0.0
|
||||||
|
self.issue_summary_data[entry]['avg_resolution_time'] += d.get('resolution_time') or 0.0
|
||||||
|
self.issue_summary_data[entry]['avg_user_resolution_time'] += d.get('user_resolution_time') or 0.0
|
||||||
|
|
||||||
|
if not assignment_map.get(entry):
|
||||||
|
assignment_map[entry] = 0
|
||||||
|
assignment_map[entry] += 1
|
||||||
|
|
||||||
|
for entry in assignment_map:
|
||||||
|
for metric in metrics_list:
|
||||||
|
self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry))
|
||||||
|
|
||||||
|
else:
|
||||||
|
data = frappe.db.sql("""
|
||||||
|
SELECT
|
||||||
|
{0}, AVG(first_response_time) as avg_frt,
|
||||||
|
AVG(avg_response_time) as avg_resp_time,
|
||||||
|
AVG(total_hold_time) as avg_hold_time,
|
||||||
|
AVG(resolution_time) as avg_resolution_time,
|
||||||
|
AVG(user_resolution_time) as avg_user_resolution_time
|
||||||
|
FROM `tabIssue`
|
||||||
|
WHERE
|
||||||
|
name IN %(issues)s
|
||||||
|
GROUP BY {0}
|
||||||
|
""".format(field), {'issues': issues}, as_dict=1)
|
||||||
|
|
||||||
|
for entry in data:
|
||||||
|
value = entry.get(field)
|
||||||
|
if not value:
|
||||||
|
value = _('Not Specified')
|
||||||
|
|
||||||
|
for metric in metrics_list:
|
||||||
|
self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(metric, 0.0)
|
||||||
|
|
||||||
|
self.issue_summary_data[value]['avg_response_time'] = entry.get('avg_resp_time') or 0.0
|
||||||
|
self.issue_summary_data[value]['avg_first_response_time'] = entry.get('avg_frt') or 0.0
|
||||||
|
self.issue_summary_data[value]['avg_hold_time'] = entry.get('avg_hold_time') or 0.0
|
||||||
|
self.issue_summary_data[value]['avg_resolution_time'] = entry.get('avg_resolution_time') or 0.0
|
||||||
|
self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0
|
||||||
|
|
||||||
|
def get_chart_data(self):
|
||||||
|
if not self.data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
open_issues = []
|
||||||
|
replied_issues = []
|
||||||
|
resolved_issues = []
|
||||||
|
closed_issues = []
|
||||||
|
|
||||||
|
entity = self.filters.based_on
|
||||||
|
entity_field = self.field_map.get(entity)
|
||||||
|
if entity == 'Assigned To':
|
||||||
|
entity_field = 'user'
|
||||||
|
|
||||||
|
for entry in self.data:
|
||||||
|
labels.append(entry.get(entity_field))
|
||||||
|
open_issues.append(entry.get('open'))
|
||||||
|
replied_issues.append(entry.get('replied'))
|
||||||
|
resolved_issues.append(entry.get('resolved'))
|
||||||
|
closed_issues.append(entry.get('closed'))
|
||||||
|
|
||||||
|
self.chart = {
|
||||||
|
'data': {
|
||||||
|
'labels': labels[:30],
|
||||||
|
'datasets': [
|
||||||
|
{
|
||||||
|
'name': 'Open',
|
||||||
|
'values': open_issues[:30]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Replied',
|
||||||
|
'values': replied_issues[:30]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Resolved',
|
||||||
|
'values': resolved_issues[:30]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Closed',
|
||||||
|
'values': closed_issues[:30]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'type': 'bar',
|
||||||
|
'barOptions': {
|
||||||
|
'stacked': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_report_summary(self):
|
||||||
|
if not self.data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
open_issues = 0
|
||||||
|
replied = 0
|
||||||
|
resolved = 0
|
||||||
|
closed = 0
|
||||||
|
|
||||||
|
for entry in self.data:
|
||||||
|
open_issues += entry.get('open')
|
||||||
|
replied += entry.get('replied')
|
||||||
|
resolved += entry.get('resolved')
|
||||||
|
closed += entry.get('closed')
|
||||||
|
|
||||||
|
self.report_summary = [
|
||||||
|
{
|
||||||
|
'value': open_issues,
|
||||||
|
'indicator': 'Red',
|
||||||
|
'label': _('Open'),
|
||||||
|
'datatype': 'Int',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'value': replied,
|
||||||
|
'indicator': 'Grey',
|
||||||
|
'label': _('Replied'),
|
||||||
|
'datatype': 'Int',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'value': resolved,
|
||||||
|
'indicator': 'Green',
|
||||||
|
'label': _('Resolved'),
|
||||||
|
'datatype': 'Int',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'value': closed,
|
||||||
|
'indicator': 'Green',
|
||||||
|
'label': _('Closed'),
|
||||||
|
'datatype': 'Int',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
Reference in New Issue
Block a user