fix: handle total row in export of report with column filters
This commit is contained in:
parent
c9cdacb4ce
commit
9fa55476b7
2 changed files with 91 additions and 26 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue