feat(reports): sort exported breakdown tables
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user