feat(reports): sort exported breakdown tables
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-05-25 00:10:28 +03:30
parent d18fdb1454
commit f99e883f12
2 changed files with 61 additions and 6 deletions

View File

@@ -225,6 +225,41 @@ def _percentage_display(locale: ExportLocale, rows: list[dict], row_data: dict,
return "-"
def _percentage_number(rows: list[dict] | None, row_data: dict) -> float:
row_id = str(row_data.get("id")) if row_data.get("id") is not None else None
row_name = row_data.get("name")
for row in rows or []:
if row_id is not None and str(row.get("id")) == row_id:
try:
return float(row.get("percentage") or 0)
except (TypeError, ValueError):
return 0.0
if row_name and row.get("name") == row_name:
try:
return float(row.get("percentage") or 0)
except (TypeError, ValueError):
return 0.0
return 0.0
def _percentage_sort_value(row: dict) -> float:
try:
return float(row.get("percentage") or 0)
except (TypeError, ValueError):
return 0.0
def _sort_breakdown_rows(rows: list[dict], hour_percentages: list[dict] | None) -> list[dict]:
return sorted(
rows,
key=lambda row: (
-_percentage_number(hour_percentages, row),
-(row.get("billable_seconds") or 0),
row.get("name") or "",
),
)
def _percentage_value(locale: ExportLocale, percentage: str, *, ascii_digits: bool = False) -> str:
value = f"{locale.format_amount(percentage, ascii_digits=ascii_digits)}%"
return f"\u202B{value}\u202C" if locale.is_rtl and ascii_digits else value # Unicode bidi control characters
@@ -244,7 +279,7 @@ def _summary_breakdown_rows(
_percentage_value(locale, row["percentage"], ascii_digits=True),
_percentage_display(locale, income_rows, row, ascii_digits=True),
]
for row in hour_rows
for row in sorted(hour_rows, key=lambda row: (-_percentage_sort_value(row), row.get("name") or ""))
]
@@ -508,7 +543,7 @@ def _append_breakdown_table(
_apply_cell_style(worksheet.cell(row=worksheet.max_row, column=1), rtl=locale.is_rtl)
return
for row in rows:
for row in _sort_breakdown_rows(rows, hour_percentages):
values = [
row["name"],
locale.format_duration(row["billable_duration"], ascii_digits=True),
@@ -808,8 +843,9 @@ def _render_all_users_overall_excel_sheet(
fill=HEADER_FILL,
)
current_row += 1
if rows:
for row in rows:
sorted_rows = _sort_breakdown_rows(rows, hour_percentages)
if sorted_rows:
for row in sorted_rows:
_write_table_row(
worksheet,
row=current_row,
@@ -1172,7 +1208,8 @@ def _append_pdf_report_sections(
hour_percentage_rows = user_summary[f"{prefix}_percentages"]
income_percentage_rows = user_summary[f"{prefix}_income_percentages"]
header = _rtl_row(locale, header_values)
body_rows = _report_table_rows(locale, rows, is_daily=is_daily)
sorted_rows = rows if is_daily else _sort_breakdown_rows(rows, hour_percentage_rows)
body_rows = _report_table_rows(locale, sorted_rows, is_daily=is_daily)
if hour_percentage_rows is not None:
body_rows = [
_rtl_row(
@@ -1193,7 +1230,7 @@ def _append_pdf_report_sections(
_percentage_display(locale, income_percentage_rows or [], row),
],
)
for row in rows
for row in sorted_rows
] or [
_rtl_row(
locale,

View File

@@ -7,6 +7,7 @@ from apps.reports.services.export_i18n import build_export_locale
from apps.reports.services.exporters import (
_pdf_summary_rate_label,
_rate_label,
_sort_breakdown_rows,
build_excel_report,
build_pdf_report,
)
@@ -133,6 +134,23 @@ class ReportExporterTests(TestCase):
"Variable rate",
)
def test_breakdown_rows_are_sorted_by_hour_percentage(self):
rows = [
{"id": "low", "name": "Low", "billable_seconds": 7200},
{"id": "high", "name": "High", "billable_seconds": 3600},
{"id": "tie", "name": "Tie", "billable_seconds": 10800},
]
percentages = [
{"id": "low", "name": "Low", "percentage": "20"},
{"id": "high", "name": "High", "percentage": "70"},
{"id": "tie", "name": "Tie", "percentage": "20"},
]
self.assertEqual(
[row["name"] for row in _sort_breakdown_rows(rows, percentages)],
["High", "Tie", "Low"],
)
def test_excel_export_adds_per_user_sheets_and_daily_rate_column(self):
locale = build_export_locale("en")
report_data = make_report_data(