fix!: Always Bold report header row (#34703)
* fix: Always bold report header row * fix: update test cases * refactor: add option to bold filter rows in XLSX output * chore: minor changes * chore: rename index variable * revert: undo bold filters param use * refactor: remove duplication for building xlsx data * revert: add has_filters parameter to check for filter row bold * refactor: add type hints and docstrings for XLSX data handling functions
This commit is contained in:
parent
a25db08e25
commit
4647986aeb
4 changed files with 97 additions and 35 deletions
|
|
@ -5,6 +5,7 @@ import datetime
|
|||
import json
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
import frappe.desk.reportview
|
||||
|
|
@ -384,7 +385,7 @@ def _export_query(form_params, csv_params, populate_response=True):
|
|||
return
|
||||
|
||||
format_fields(data)
|
||||
xlsx_data, column_widths = build_xlsx_data(
|
||||
xlsx_data, column_widths, header_index = build_xlsx_data(
|
||||
data,
|
||||
visible_idx,
|
||||
include_indentation,
|
||||
|
|
@ -400,7 +401,13 @@ def _export_query(form_params, csv_params, populate_response=True):
|
|||
file_extension = "csv"
|
||||
elif file_format_type == "Excel":
|
||||
file_extension = "xlsx"
|
||||
content = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths).getvalue()
|
||||
content = make_xlsx(
|
||||
xlsx_data,
|
||||
"Query Report",
|
||||
column_widths=column_widths,
|
||||
header_index=header_index,
|
||||
has_filters=bool(include_filters),
|
||||
).getvalue()
|
||||
|
||||
if include_filters:
|
||||
for value in (data.filters or {}).values():
|
||||
|
|
@ -440,13 +447,30 @@ def format_fields(data: frappe._dict) -> None:
|
|||
|
||||
|
||||
def build_xlsx_data(
|
||||
data,
|
||||
visible_idx,
|
||||
include_indentation,
|
||||
include_filters=False,
|
||||
ignore_visible_idx=False,
|
||||
include_hidden_columns=False,
|
||||
):
|
||||
data: frappe._dict,
|
||||
visible_idx: list[int],
|
||||
include_indentation: bool,
|
||||
include_filters: bool = False,
|
||||
ignore_visible_idx: bool = False,
|
||||
include_hidden_columns: bool = False,
|
||||
) -> tuple[list[list[Any]], list[int], int]:
|
||||
"""
|
||||
Build Excel data structure from report data with proper formatting.
|
||||
|
||||
Args:
|
||||
data: Report data containing columns, result, and filters
|
||||
visible_idx: List of row indices that are visible in the report
|
||||
include_indentation: Whether to include indentation for tree-like data
|
||||
include_filters: Whether to include filter rows at the top of the Excel sheet
|
||||
ignore_visible_idx: Whether to ignore the visible_idx parameter
|
||||
include_hidden_columns: Whether to include columns marked as hidden
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing:
|
||||
- result: List of rows for the Excel sheet
|
||||
- column_widths: List of column widths for the Excel sheet
|
||||
- header_index: Index of the header row in the result
|
||||
"""
|
||||
EXCEL_TYPES = (
|
||||
str,
|
||||
bool,
|
||||
|
|
@ -468,6 +492,7 @@ def build_xlsx_data(
|
|||
|
||||
result = []
|
||||
column_widths = []
|
||||
header_index = 0
|
||||
|
||||
include_hidden_columns = cint(include_hidden_columns)
|
||||
include_indentation = cint(include_indentation)
|
||||
|
|
@ -487,6 +512,9 @@ def build_xlsx_data(
|
|||
filter_data.append([])
|
||||
result += filter_data
|
||||
|
||||
# header is after filters + 1 empty row
|
||||
header_index = len(result)
|
||||
|
||||
column_data = []
|
||||
for column in data.columns:
|
||||
if column.get("hidden") and not include_hidden_columns:
|
||||
|
|
@ -525,7 +553,7 @@ def build_xlsx_data(
|
|||
|
||||
result.append(row_data)
|
||||
|
||||
return result, column_widths
|
||||
return result, column_widths, header_index
|
||||
|
||||
|
||||
def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
|
||||
|
|
|
|||
|
|
@ -171,22 +171,28 @@ class AutoEmailReport(Document):
|
|||
columns = update_field_types(columns)
|
||||
return self.get_html_table(columns, data)
|
||||
|
||||
elif self.format == "XLSX":
|
||||
elif self.format in ("XLSX", "CSV"):
|
||||
report_data = frappe._dict()
|
||||
report_data["columns"] = columns
|
||||
report_data["result"] = data
|
||||
|
||||
xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
|
||||
xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths)
|
||||
return xlsx_file.getvalue()
|
||||
xlsx_data, column_widths, header_index = build_xlsx_data(
|
||||
report_data, [], 1, ignore_visible_idx=True
|
||||
)
|
||||
|
||||
elif self.format == "CSV":
|
||||
report_data = frappe._dict()
|
||||
report_data["columns"] = columns
|
||||
report_data["result"] = data
|
||||
if self.format == "XLSX":
|
||||
xlsx_file = make_xlsx(
|
||||
xlsx_data,
|
||||
"Auto Email Report",
|
||||
column_widths=column_widths,
|
||||
header_index=header_index,
|
||||
has_filters=bool(self.filters),
|
||||
)
|
||||
|
||||
xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
|
||||
return to_csv(xlsx_data)
|
||||
return xlsx_file.getvalue()
|
||||
|
||||
else:
|
||||
return to_csv(xlsx_data)
|
||||
|
||||
elif self.format == "PDF":
|
||||
columns, data = make_links(columns, data)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class TestQueryReport(IntegrationTestCase):
|
|||
visible_idx = [0, 2, 3]
|
||||
|
||||
# Build the result
|
||||
xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation=0)
|
||||
xlsx_data, column_widths, _ = build_xlsx_data(data, visible_idx, include_indentation=0)
|
||||
|
||||
self.assertEqual(type(xlsx_data), list)
|
||||
self.assertEqual(len(xlsx_data), 4) # columns + data
|
||||
|
|
@ -53,10 +53,12 @@ class TestQueryReport(IntegrationTestCase):
|
|||
visible_idx = [0, 2, 3]
|
||||
|
||||
# Build the result
|
||||
xlsx_data, _column_widths = build_xlsx_data(
|
||||
xlsx_data, _column_widths, header_index = build_xlsx_data(
|
||||
data, visible_idx, include_indentation=False, include_filters=True
|
||||
)
|
||||
|
||||
self.assertEqual(header_index, 3) # 2 filter rows + 1 empty row
|
||||
|
||||
# Check if unset filters are skipped | Rows - 2 filters + 1 empty + 1 column + 3 data
|
||||
self.assertEqual(len(xlsx_data), 7)
|
||||
|
||||
|
|
@ -80,9 +82,9 @@ class TestQueryReport(IntegrationTestCase):
|
|||
visible_idx = [0, 1]
|
||||
|
||||
# Build the result
|
||||
xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation=0)
|
||||
xlsx_data, column_widths, header_index = build_xlsx_data(data, visible_idx, include_indentation=0)
|
||||
# Export to excel
|
||||
make_xlsx(xlsx_data, "Query Report", column_widths=column_widths)
|
||||
make_xlsx(xlsx_data, "Query Report", column_widths=column_widths, header_index=header_index)
|
||||
|
||||
for row in xlsx_data:
|
||||
# column_b should be 'str' even with composite cell value
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import datetime
|
||||
import re
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import openpyxl
|
||||
import xlrd
|
||||
|
|
@ -31,7 +32,28 @@ def get_excel_date_format():
|
|||
|
||||
|
||||
# return xlsx file object
|
||||
def make_xlsx(data, sheet_name, wb=None, column_widths=None):
|
||||
def make_xlsx(
|
||||
data: list[list[Any]],
|
||||
sheet_name: str,
|
||||
wb: openpyxl.Workbook | None = None,
|
||||
column_widths: list[int] | None = None,
|
||||
header_index: int = 0,
|
||||
has_filters: bool = False,
|
||||
) -> BytesIO:
|
||||
"""
|
||||
Create an Excel file with the given data and formatting options.
|
||||
|
||||
Args:
|
||||
data: List of rows, where each row is a list of cell values
|
||||
sheet_name: Name of the Excel sheet
|
||||
wb: Existing workbook to add sheet to. If None, creates new workbook
|
||||
column_widths: List of column widths in Excel units. If None, auto-sized
|
||||
header_index: Row index (0-based) that should be formatted as header making it bold
|
||||
has_filters: If True, applies bold formatting to the first column of filter rows
|
||||
|
||||
Returns:
|
||||
BytesIO: object containing the Excel file data
|
||||
"""
|
||||
column_widths = column_widths or []
|
||||
if wb is None:
|
||||
wb = openpyxl.Workbook(write_only=True)
|
||||
|
|
@ -43,14 +65,15 @@ def make_xlsx(data, sheet_name, wb=None, column_widths=None):
|
|||
if column_width:
|
||||
ws.column_dimensions[get_column_letter(i + 1)].width = column_width
|
||||
|
||||
row1 = ws.row_dimensions[1]
|
||||
row1.font = Font(name="Calibri", bold=True)
|
||||
|
||||
date_format, time_format = get_excel_date_format()
|
||||
bold_font = Font(name="Calibri", bold=True)
|
||||
|
||||
for row in data:
|
||||
for row_idx, row in enumerate(data):
|
||||
clean_row = []
|
||||
for item in row:
|
||||
is_header_row = row_idx == header_index
|
||||
is_filter_row = has_filters and row_idx < header_index
|
||||
|
||||
for col_idx, item in enumerate(row):
|
||||
if isinstance(item, str) and (sheet_name not in ["Data Import Template", "Data Export"]):
|
||||
value = handle_html(item)
|
||||
else:
|
||||
|
|
@ -60,16 +83,19 @@ def make_xlsx(data, sheet_name, wb=None, column_widths=None):
|
|||
# Remove illegal characters from the string
|
||||
value = ILLEGAL_CHARACTERS_RE.sub("", value)
|
||||
|
||||
cell = WriteOnlyCell(ws, value=value)
|
||||
|
||||
if isinstance(value, datetime.date | datetime.datetime):
|
||||
number_format = date_format
|
||||
if isinstance(value, datetime.datetime):
|
||||
number_format = f"{date_format} {time_format}"
|
||||
|
||||
cell = WriteOnlyCell(ws, value=value)
|
||||
cell.number_format = number_format
|
||||
clean_row.append(cell)
|
||||
else:
|
||||
clean_row.append(value)
|
||||
|
||||
# Apply bold font for header row or first column of filter rows
|
||||
if is_header_row or (is_filter_row and col_idx == 0):
|
||||
cell.font = bold_font
|
||||
|
||||
clean_row.append(cell)
|
||||
|
||||
ws.append(clean_row)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue