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 };