diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index b890f743cc..6cdf2e40ac 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -609,7 +609,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2020-08-06 12:59:32.369093",
+ "modified": "2020-08-06 12:59:32.369095",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
diff --git a/frappe/core/doctype/report/report.js b/frappe/core/doctype/report/report.js
index c410e9aa1a..14e9b3a901 100644
--- a/frappe/core/doctype/report/report.js
+++ b/frappe/core/doctype/report/report.js
@@ -42,26 +42,6 @@ frappe.ui.form.on('Report', {
}
},
- report_type: function(frm) {
- frm.set_intro("");
- switch(frm.doc.report_type) {
- case "Report Builder":
- frm.set_intro(__("Report Builder reports are managed directly by the report builder. Nothing to do."));
- break;
- case "Query Report":
- frm.set_intro(__("Write a SELECT query. Note result is not paged (all data is sent in one go).")
- + __("To format columns, give column labels in the query.") + "
"
- + __("[Label]:[Field Type]/[Options]:[Width]") + "
"
- + __("Example:") + "
"
- + "Employee:Link/Employee:200" + "
"
- + "Rate:Currency:120" + "
")
- break;
- case "Script Report":
- frm.set_intro(__("Write a Python file in the same folder where this is saved and return column and result."));
- break;
- }
- },
-
set_doctype_roles: function(frm) {
return frm.call('set_doctype_roles').then(() => {
frm.refresh_field('roles');
diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json
index 40d2417a56..5b3593e658 100644
--- a/frappe/core/doctype/report/report.json
+++ b/frappe/core/doctype/report/report.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "field:report_name",
"creation": "2013-03-09 15:45:57",
"doctype": "DocType",
@@ -17,10 +18,15 @@
"disabled",
"disable_prepared_report",
"prepared_report",
+ "filters_section",
+ "filters",
+ "columns_section",
+ "columns",
"section_break_6",
"query",
- "javascript",
"report_script",
+ "client_code_section",
+ "javascript",
"json",
"permission_rules",
"roles"
@@ -94,7 +100,8 @@
},
{
"fieldname": "section_break_6",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Query / Script"
},
{
"depends_on": "eval:doc.report_type==\"Query Report\"",
@@ -142,15 +149,50 @@
"read_only": 1
},
{
- "depends_on": "eval:doc.report_type==\"Script Report\" && doc.is_standard===\"No\"",
- "description": "output in the form of `data = [columns, result]`",
+ "depends_on": "eval:(doc.report_type===\"Script Report\" \n|| doc.report_type==\"Query Report\") \n&& doc.is_standard===\"No\"",
+ "description": "Filters will be accessible via filters.
Send output as result = [result], or for old style data = [columns], [result]",
"fieldname": "report_script",
"fieldtype": "Code",
"label": "Script"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "filters",
+ "fieldname": "filters_section",
+ "fieldtype": "Section Break",
+ "label": "Filters"
+ },
+ {
+ "fieldname": "filters",
+ "fieldtype": "Table",
+ "label": "Filters",
+ "options": "Report Filter"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "columns",
+ "fieldname": "columns_section",
+ "fieldtype": "Section Break",
+ "label": "Columns"
+ },
+ {
+ "fieldname": "columns",
+ "fieldtype": "Table",
+ "label": "Columns",
+ "options": "Report Column"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "javascript",
+ "fieldname": "client_code_section",
+ "fieldtype": "Section Break",
+ "label": "Client Code"
}
],
"idx": 1,
- "modified": "2019-10-09 15:43:08.577610",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-08-17 16:49:28.474274",
"modified_by": "Administrator",
"module": "Core",
"name": "Report",
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index dd695d484e..8b7a03aa28 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -51,6 +51,9 @@ class Report(Document):
def on_trash(self):
delete_custom_role('report', self.name)
+ def get_columns(self):
+ return [d.as_dict(no_default_fields = True) for d in self.columns]
+
def set_doctype_roles(self):
if not self.get('roles') and self.is_standard == 'No':
meta = frappe.get_meta(self.ref_doctype)
@@ -99,8 +102,8 @@ class Report(Document):
if not self.query.lower().startswith("select"):
frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
- result = [list(t) for t in frappe.db.sql(self.query, filters)]
- columns = [cstr(c[0]) for c in frappe.db.get_description()]
+ result = [list(t) for t in frappe.db.sql(self.query, filters, debug=True)]
+ columns = self.get_columns() or [cstr(c[0]) for c in frappe.db.get_description()]
return [columns, result]
@@ -134,135 +137,167 @@ class Report(Document):
def execute_script(self, filters):
# server script
- loc = {"filters": frappe._dict(filters), 'data':[]}
+ loc = {"filters": frappe._dict(filters), 'data':None, 'result':None}
safe_exec(self.report_script, None, loc)
- return loc['data']
+ if loc['data']:
+ return loc['data']
+ else:
+ return self.get_columns(), loc['result']
def get_data(self, filters=None, limit=None, user=None, as_dict=False, ignore_prepared_report=False):
- columns = []
- out = []
-
if self.report_type in ('Query Report', 'Script Report', 'Custom Report'):
- # query and script reports
- data = frappe.desk.query_report.run(self.name,
- filters=filters, user=user, ignore_prepared_report=ignore_prepared_report)
-
- for d in data.get('columns'):
- if isinstance(d, dict):
- col = frappe._dict(d)
- if not col.fieldname:
- col.fieldname = col.label
- columns.append(col)
- else:
- fieldtype, options = "Data", None
- parts = d.split(':')
- if len(parts) > 1:
- if parts[1]:
- fieldtype, options = parts[1], None
- if fieldtype and '/' in fieldtype:
- fieldtype, options = fieldtype.split('/')
-
- columns.append(frappe._dict(label=parts[0], fieldtype=fieldtype, fieldname=parts[0], options=options))
-
- out += data.get('result')
+ columns, result = self.run_query_report(filters, user, ignore_prepared_report)
else:
- # standard report
- params = json.loads(self.json)
-
- if params.get('fields'):
- columns = params.get('fields')
- elif params.get('columns'):
- columns = params.get('columns')
- else:
- columns = [['name', self.ref_doctype]]
- for df in frappe.get_meta(self.ref_doctype).fields:
- if df.in_list_view:
- columns.append([df.fieldname, self.ref_doctype])
-
- _filters = params.get('filters') or []
-
- if filters:
- for key, value in iteritems(filters):
- condition, _value = '=', value
- if isinstance(value, (list, tuple)):
- condition, _value = value
- _filters.append([key, condition, _value])
-
- def _format(parts):
- # sort by is saved as DocType.fieldname, covert it to sql
- return '`tab{0}`.`{1}`'.format(*parts)
-
- if params.get('sort_by'):
- order_by = _format(params.get('sort_by').split('.')) + ' ' + params.get('sort_order')
- elif params.get('order_by'):
- order_by = params.get('order_by')
- else:
- order_by = _format([self.ref_doctype, 'modified']) + ' desc'
-
- if params.get('sort_by_next'):
- order_by += ', ' + _format(params.get('sort_by_next').split('.')) + ' ' + params.get('sort_order_next')
-
- group_by = None
- if params.get('group_by'):
- group_by_args = frappe._dict(params['group_by'])
- group_by = group_by_args['group_by']
- order_by = '_aggregate_column desc'
-
- result = frappe.get_list(self.ref_doctype,
- fields = [
- get_group_by_field(group_by_args, c[1]) if c[0] == '_aggregate_column' and group_by_args
- else _format([c[1], c[0]])
- for c in columns
- ],
- filters=_filters,
- order_by = order_by,
- as_list=True,
- limit=limit,
- group_by=group_by,
- user=user)
-
- _columns = []
-
- for (fieldname, doctype) in columns:
- meta = frappe.get_meta(doctype)
-
- if meta.get_field(fieldname):
- field = meta.get_field(fieldname)
- else:
- if fieldname == '_aggregate_column':
- label = get_group_by_column_label(group_by_args, meta)
- else:
- label = meta.get_label(fieldname)
-
- field = frappe._dict(fieldname=fieldname, label=label)
- # since name is the primary key for a document, it will always be a Link datatype
- if fieldname == "name":
- field.fieldtype = "Link"
- field.options = doctype
-
- _columns.append(field)
- columns = _columns
-
- out = out + [list(d) for d in result]
-
- if params.get('add_totals_row'):
- out = append_totals_row(out)
+ columns, result = self.run_standard_report(filters, limit, user)
if as_dict:
- data = []
- for row in out:
- if isinstance(row, (list, tuple)):
- _row = frappe._dict()
- for i, val in enumerate(row):
- _row[columns[i].get('fieldname')] = val
- elif isinstance(row, dict):
- # no need to convert from dict to dict
- _row = frappe._dict(row)
- data.append(_row)
- else:
- data = out
- return columns, data
+ result = self.build_data_dict(result, columns)
+ return columns, result
+
+ def run_query_report(self, filters, user, ignore_prepared_report=False):
+ columns, result = [], []
+ data = frappe.desk.query_report.run(self.name,
+ filters=filters, user=user, ignore_prepared_report=ignore_prepared_report)
+
+ for d in data.get('columns'):
+ if isinstance(d, dict):
+ col = frappe._dict(d)
+ if not col.fieldname:
+ col.fieldname = col.label
+ columns.append(col)
+ else:
+ fieldtype, options = "Data", None
+ parts = d.split(':')
+ if len(parts) > 1:
+ if parts[1]:
+ fieldtype, options = parts[1], None
+ if fieldtype and '/' in fieldtype:
+ fieldtype, options = fieldtype.split('/')
+
+ columns.append(frappe._dict(label=parts[0], fieldtype=fieldtype, fieldname=parts[0], options=options))
+
+ result += data.get('result')
+
+ return columns, result
+
+ def run_standard_report(self, filters, limit, user):
+ params = json.loads(self.json)
+ columns = self.get_standard_report_columns(params)
+ result = []
+ order_by, group_by, group_by_args = self.get_standard_report_order_by(params)
+
+ _result = frappe.get_list(self.ref_doctype,
+ fields = [
+ get_group_by_field(group_by_args, c[1]) if c[0] == '_aggregate_column' and group_by_args
+ else Report._format([c[1], c[0]]) for c in columns
+ ],
+ filters = self.get_standard_report_filters(params, filters),
+ order_by = order_by,
+ group_by = group_by,
+ as_list = True,
+ limit = limit,
+ user = user)
+
+ columns = self.build_standard_report_columns(columns, group_by_args)
+
+ result = result + [list(d) for d in _result]
+
+ if params.get('add_totals_row'):
+ result = append_totals_row(result)
+
+ return columns, result
+
+ @staticmethod
+ def _format(parts):
+ # sort by is saved as DocType.fieldname, covert it to sql
+ return '`tab{0}`.`{1}`'.format(*parts)
+
+ def get_standard_report_columns(self, params):
+ if params.get('fields'):
+ columns = params.get('fields')
+ elif params.get('columns'):
+ columns = params.get('columns')
+ elif params.get('fields'):
+ columns = params.get('fields')
+ else:
+ columns = [['name', self.ref_doctype]]
+ for df in frappe.get_meta(self.ref_doctype).fields:
+ if df.in_list_view:
+ columns.append([df.fieldname, self.ref_doctype])
+
+ return columns
+
+ def get_standard_report_filters(self, params, filters):
+ _filters = params.get('filters') or []
+
+ if filters:
+ for key, value in iteritems(filters):
+ condition, _value = '=', value
+ if isinstance(value, (list, tuple)):
+ condition, _value = value
+ _filters.append([key, condition, _value])
+
+ return _filters
+
+ def get_standard_report_order_by(self, params):
+ group_by_args = None
+ if params.get('sort_by'):
+ order_by = Report._format(params.get('sort_by').split('.')) + ' ' + params.get('sort_order')
+
+ elif params.get('order_by'):
+ order_by = params.get('order_by')
+ else:
+ order_by = Report._format([self.ref_doctype, 'modified']) + ' desc'
+
+ if params.get('sort_by_next'):
+ order_by += ', ' + Report._format(params.get('sort_by_next').split('.')) + ' ' + params.get('sort_order_next')
+
+ group_by = None
+ if params.get('group_by'):
+ group_by_args = frappe._dict(params['group_by'])
+ group_by = group_by_args['group_by']
+ order_by = '_aggregate_column desc'
+
+ return order_by, group_by, group_by_args
+
+ def build_standard_report_columns(self, columns, group_by_args):
+ _columns = []
+
+ for (fieldname, doctype) in columns:
+ meta = frappe.get_meta(doctype)
+
+ if meta.get_field(fieldname):
+ field = meta.get_field(fieldname)
+ else:
+ if fieldname == '_aggregate_column':
+ label = get_group_by_column_label(group_by_args, meta)
+ else:
+ label = meta.get_label(fieldname)
+
+ field = frappe._dict(fieldname=fieldname, label=label)
+
+ # since name is the primary key for a document, it will always be a Link datatype
+ if fieldname == "name":
+ field.fieldtype = "Link"
+ field.options = doctype
+
+ _columns.append(field)
+ return _columns
+
+ def build_data_dict(self, result, columns):
+ data = []
+ for row in result:
+ if isinstance(row, (list, tuple)):
+ _row = frappe._dict()
+ for i, val in enumerate(row):
+ _row[columns[i].get('fieldname')] = val
+ elif isinstance(row, dict):
+ # no need to convert from dict to dict
+ _row = frappe._dict(row)
+ data.append(_row)
+
+ return data
@Document.whitelist
def toggle_disable(self, disable):
diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py
index b8b18205b4..805b903300 100644
--- a/frappe/core/doctype/report/test_report.py
+++ b/frappe/core/doctype/report/test_report.py
@@ -111,3 +111,41 @@ data = [
# check values
self.assertTrue('System User' in [d.get('type') for d in data[1]])
+ def test_script_report_with_columns(self):
+ report_name = 'Test Script Report With Columns'
+
+ if frappe.db.exists("Report", report_name):
+ frappe.delete_doc('Report', report_name)
+
+ report = frappe.get_doc({
+ 'doctype': 'Report',
+ 'ref_doctype': 'User',
+ 'report_name': report_name,
+ 'report_type': 'Script Report',
+ 'is_standard': 'No',
+ 'columns': [
+ dict(fieldname='type', label='Type', fieldtype='Data'),
+ dict(fieldname='value', label='Value', fieldtype='Int'),
+ ]
+ }).insert(ignore_permissions=True)
+
+ report.report_script = '''
+totals = {}
+for user in frappe.get_all('User', fields = ['name', 'user_type', 'creation']):
+ if not user.user_type in totals:
+ totals[user.user_type] = 0
+ totals[user.user_type] = totals[user.user_type] + 1
+
+result = [
+ {"type":key, "value": value} for key, value in totals.items()
+ ]
+'''
+
+ report.save()
+ data = report.get_data()
+
+ # check columns
+ self.assertEqual(data[0][0]['label'], 'Type')
+
+ # check values
+ self.assertTrue('System User' in [d.get('type') for d in data[1]])
diff --git a/frappe/core/doctype/report_column/__init__.py b/frappe/core/doctype/report_column/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/report_column/report_column.json b/frappe/core/doctype/report_column/report_column.json
new file mode 100644
index 0000000000..53b5dff9b6
--- /dev/null
+++ b/frappe/core/doctype/report_column/report_column.json
@@ -0,0 +1,61 @@
+{
+ "actions": [],
+ "creation": "2020-01-14 11:28:37.583656",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "fieldname",
+ "label",
+ "fieldtype",
+ "options",
+ "width"
+ ],
+ "fields": [
+ {
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Fieldname",
+ "reqd": 1
+ },
+ {
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "reqd": 1
+ },
+ {
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Fieldtype",
+ "options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
+ "reqd": 1
+ },
+ {
+ "fieldname": "options",
+ "fieldtype": "Data",
+ "label": "Options"
+ },
+ {
+ "fieldname": "width",
+ "fieldtype": "Int",
+ "label": "Width"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-08-17 14:32:17.174796",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Report Column",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/report_column/report_column.py b/frappe/core/doctype/report_column/report_column.py
new file mode 100644
index 0000000000..69c88b7bda
--- /dev/null
+++ b/frappe/core/doctype/report_column/report_column.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ReportColumn(Document):
+ pass
diff --git a/frappe/core/doctype/report_filter/__init__.py b/frappe/core/doctype/report_filter/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/report_filter/report_filter.json b/frappe/core/doctype/report_filter/report_filter.json
new file mode 100644
index 0000000000..9d277db11d
--- /dev/null
+++ b/frappe/core/doctype/report_filter/report_filter.json
@@ -0,0 +1,71 @@
+{
+ "actions": [],
+ "creation": "2020-01-14 11:38:58.016498",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "fieldname",
+ "label",
+ "fieldtype",
+ "mandatory",
+ "options",
+ "wildcard_filter"
+ ],
+ "fields": [
+ {
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Fieldname",
+ "reqd": 1
+ },
+ {
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "reqd": 1
+ },
+ {
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Fieldtype",
+ "options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "mandatory",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Mandatory"
+ },
+ {
+ "fieldname": "options",
+ "fieldtype": "Data",
+ "label": "Options"
+ },
+ {
+ "default": "0",
+ "description": "Will add \"%\" before and after the query",
+ "fieldname": "wildcard_filter",
+ "fieldtype": "Check",
+ "label": "Wildcard Filter"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-08-17 16:15:46.937267",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Report Filter",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/report_filter/report_filter.py b/frappe/core/doctype/report_filter/report_filter.py
new file mode 100644
index 0000000000..d85a1a5a65
--- /dev/null
+++ b/frappe/core/doctype/report_filter/report_filter.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ReportFilter(Document):
+ pass
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index e0b9019ca3..706cde13b7 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -366,6 +366,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.report_settings.html_format = settings.html_format;
this.report_settings.execution_time = settings.execution_time || 0;
frappe.query_reports[this.report_name] = this.report_settings;
+
+ if (this.report_doc.filters && !this.report_settings.filters) {
+ // add configured filters
+ this.report_settings.filters = this.report_doc.filters;
+ }
+
resolve();
});
}).catch(reject);
@@ -1109,8 +1115,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
.map(f => {
var v = f.get_value();
// hidden fields dont have $input
- if(f.df.hidden) v = f.value;
- if(v === '%') v = null;
+ if (f.df.hidden) v = f.value;
+ if (v === '%') v = null;
+ if (f.df.wildcard_filter) {
+ v = `%${v}%`;
+ }
return {
[f.df.fieldname]: v
};