feat(reports): improve summary rates and export formatting
This commit is contained in:
@@ -76,7 +76,7 @@ def _money_label_excel(locale: ExportLocale, income_totals: list[dict]) -> str:
|
||||
|
||||
def _rates_label(locale: ExportLocale, rates: list[dict], *, ascii_digits: bool = False) -> str:
|
||||
if not rates:
|
||||
return "-"
|
||||
return locale.format_number("0", ascii_digits=ascii_digits)
|
||||
items = [
|
||||
f"{locale.format_amount_for_currency(rate['amount'], rate['currency'], ascii_digits=ascii_digits)} {locale.currency_label(rate['currency'])}"
|
||||
for rate in rates
|
||||
@@ -103,13 +103,13 @@ def _rate_period_label(locale: ExportLocale, row: dict, *, ascii_digits: bool =
|
||||
|
||||
def _rate_label(locale: ExportLocale, rate: dict | None) -> str:
|
||||
if not rate:
|
||||
return "-"
|
||||
return locale.format_number("0")
|
||||
return f"{locale.format_amount_for_currency(rate['amount'], rate['currency'])} {locale.currency_label(rate['currency'])}"
|
||||
|
||||
|
||||
def _rate_label_excel(locale: ExportLocale, rate: dict | None) -> str:
|
||||
if not rate:
|
||||
return "-"
|
||||
return locale.format_number("0", ascii_digits=True)
|
||||
value = (
|
||||
f"{locale.format_amount_for_currency(rate['amount'], rate['currency'], ascii_digits=True)} "
|
||||
f"{locale.currency_label(rate['currency'])}"
|
||||
@@ -213,7 +213,14 @@ def _append_user_summary_block(worksheet, *, locale: ExportLocale, user_summary:
|
||||
worksheet.append(row)
|
||||
|
||||
|
||||
def _percentage_display(locale: ExportLocale, rows: list[dict], row_data: dict, *, ascii_digits: bool = False) -> str:
|
||||
def _percentage_display(
|
||||
locale: ExportLocale,
|
||||
rows: list[dict],
|
||||
row_data: dict,
|
||||
*,
|
||||
ascii_digits: bool = False,
|
||||
default: str = "0%",
|
||||
) -> str:
|
||||
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:
|
||||
@@ -222,7 +229,7 @@ def _percentage_display(locale: ExportLocale, rows: list[dict], row_data: dict,
|
||||
return value
|
||||
if row_name and row["name"] == row_name:
|
||||
return value
|
||||
return "-"
|
||||
return default
|
||||
|
||||
|
||||
def _percentage_number(rows: list[dict] | None, row_data: dict) -> float:
|
||||
@@ -277,7 +284,7 @@ def _summary_breakdown_rows(
|
||||
[
|
||||
row["name"],
|
||||
_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, default="-"),
|
||||
]
|
||||
for row in sorted(hour_rows, key=lambda row: (-_percentage_sort_value(row), row.get("name") or ""))
|
||||
]
|
||||
@@ -304,11 +311,11 @@ def _rate_to_label(locale: ExportLocale, to_date: str | None, *, ascii_digits: b
|
||||
|
||||
def _append_rate_history_table_excel(worksheet, *, locale: ExportLocale, user_summary: dict) -> None:
|
||||
worksheet.append([])
|
||||
_append_merged_heading(worksheet, locale=locale, title=locale.t("rate_history"), span=3)
|
||||
_append_merged_heading(worksheet, locale=locale, title=locale.t("rate_history"), span=4)
|
||||
header_row = worksheet.max_row + 1
|
||||
worksheet.append(
|
||||
_excel_table_row(
|
||||
[locale.t("hourly_rate"), locale.t("from"), locale.t("to")],
|
||||
[locale.t("hourly_rate"), locale.t("from"), locale.t("to"), locale.t("project")],
|
||||
)
|
||||
)
|
||||
for cell in worksheet[header_row]:
|
||||
@@ -327,6 +334,7 @@ def _append_rate_history_table_excel(worksheet, *, locale: ExportLocale, user_su
|
||||
_rate_period_label(locale, row, ascii_digits=True),
|
||||
locale.format_date(row["from_date"], ascii_digits=True),
|
||||
_rate_to_label(locale, row.get("to_date"), ascii_digits=True),
|
||||
row.get("project_name") or "-",
|
||||
],
|
||||
)
|
||||
)
|
||||
@@ -562,7 +570,7 @@ def _append_breakdown_table(
|
||||
),
|
||||
_money_label_excel(locale, row["income_totals"]),
|
||||
*(
|
||||
[_percentage_display(locale, income_percentages or [], row, ascii_digits=True)]
|
||||
[_percentage_display(locale, income_percentages or [], row, ascii_digits=True, default="-")]
|
||||
if hour_percentages is not None
|
||||
else []
|
||||
),
|
||||
@@ -669,8 +677,11 @@ def _user_summary_row_payload(locale: ExportLocale, summary: dict) -> tuple[int,
|
||||
locale.format_duration(summary["billable_duration"], ascii_digits=True) if index == 0 else None,
|
||||
*(rate_rows[index] if index < len(rate_rows) else [None, None]),
|
||||
_money_label_excel(locale, summary["income_totals"]) if index == 0 else None,
|
||||
None,
|
||||
*(client_rows[index] if index < len(client_rows) else [None, None, None]),
|
||||
None,
|
||||
*(project_rows[index] if index < len(project_rows) else [None, None, None]),
|
||||
None,
|
||||
*(tag_rows[index] if index < len(tag_rows) else [None, None, None]),
|
||||
],
|
||||
)
|
||||
@@ -733,7 +744,7 @@ def _render_all_users_overall_excel_sheet(
|
||||
worksheet,
|
||||
row=15,
|
||||
start_col=1,
|
||||
end_col=15,
|
||||
end_col=18,
|
||||
value=locale.t("users_summary_sheet"),
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
@@ -744,12 +755,15 @@ def _render_all_users_overall_excel_sheet(
|
||||
locale.t("hourly_rate"),
|
||||
locale.t("period"),
|
||||
locale.t("income"),
|
||||
"",
|
||||
locale.t("clients"),
|
||||
locale.t("hour_percentage"),
|
||||
locale.t("income_percentage"),
|
||||
"",
|
||||
locale.t("projects"),
|
||||
locale.t("hour_percentage"),
|
||||
locale.t("income_percentage"),
|
||||
"",
|
||||
locale.t("tags"),
|
||||
locale.t("hour_percentage"),
|
||||
locale.t("income_percentage"),
|
||||
@@ -784,19 +798,26 @@ def _render_all_users_overall_excel_sheet(
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=4, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=5, value_present=True)
|
||||
if len(client_rows) == 1:
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=7, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=8, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=9, value_present=True)
|
||||
if len(project_rows) == 1:
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=10, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=11, value_present=True)
|
||||
if len(project_rows) == 1:
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=12, value_present=True)
|
||||
if len(tag_rows) == 1:
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=13, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=14, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=15, value_present=True)
|
||||
if len(tag_rows) == 1:
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=16, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=17, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=18, value_present=True)
|
||||
current_row += span
|
||||
|
||||
for row_index in range(16, current_row):
|
||||
for column_index in (7, 11, 15):
|
||||
cell = worksheet.cell(row=row_index, column=column_index)
|
||||
cell.value = None
|
||||
cell.fill = PatternFill(fill_type=None)
|
||||
cell.border = Border()
|
||||
|
||||
current_row += 2
|
||||
for title_key, rows, hour_percentages, income_percentages in (
|
||||
(
|
||||
@@ -855,7 +876,7 @@ def _render_all_users_overall_excel_sheet(
|
||||
locale.format_duration(row["billable_duration"], ascii_digits=True),
|
||||
_percentage_display(locale, hour_percentages or [], row, ascii_digits=True),
|
||||
_money_label_excel(locale, row["income_totals"]),
|
||||
_percentage_display(locale, income_percentages or [], row, ascii_digits=True),
|
||||
_percentage_display(locale, income_percentages or [], row, ascii_digits=True, default="-"),
|
||||
],
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
@@ -1121,7 +1142,7 @@ def _build_pdf_user_summary_table(locale: ExportLocale, summary: dict, doc_width
|
||||
def _build_pdf_rate_history_table(locale: ExportLocale, summary: dict, doc_width: float) -> Table:
|
||||
rows = summary.get("rate_periods") or []
|
||||
data = [
|
||||
_rtl_row(locale, [locale.t("hourly_rate"), locale.t("from"), locale.t("to")]),
|
||||
_rtl_row(locale, [locale.t("hourly_rate"), locale.t("from"), locale.t("to"), locale.t("project")]),
|
||||
*(
|
||||
_rtl_row(
|
||||
locale,
|
||||
@@ -1129,14 +1150,15 @@ def _build_pdf_rate_history_table(locale: ExportLocale, summary: dict, doc_width
|
||||
_rate_period_label(locale, row),
|
||||
locale.format_date(row["from_date"]),
|
||||
_rate_to_label(locale, row.get("to_date")),
|
||||
row.get("project_name") or "-",
|
||||
],
|
||||
)
|
||||
for row in rows
|
||||
),
|
||||
]
|
||||
if not rows:
|
||||
data.append(_rtl_row(locale, [locale.t("no_data"), "", ""]))
|
||||
fixed_widths = [doc_width * 0.18, doc_width * 0.18]
|
||||
data.append(_rtl_row(locale, [locale.t("no_data"), "", "", ""]))
|
||||
fixed_widths = [doc_width * 0.18, doc_width * 0.18, doc_width * 0.24]
|
||||
column_widths = [doc_width - sum(fixed_widths), *fixed_widths]
|
||||
if locale.is_rtl:
|
||||
column_widths = list(reversed(column_widths))
|
||||
@@ -1227,7 +1249,7 @@ def _append_pdf_report_sections(
|
||||
]
|
||||
),
|
||||
_money_label(locale, row["income_totals"]),
|
||||
_percentage_display(locale, income_percentage_rows or [], row),
|
||||
_percentage_display(locale, income_percentage_rows or [], row, default="-"),
|
||||
],
|
||||
)
|
||||
for row in sorted_rows
|
||||
|
||||
Reference in New Issue
Block a user