feat(reports): add daily rate to report tables and exports
This commit is contained in:
@@ -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)])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user