diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 2d49915f59..967b28b8b2 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -6,7 +6,7 @@ import frappe import json, datetime from frappe import _, scrub import frappe.desk.query_report -from frappe.utils import cint +from frappe.utils import cint, cstr from frappe.model.document import Document from frappe.modules.export_file import export_to_files from frappe.modules import make_boilerplate @@ -92,6 +92,18 @@ class Report(Document): make_boilerplate("controller.py", self, {"name": self.name}) make_boilerplate("controller.js", self, {"name": self.name}) + def execute_query_report(self, filters): + if not self.query: + frappe.throw(_("Must specify a Query to run"), title=_('Report Document Error')) + + 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()] + + return [columns, result] + def execute_script_report(self, filters): # save the timestamp to automatically set to prepared threshold = 30 diff --git a/frappe/core/utils.py b/frappe/core/utils.py index 55767ffe34..55cfbc34d7 100644 --- a/frappe/core/utils.py +++ b/frappe/core/utils.py @@ -67,3 +67,19 @@ def find_all(list_of_dict, match_function): if match_function(entry): found.append(entry) return found + +def ljust_list(_list, length, fill_word=None): + """ + Similar to ljust but for list. + + Usage: + $ ljust_list([1, 2, 3], 5) + > [1, 2, 3, None, None] + """ + # make a copy to avoid mutation of passed list + _list = list(_list) + fill_length = length - len(_list) + if fill_length > 0: + _list.extend([fill_word] * fill_length) + + return _list diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index d210af02fd..aaf859e7fd 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -8,7 +8,7 @@ import os, json from frappe import _ from frappe.modules import scrub, get_module_path -from frappe.utils import flt, cint, get_html_format, cstr, get_url_to_form +from frappe.utils import flt, cint, get_html_format, get_url_to_form from frappe.model.utils import render_include from frappe.translate import send_translations import frappe.desk.reportview @@ -16,6 +16,7 @@ from frappe.permissions import get_role_permissions from six import string_types, iteritems from datetime import timedelta from frappe.utils import gzip_decompress +from frappe.core.utils import ljust_list def get_report_doc(report_name): doc = frappe.get_doc("Report", report_name) @@ -42,44 +43,32 @@ def get_report_doc(report_name): return doc -def generate_report_result(report, filters=None, user=None): - status = None - if not user: - user = frappe.session.user - if not filters: - filters = [] +def generate_report_result(report, filters=None, user=None, custom_columns=None): + user = user or frappe.session.user + filters = filters or [] if filters and isinstance(filters, string_types): filters = json.loads(filters) - columns, result, message, chart, report_summary, skip_total_row = [], [], None, None, None, 0 + + res = [] + if report.report_type == "Query Report": - if not report.query: - status = "error" - frappe.msgprint(_("Must specify a Query to run"), raise_exception=True) - - if not report.query.lower().startswith("select"): - status = "error" - frappe.msgprint(_("Query must be a SELECT"), raise_exception=True) - - result = [list(t) for t in frappe.db.sql(report.query, filters)] - columns = [cstr(c[0]) for c in frappe.db.get_description()] + res = report.execute_query_report(filters) elif report.report_type == 'Script Report': res = report.execute_script_report(filters) - columns, result = res[0], res[1] - if len(res) > 2: - message = res[2] - if len(res) > 3: - chart = res[3] - if len(res) > 4: - report_summary = res[4] - if len(res) > 5: - skip_total_row = cint(res[5]) + columns, result, message, chart, report_summary, skip_total_row = \ + ljust_list(res, 6) if report.custom_columns: columns = json.loads(report.custom_columns) result = add_data_to_custom_columns(columns, result) + if custom_columns: + result = add_data_to_custom_columns(custom_columns, result) + + for custom_column in custom_columns: + columns.insert(custom_column['insert_after_index'] + 1, custom_column) if result: result = get_filtered_data(report.ref_doctype, columns, result, user) @@ -93,8 +82,8 @@ def generate_report_result(report, filters=None, user=None): "message": message, "chart": chart, "report_summary": report_summary, - "skip_total_row": skip_total_row, - "status": status, + "skip_total_row": skip_total_row or 0, + "status": None, "execution_time": frappe.cache().hget('report_execution_time', report.name) or 0 } @@ -161,7 +150,7 @@ def get_script(report_name): @frappe.whitelist() @frappe.read_only() -def run(report_name, filters=None, user=None, ignore_prepared_report=False): +def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None): report = get_report_doc(report_name) if not user: @@ -183,7 +172,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False): dn = "" result = get_prepared_report_result(report, filters, dn, user) else: - result = generate_report_result(report, filters, user) + result = generate_report_result(report, filters, user, custom_columns) result["add_total_row"] = report.add_total_row and not result.get('skip_total_row', False) @@ -294,6 +283,8 @@ def export_query(): if isinstance(data.get("file_format_type"), string_types): file_format_type = data["file_format_type"] + custom_columns = frappe.parse_json(data["custom_columns"]) + include_indentation = data["include_indentation"] if isinstance(data.get("visible_idx"), string_types): visible_idx = json.loads(data.get("visible_idx")) @@ -301,7 +292,7 @@ def export_query(): visible_idx = None if file_format_type == "Excel": - data = run(report_name, filters) + data = run(report_name, filters, custom_columns=custom_columns) data = frappe._dict(data) if not data.columns: frappe.respond_as_web_page(_("No data to export"), diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 1d7065e70d..2276bc07d6 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -616,6 +616,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { prepare_report_data(data) { this.raw_data = data; this.columns = this.prepare_columns(data.columns); + this.custom_columns = []; this.data = this.prepare_data(data.result); this.linked_doctypes = this.get_linked_doctypes(); this.tree_report = this.data.some(d => 'indent' in d); @@ -1110,6 +1111,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { const args = { cmd: 'frappe.desk.query_report.export_query', report_name: this.report_name, + custom_columns: this.custom_columns.length? this.custom_columns: [], file_format_type: file_format, filters: filters, visible_idx, @@ -1275,16 +1277,20 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { primary_action: (values) => { const custom_columns = []; let df = frappe.meta.get_docfield(values.doctype, values.field); + const insert_after_index = this.columns + .findIndex(column => column.label === values.insert_after); custom_columns.push({ fieldname: df.fieldname, fieldtype: df.fieldtype, label: df.label, + insert_after_index: insert_after_index, link_field: this.doctype_field_map[values.doctype], doctype: values.doctype, options: df.fieldtype === "Link" ? df.options : undefined, width: 100 }); + this.custom_columns = this.custom_columns.concat(custom_columns); frappe.call({ method: 'frappe.desk.query_report.get_data_for_custom_field', args: { @@ -1294,7 +1300,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { callback: (r) => { const custom_data = r.message; const link_field = this.doctype_field_map[values.doctype]; - this.add_custom_column(custom_columns, custom_data, link_field, values.field, values.insert_after); + + this.add_custom_column(custom_columns, custom_data, link_field, values.field, insert_after_index); d.hide(); } }); @@ -1369,11 +1376,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } - add_custom_column(custom_column, custom_data, link_field, column_field, insert_after) { + add_custom_column(custom_column, custom_data, link_field, column_field, insert_after_index) { const column = this.prepare_columns(custom_column); - const insert_after_index = this.columns - .findIndex(column => column.label === insert_after); this.columns.splice(insert_after_index + 1, 0, column[0]); this.data.forEach(row => {