fix: handle total row in export of report with column filters

This commit is contained in:
Abdeali Chharchhoda 2026-02-05 19:10:39 +05:30
parent c9cdacb4ce
commit 9fa55476b7
2 changed files with 91 additions and 26 deletions

View file

@ -75,7 +75,13 @@ def get_report_result(report, filters):
@frappe.read_only()
def generate_report_result(
report, filters=None, user=None, custom_columns=None, is_tree=False, parent_field=None
report,
filters=None,
user=None,
custom_columns=None,
is_tree=False,
parent_field=None,
skip_total_calculation=False,
):
user = user or frappe.session.user
filters = filters or []
@ -110,12 +116,13 @@ def generate_report_result(
if result:
result = get_filtered_data(report.ref_doctype, columns, result, user)
if cint(report.add_total_row) and result and not skip_total_row:
has_total_row = cint(report.add_total_row) and result and not skip_total_row
if has_total_row and not skip_total_calculation:
result = add_total_row(result, columns, is_tree=is_tree, parent_field=parent_field)
if isinstance(filters, dict) and filters.get("translate_data"):
total_row = cint(report.add_total_row) and result and not skip_total_row
result = translate_report_data(result, total_row)
result = translate_report_data(result, has_total_row)
return {
"result": result,
@ -201,6 +208,7 @@ def run(
parent_field=None,
are_default_filters=True,
js_filters=None,
skip_total_calculation=False,
):
if not user:
user = frappe.session.user
@ -217,8 +225,10 @@ def run(
if sbool(are_default_filters) and report.get("custom_filters"):
filters = report.custom_filters
is_prepared_report = report.prepared_report and not sbool(ignore_prepared_report) and not custom_columns
try:
if report.prepared_report and not sbool(ignore_prepared_report) and not custom_columns:
if is_prepared_report:
if filters:
if isinstance(filters, str):
filters = json.loads(filters)
@ -228,7 +238,9 @@ def run(
dn = ""
result = get_prepared_report_result(report, filters, dn, user)
else:
result = generate_report_result(report, filters, user, custom_columns, is_tree, parent_field)
result = generate_report_result(
report, filters, user, custom_columns, is_tree, parent_field, skip_total_calculation
)
add_data_to_monitor(report=report.reference_report or report.name)
except Exception:
frappe.log_error("Report Error")
@ -239,6 +251,8 @@ def run(
if sbool(are_default_filters) and report.get("custom_filters"):
result["custom_filters"] = report.custom_filters
result["is_prepared_report"] = is_prepared_report
return result
@ -367,13 +381,22 @@ def _export_query(form_params, csv_params, populate_response=True):
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
visible_idx = form_params.visible_idx or []
ignore_visible_idx = sbool(form_params.get("ignore_visible_idx"))
skip_all_rows_total = not ignore_visible_idx
include_hidden_columns = form_params.include_hidden_columns
if isinstance(visible_idx, str):
visible_idx = json.loads(visible_idx)
data = run(report_name, form_params.filters, custom_columns=custom_columns, are_default_filters=False)
data = run(
report_name,
form_params.filters,
custom_columns=custom_columns,
are_default_filters=False,
skip_total_calculation=skip_all_rows_total,
)
data = frappe._dict(data)
data.filters = form_params.applied_filters
@ -384,13 +407,22 @@ def _export_query(form_params, csv_params, populate_response=True):
)
return
# calculate total row only for visible rows
if skip_all_rows_total and cint(data.get("add_total_row")):
if data.get("is_prepared_report"):
data.result = data.result[:-1] # delete total row from result
data["result"] = add_total_row(data.result, data.columns, visible_idx=visible_idx)
format_fields(data)
xlsx_data, column_widths, header_index = build_xlsx_data(
data,
visible_idx,
include_indentation,
include_filters=include_filters,
include_hidden_columns=include_hidden_columns,
ignore_visible_idx=ignore_visible_idx,
)
if file_format_type == "CSV":
@ -555,11 +587,31 @@ def build_xlsx_data(
return result, column_widths, header_index
def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
def add_total_row(
result,
columns,
meta=None,
is_tree=False,
parent_field=None,
visible_idx: list[int] | None = None,
ignore_visible_idx: bool = False,
) -> list[dict | list[Any]]:
total_row = [""] * len(columns)
has_percent = []
for i, col in enumerate(columns):
if not visible_idx or len(visible_idx) == len(result):
# It's not possible to have same length and different content.
ignore_visible_idx = True
visible_idx_set = set()
else:
# Note: converted for faster lookups
ignore_visible_idx = False
visible_idx_set = set(visible_idx)
# all rows are dict or list/tuple, we can check the first row to decide the type
is_row_dict = isinstance(result[0], dict) if result else False
for col_idx, col in enumerate(columns):
fieldtype, options, fieldname = None, None, None
if isinstance(col, str):
if meta:
@ -582,10 +634,16 @@ def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
fieldname = col.get("fieldname")
options = col.get("options")
for row in result:
if i >= len(row):
for row_idx, row in enumerate(result):
# Skip rows not in visible_idx when filtering is enabled
if not ignore_visible_idx and row_idx not in visible_idx_set:
continue
cell = row.get(fieldname) if isinstance(row, dict) else row[i]
# Skip if column index is out of bounds for list/tuple rows
if not is_row_dict and col_idx >= len(row):
continue
cell = row.get(fieldname) if is_row_dict else row[col_idx]
if fieldtype is None:
if isinstance(cell, int):
fieldtype = "Int"
@ -593,21 +651,21 @@ def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
fieldtype = "Float"
if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(cell):
if not (is_tree and row.get(parent_field)):
total_row[i] = flt(total_row[i]) + flt(cell)
total_row[col_idx] = flt(total_row[col_idx]) + flt(cell)
if fieldtype == "Percent" and i not in has_percent:
has_percent.append(i)
if fieldtype == "Percent" and col_idx not in has_percent:
has_percent.append(col_idx)
if fieldtype == "Time" and cell:
if not total_row[i]:
total_row[i] = timedelta(hours=0, minutes=0, seconds=0)
total_row[i] = total_row[i] + cell
if not total_row[col_idx]:
total_row[col_idx] = timedelta(hours=0, minutes=0, seconds=0)
total_row[col_idx] = total_row[col_idx] + cell
if fieldtype == "Link" and options == "Currency":
total_row[i] = result[0].get(fieldname) if isinstance(result[0], dict) else result[0][i]
total_row[col_idx] = result[0].get(fieldname) if is_row_dict else result[0][col_idx]
for i in has_percent:
total_row[i] = flt(total_row[i]) / len(result)
for col_idx in has_percent:
total_row[col_idx] = flt(total_row[col_idx]) / len(result)
first_col_fieldtype = None
if isinstance(columns[0], str):
@ -971,7 +1029,7 @@ def validate_filters_permissions(report_name, filters=None, user=None, js_filter
)
def translate_report_data(data, total_row):
def translate_report_data(data, total_row: bool):
for d in data[:-1] if total_row else data:
for field, value in d.items():
if isinstance(value, str):

View file

@ -1768,10 +1768,16 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
filters.prepared_report_name = this.prepared_report_name;
}
const visible_idx = this.datatable?.bodyRenderer.visibleRowIndices || [];
if (visible_idx.length + 1 === this.data?.length) {
visible_idx.push(visible_idx.length);
// excluding total row index
let visible_idx = this.datatable?.bodyRenderer.visibleRowIndices || [];
if (this.raw_data.add_total_row) {
visible_idx.push(this.data.length - 1); // total row index
}
const ignore_visible_idx = visible_idx.length === this.data.length;
visible_idx = ignore_visible_idx ? [] : visible_idx;
const args = {
cmd: "frappe.desk.query_report.export_query",
report_name: this.report_name,
@ -1780,6 +1786,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
filters: filters,
applied_filters: applied_filters,
visible_idx,
ignore_visible_idx,
csv_delimiter,
csv_quoting,
csv_decimal_sep,