diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 7ca483d806..161e45c75e 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -318,6 +318,7 @@ def export_query(): file_format_type = form_params.file_format_type custom_columns = frappe.parse_json(form_params.custom_columns or "[]") include_indentation = form_params.include_indentation + include_filters = form_params.include_filters visible_idx = form_params.visible_idx if isinstance(visible_idx, str): @@ -327,6 +328,8 @@ def export_query(): report_name, form_params.filters, custom_columns=custom_columns, are_default_filters=False ) data = frappe._dict(data) + data.filters = form_params.applied_filters + if not data.columns: frappe.respond_as_web_page( _("No data to export"), @@ -335,7 +338,9 @@ def export_query(): return format_duration_fields(data) - xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation) + xlsx_data, column_widths = build_xlsx_data( + data, visible_idx, include_indentation, include_filters=include_filters + ) if file_format_type == "CSV": content = get_csv_bytes(xlsx_data, csv_params) @@ -360,7 +365,9 @@ def format_duration_fields(data: frappe._dict) -> None: row[index] = format_duration(row[index]) -def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=False): +def build_xlsx_data( + data, visible_idx, include_indentation, include_filters=False, ignore_visible_idx=False +): EXCEL_TYPES = ( str, bool, @@ -380,17 +387,34 @@ def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=F # Note: converted for faster lookups visible_idx = set(visible_idx) - result = [[]] + result = [] column_widths = [] + if cint(include_filters): + filter_data = [] + filters = data.filters + for filter_name, filter_value in filters.items(): + if not filter_value: + continue + filter_value = ( + ", ".join([cstr(x) for x in filter_value]) + if isinstance(filter_value, list) + else cstr(filter_value) + ) + filter_data.append([cstr(filter_name), filter_value]) + filter_data.append([]) + result += filter_data + + column_data = [] for column in data.columns: if column.get("hidden"): continue - result[0].append(_(column.get("label"))) + column_data.append(_(column.get("label"))) column_width = cint(column.get("width", 0)) # to convert into scale accepted by openpyxl column_width /= 10 column_widths.append(column_width) + result.append(column_data) # build table from result for row_idx, row in enumerate(data.result): diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index b8c84da573..746c6b299f 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -217,6 +217,8 @@ def clean_params(data): def parse_json(data): if (filters := data.get("filters")) and isinstance(filters, str): data["filters"] = json.loads(filters) + if (applied_filters := data.get("applied_filters")) and isinstance(applied_filters, str): + data["applied_filters"] = json.loads(applied_filters) if (or_filters := data.get("or_filters")) and isinstance(or_filters, str): data["or_filters"] = json.loads(or_filters) if (fields := data.get("fields")) and isinstance(fields, str): diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 37c1cd6ea8..3bda8340b8 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1473,21 +1473,34 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return; } - let extra_fields = null; + let extra_fields = []; + if (this.tree_report) { - extra_fields = [ - { - label: __("Include indentation"), - fieldname: "include_indentation", - fieldtype: "Check", - }, - ]; + extra_fields.push({ + label: __("Include indentation"), + fieldname: "include_indentation", + fieldtype: "Check", + }); + } + + if (this.filters.length > 0) { + extra_fields.push({ + label: __("Include filters"), + fieldname: "include_filters", + fieldtype: "Check", + }); } this.export_dialog = frappe.report_utils.get_export_dialog( __(this.report_name), extra_fields, - ({ file_format, include_indentation, csv_delimiter, csv_quoting }) => { + ({ + file_format, + include_indentation, + include_filters, + csv_delimiter, + csv_quoting, + }) => { this.make_access_log("Export", file_format); let filters = this.get_filter_values(true); @@ -1497,6 +1510,15 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { filters ); } + let boolean_labels = { 1: __("Yes"), 0: __("No") }; + let applied_filters = Object.fromEntries( + Object.entries(filters).map(([key, value]) => [ + frappe.query_report.get_filter(key).df.label, + frappe.query_report.get_filter(key).df.fieldtype == "Check" + ? boolean_labels[value] + : value, + ]) + ); const visible_idx = this.datatable.bodyRenderer.visibleRowIndices; if (visible_idx.length + 1 === this.data.length) { @@ -1509,10 +1531,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { custom_columns: this.custom_columns.length ? this.custom_columns : [], file_format_type: file_format, filters: filters, + applied_filters: applied_filters, visible_idx, csv_delimiter, csv_quoting, include_indentation, + include_filters, }; open_url_post(frappe.request.url, args); diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 0a22c0f1f2..8d47a846bb 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -22,18 +22,7 @@ class TestQueryReport(FrappeTestCase): """Test exporting report using rows with multiple datatypes (list, dict)""" # Create mock data - data = frappe._dict() - data.columns = [ - {"label": "Column A", "fieldname": "column_a", "fieldtype": "Float"}, - {"label": "Column B", "fieldname": "column_b", "width": 100, "fieldtype": "Float"}, - {"label": "Column C", "fieldname": "column_c", "width": 150, "fieldtype": "Duration"}, - ] - data.result = [ - [1.0, 3.0, 600], - {"column_a": 22.1, "column_b": 21.8, "column_c": 86412}, - {"column_b": 5.1, "column_c": 53234, "column_a": 11.1}, - [3.0, 1.5, 333], - ] + data = create_mock_data() # Define the visible rows visible_idx = [0, 2, 3] @@ -54,6 +43,27 @@ class TestQueryReport(FrappeTestCase): for cell in row: self.assertIsInstance(cell, (int, float)) + def test_xlsx_data_with_filters(self): + """Test building xlsx data along with filters""" + + # Create mock data + data = create_mock_data() + data.filters = {"Label 1": "Filter Value", "Label 2": None, "Label 3": list(range(5))} + + # Define the visible rows + visible_idx = [0, 2, 3] + + # Build the result + xlsx_data, column_widths = build_xlsx_data( + data, visible_idx, include_indentation=False, include_filters=True + ) + + # Check if unset filters are skipped | Rows - 2 filters + 1 empty + 1 column + 3 data + self.assertEqual(len(xlsx_data), 7) + + # Check filter formatting + self.assertListEqual(xlsx_data[:2], [["Label 1", "Filter Value"], ["Label 3", "0, 1, 2, 3, 4"]]) + def test_xlsx_export_with_composite_cell_value(self): """Test excel export using rows with composite cell value""" @@ -236,3 +246,19 @@ data = columns, result except Exception as e: raise e frappe.db.rollback() + + +def create_mock_data(): + data = frappe._dict() + data.columns = [ + {"label": "Column A", "fieldname": "column_a", "fieldtype": "Float"}, + {"label": "Column B", "fieldname": "column_b", "width": 100, "fieldtype": "Float"}, + {"label": "Column C", "fieldname": "column_c", "width": 150, "fieldtype": "Duration"}, + ] + data.result = [ + [1.0, 3.0, 600], + {"column_a": 22.1, "column_b": 21.8, "column_c": 86412}, + {"column_b": 5.1, "column_c": 53234, "column_a": 11.1}, + [3.0, 1.5, 333], + ] + return data