feat(reports): add daily rate to report tables and exports

This commit is contained in:
2026-04-28 20:26:20 +03:30
parent 1cd948592c
commit ef05f0a89e
5 changed files with 195 additions and 26 deletions

View File

@@ -63,6 +63,18 @@ def _money_label_excel(locale: ExportLocale, income_totals: list[dict]) -> str:
return locale.format_money_label(income_totals, ascii_digits=True)
def _rate_label(locale: ExportLocale, rate: dict | None) -> str:
if not rate:
return "-"
return f"{locale.format_amount(rate['amount'])} {locale.currency_label(rate['currency'])}"
def _rate_label_excel(locale: ExportLocale, rate: dict | None) -> str:
if not rate:
return "-"
return f"{locale.format_amount(rate['amount'], ascii_digits=True)} {locale.currency_label(rate['currency'])}"
def _section_headers(locale: ExportLocale) -> list[str]:
headers = [
locale.t("name"),
@@ -87,7 +99,24 @@ def _append_meta_block(worksheet, *, locale: ExportLocale, report_data: dict) ->
worksheet.append(_rtl_row(locale, [locale.t("period"), locale.period_label(scope["period"])]))
worksheet.append(_rtl_row(locale, [locale.t("from_date"), locale.format_date(scope["from_date"], ascii_digits=True)]))
worksheet.append(_rtl_row(locale, [locale.t("to_date"), locale.format_date(scope["to_date"], ascii_digits=True)]))
worksheet.append(_rtl_row(locale, [locale.t("user"), user_label(scope.get("user"), locale, ascii_digits=True)]))
worksheet.append(
_rtl_row(
locale,
[
locale.t("user"),
scope["user"]["name"] if scope.get("user") else locale.t("all_users"),
],
)
)
worksheet.append(
_rtl_row(
locale,
[
locale.t("mobile"),
locale.format_number(scope["user"]["mobile"], ascii_digits=True) if scope.get("user") and scope["user"].get("mobile") else "-",
],
)
)
worksheet.append(_rtl_row(locale, [locale.t("generated_at"), locale.format_date(datetime.now().date(), ascii_digits=True)]))
worksheet.append([])
worksheet.append([locale.t("summary")])
@@ -99,12 +128,12 @@ def _append_meta_block(worksheet, *, locale: ExportLocale, report_data: dict) ->
for row_index in range(1, worksheet.max_row + 1):
first_cell = worksheet.cell(row=row_index, column=1)
second_cell = worksheet.cell(row=row_index, column=2)
if row_index in {1, 9}:
if row_index in {1, 10}:
_apply_cell_style(first_cell, bold=True, fill=HEADER_FILL, rtl=locale.is_rtl)
if second_cell.value:
_apply_cell_style(second_cell, bold=row_index == 1, fill=HEADER_FILL if row_index == 1 else None, rtl=locale.is_rtl)
elif first_cell.value:
_apply_cell_style(first_cell, bold=False, fill=SECTION_FILL if row_index == 8 else None, rtl=locale.is_rtl)
_apply_cell_style(first_cell, bold=False, fill=None, rtl=locale.is_rtl)
if second_cell.value:
_apply_cell_style(second_cell, rtl=locale.is_rtl)
@@ -121,6 +150,7 @@ def _append_daily_table(worksheet, *, locale: ExportLocale, report_data: dict) -
locale.t("billable_hours"),
locale.t("non_billable_hours"),
locale.t("total_hours"),
locale.t("hourly_rate"),
locale.t("income"),
],
)
@@ -142,6 +172,7 @@ def _append_daily_table(worksheet, *, locale: ExportLocale, report_data: dict) -
locale.format_duration(row["billable_duration"], ascii_digits=True),
locale.format_duration(row["non_billable_duration"], ascii_digits=True),
locale.format_duration(row["total_duration"], ascii_digits=True),
_rate_label_excel(locale, row.get("latest_hourly_rate")),
_money_label_excel(locale, row["income_totals"]),
],
)
@@ -157,6 +188,7 @@ def _append_daily_table(worksheet, *, locale: ExportLocale, report_data: dict) -
locale.format_duration(report_data["summary"]["billable_duration"], ascii_digits=True),
locale.format_duration(report_data["summary"]["non_billable_duration"], ascii_digits=True),
locale.format_duration(report_data["summary"]["total_duration"], ascii_digits=True),
"-",
_money_label_excel(locale, report_data["summary"]["income_totals"]),
],
)
@@ -261,18 +293,20 @@ def _styled_table(data: list[list[str]], *, locale: ExportLocale, column_widths:
return table
def _report_table_rows(locale: ExportLocale, rows: list[dict]) -> list[list[str]]:
def _report_table_rows(locale: ExportLocale, rows: list[dict], *, is_daily: bool) -> list[list[str]]:
if not rows:
return [_rtl_row(locale, [locale.t("no_data"), "", "", "", ""])]
column_count = 6 if is_daily else 5
return [_rtl_row(locale, [locale.t("no_data"), *([""] * (column_count - 1))])]
return [
_rtl_row(
locale,
[
locale.format_date(row.get("date")) if row.get("date") else row["name"],
locale.format_duration(row["billable_duration"]),
locale.format_duration(row["non_billable_duration"]),
locale.format_duration(row["total_duration"]),
_money_label(locale, row["income_totals"]),
locale.format_date(row.get("date")) if row.get("date") else row["name"],
locale.format_duration(row["billable_duration"]),
locale.format_duration(row["non_billable_duration"]),
locale.format_duration(row["total_duration"]),
*([_rate_label(locale, row.get("latest_hourly_rate"))] if is_daily else []),
_money_label(locale, row["income_totals"]),
],
)
for row in rows
@@ -360,7 +394,8 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale) -> bytes:
[locale.t("period"), locale.period_label(scope["period"])],
[locale.t("from_date"), locale.format_date(scope["from_date"])],
[locale.t("to_date"), locale.format_date(scope["to_date"])],
[locale.t("user"), user_label(scope.get("user"), locale)],
[locale.t("user"), scope["user"]["name"] if scope.get("user") else locale.t("all_users")],
[locale.t("mobile"), locale.format_number(scope["user"]["mobile"]) if scope.get("user") and scope["user"].get("mobile") else "-"],
[locale.t("generated_at"), locale.format_date(datetime.now().date())],
]
if locale.is_rtl:
@@ -421,23 +456,35 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale) -> bytes:
header = _rtl_row(
locale,
[
locale.t("date") if is_daily else locale.t("name"),
locale.t("billable_hours"),
locale.t("non_billable_hours"),
locale.t("total_hours"),
locale.t("income"),
locale.t("date") if is_daily else locale.t("name"),
locale.t("billable_hours"),
locale.t("non_billable_hours"),
locale.t("total_hours"),
*( [locale.t("hourly_rate")] if is_daily else [] ),
locale.t("income"),
],
)
table = _styled_table(
[header, *_report_table_rows(locale, rows)],
[header, *_report_table_rows(locale, rows, is_daily=is_daily)],
locale=locale,
column_widths=[
doc.width * 0.26,
doc.width * 0.15,
doc.width * 0.17,
doc.width * 0.14,
doc.width * 0.28,
],
column_widths=(
[
doc.width * 0.21,
doc.width * 0.13,
doc.width * 0.15,
doc.width * 0.13,
doc.width * 0.16,
doc.width * 0.22,
]
if is_daily
else [
doc.width * 0.26,
doc.width * 0.15,
doc.width * 0.17,
doc.width * 0.14,
doc.width * 0.28,
]
),
)
story.extend([table, Spacer(1, 5 * mm)])