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 "-"
|
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:
|
def _percentage_value(locale: ExportLocale, percentage: str, *, ascii_digits: bool = False) -> str:
|
||||||
value = f"{locale.format_amount(percentage, ascii_digits=ascii_digits)}%"
|
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
|
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_value(locale, row["percentage"], ascii_digits=True),
|
||||||
_percentage_display(locale, income_rows, row, 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)
|
_apply_cell_style(worksheet.cell(row=worksheet.max_row, column=1), rtl=locale.is_rtl)
|
||||||
return
|
return
|
||||||
|
|
||||||
for row in rows:
|
for row in _sort_breakdown_rows(rows, hour_percentages):
|
||||||
values = [
|
values = [
|
||||||
row["name"],
|
row["name"],
|
||||||
locale.format_duration(row["billable_duration"], ascii_digits=True),
|
locale.format_duration(row["billable_duration"], ascii_digits=True),
|
||||||
@@ -808,8 +843,9 @@ def _render_all_users_overall_excel_sheet(
|
|||||||
fill=HEADER_FILL,
|
fill=HEADER_FILL,
|
||||||
)
|
)
|
||||||
current_row += 1
|
current_row += 1
|
||||||
if rows:
|
sorted_rows = _sort_breakdown_rows(rows, hour_percentages)
|
||||||
for row in rows:
|
if sorted_rows:
|
||||||
|
for row in sorted_rows:
|
||||||
_write_table_row(
|
_write_table_row(
|
||||||
worksheet,
|
worksheet,
|
||||||
row=current_row,
|
row=current_row,
|
||||||
@@ -1172,7 +1208,8 @@ def _append_pdf_report_sections(
|
|||||||
hour_percentage_rows = user_summary[f"{prefix}_percentages"]
|
hour_percentage_rows = user_summary[f"{prefix}_percentages"]
|
||||||
income_percentage_rows = user_summary[f"{prefix}_income_percentages"]
|
income_percentage_rows = user_summary[f"{prefix}_income_percentages"]
|
||||||
header = _rtl_row(locale, header_values)
|
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:
|
if hour_percentage_rows is not None:
|
||||||
body_rows = [
|
body_rows = [
|
||||||
_rtl_row(
|
_rtl_row(
|
||||||
@@ -1193,7 +1230,7 @@ def _append_pdf_report_sections(
|
|||||||
_percentage_display(locale, income_percentage_rows or [], row),
|
_percentage_display(locale, income_percentage_rows or [], row),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
for row in rows
|
for row in sorted_rows
|
||||||
] or [
|
] or [
|
||||||
_rtl_row(
|
_rtl_row(
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from apps.reports.services.export_i18n import build_export_locale
|
|||||||
from apps.reports.services.exporters import (
|
from apps.reports.services.exporters import (
|
||||||
_pdf_summary_rate_label,
|
_pdf_summary_rate_label,
|
||||||
_rate_label,
|
_rate_label,
|
||||||
|
_sort_breakdown_rows,
|
||||||
build_excel_report,
|
build_excel_report,
|
||||||
build_pdf_report,
|
build_pdf_report,
|
||||||
)
|
)
|
||||||
@@ -133,6 +134,23 @@ class ReportExporterTests(TestCase):
|
|||||||
"Variable rate",
|
"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):
|
def test_excel_export_adds_per_user_sheets_and_daily_rate_column(self):
|
||||||
locale = build_export_locale("en")
|
locale = build_export_locale("en")
|
||||||
report_data = make_report_data(
|
report_data = make_report_data(
|
||||||
|
|||||||
Reference in New Issue
Block a user