feat(reports): add uncategorized dual-share exports
This commit is contained in:
@@ -173,7 +173,13 @@ def _append_user_summary_block(worksheet, *, locale: ExportLocale, user_summary:
|
||||
[locale.t("working_hours"), locale.format_duration(user_summary["billable_duration"], ascii_digits=True)],
|
||||
),
|
||||
_excel_pair_row(
|
||||
[locale.t("non_working_hours"), locale.format_duration(user_summary["non_billable_duration"], ascii_digits=True)],
|
||||
[
|
||||
locale.t("non_working_hours"),
|
||||
locale.format_duration(
|
||||
user_summary["non_billable_duration"],
|
||||
ascii_digits=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
_excel_pair_row([locale.t("income"), _money_label_excel(locale, user_summary["income_totals"])]),
|
||||
):
|
||||
@@ -184,14 +190,49 @@ def _percentage_display(locale: ExportLocale, rows: list[dict], row_data: dict,
|
||||
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:
|
||||
value = f"{locale.format_amount(row['percentage'], ascii_digits=ascii_digits)}%"
|
||||
value = _percentage_value(locale, row["percentage"], ascii_digits=ascii_digits)
|
||||
if row_id is not None and str(row["id"]) == row_id:
|
||||
return f"\u202B{value}\u202C" if locale.is_rtl and ascii_digits else value
|
||||
return value
|
||||
if row_name and row["name"] == row_name:
|
||||
return f"\u202B{value}\u202C" if locale.is_rtl and ascii_digits else value
|
||||
return value
|
||||
return "-"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _summary_breakdown_rows(
|
||||
locale: ExportLocale,
|
||||
hour_rows: list[dict],
|
||||
income_rows: list[dict],
|
||||
) -> list[list[str]]:
|
||||
if not hour_rows:
|
||||
return []
|
||||
|
||||
return [
|
||||
[
|
||||
row["name"],
|
||||
_percentage_value(locale, row["percentage"], ascii_digits=True),
|
||||
_percentage_display(locale, income_rows, row, ascii_digits=True),
|
||||
]
|
||||
for row in hour_rows
|
||||
]
|
||||
|
||||
|
||||
def _summary_period_label(locale: ExportLocale, rate_periods: list[dict], *, ascii_digits: bool = False) -> str:
|
||||
if not rate_periods:
|
||||
return locale.t("none")
|
||||
|
||||
first_row = rate_periods[0]
|
||||
last_row = rate_periods[-1]
|
||||
return (
|
||||
f"{locale.format_date(first_row['from_date'], ascii_digits=ascii_digits)} - "
|
||||
f"{locale.format_date(last_row['to_date'], ascii_digits=ascii_digits)}"
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
@@ -252,8 +293,16 @@ def _append_meta_block(worksheet, *, locale: ExportLocale, report_data: dict) ->
|
||||
worksheet.append(_excel_pair_row([locale.t("report_title"), scope["workspace"]["name"]]))
|
||||
worksheet.append(_excel_pair_row([locale.t("workspace"), scope["workspace"]["name"]]))
|
||||
worksheet.append(_excel_pair_row([locale.t("period"), locale.period_label(scope["period"])]))
|
||||
worksheet.append(_excel_pair_row([locale.t("from_date"), locale.format_date(scope["from_date"], ascii_digits=True)]))
|
||||
worksheet.append(_excel_pair_row([locale.t("to_date"), locale.format_date(scope["to_date"], ascii_digits=True)]))
|
||||
worksheet.append(
|
||||
_excel_pair_row(
|
||||
[locale.t("from_date"), locale.format_date(scope["from_date"], ascii_digits=True)],
|
||||
)
|
||||
)
|
||||
worksheet.append(
|
||||
_excel_pair_row(
|
||||
[locale.t("to_date"), locale.format_date(scope["to_date"], ascii_digits=True)],
|
||||
)
|
||||
)
|
||||
worksheet.append(
|
||||
_excel_pair_row(
|
||||
[
|
||||
@@ -266,16 +315,48 @@ def _append_meta_block(worksheet, *, locale: ExportLocale, report_data: dict) ->
|
||||
_excel_pair_row(
|
||||
[
|
||||
locale.t("mobile"),
|
||||
locale.format_number(scope["user"]["mobile"], ascii_digits=True) if scope.get("user") and scope["user"].get("mobile") else "-",
|
||||
(
|
||||
locale.format_number(
|
||||
scope["user"]["mobile"],
|
||||
ascii_digits=True,
|
||||
)
|
||||
if scope.get("user") and scope["user"].get("mobile")
|
||||
else "-"
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
worksheet.append(
|
||||
_excel_pair_row(
|
||||
[
|
||||
locale.t("generated_at"),
|
||||
locale.format_date(datetime.now().date(), ascii_digits=True),
|
||||
],
|
||||
)
|
||||
)
|
||||
worksheet.append(_excel_pair_row([locale.t("generated_at"), locale.format_date(datetime.now().date(), ascii_digits=True)]))
|
||||
worksheet.append([])
|
||||
_append_merged_heading(worksheet, locale=locale, title=locale.t("summary"), span=2)
|
||||
worksheet.append(_excel_pair_row([locale.t("total_hours"), locale.format_duration(summary["total_duration"], ascii_digits=True)]))
|
||||
worksheet.append(_excel_pair_row([locale.t("billable_hours"), locale.format_duration(summary["billable_duration"], ascii_digits=True)]))
|
||||
worksheet.append(_excel_pair_row([locale.t("non_billable_hours"), locale.format_duration(summary["non_billable_duration"], ascii_digits=True)]))
|
||||
worksheet.append(
|
||||
_excel_pair_row(
|
||||
[locale.t("total_hours"), locale.format_duration(summary["total_duration"], ascii_digits=True)],
|
||||
)
|
||||
)
|
||||
worksheet.append(
|
||||
_excel_pair_row(
|
||||
[locale.t("billable_hours"), locale.format_duration(summary["billable_duration"], ascii_digits=True)],
|
||||
)
|
||||
)
|
||||
worksheet.append(
|
||||
_excel_pair_row(
|
||||
[
|
||||
locale.t("non_billable_hours"),
|
||||
locale.format_duration(
|
||||
summary["non_billable_duration"],
|
||||
ascii_digits=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
worksheet.append(_excel_pair_row([locale.t("income"), _money_label_excel(locale, summary["income_totals"])]))
|
||||
|
||||
for row_index in range(1, worksheet.max_row + 1):
|
||||
@@ -284,7 +365,12 @@ def _append_meta_block(worksheet, *, locale: ExportLocale, report_data: dict) ->
|
||||
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)
|
||||
_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=None, rtl=locale.is_rtl)
|
||||
if second_cell.value:
|
||||
@@ -353,25 +439,26 @@ def _append_breakdown_table(
|
||||
locale: ExportLocale,
|
||||
title_key: str,
|
||||
rows: list[dict],
|
||||
percentages: list[dict] | None = None,
|
||||
hour_percentages: list[dict] | None = None,
|
||||
income_percentages: list[dict] | None = None,
|
||||
) -> None:
|
||||
worksheet.append([])
|
||||
_append_merged_heading(
|
||||
worksheet,
|
||||
locale=locale,
|
||||
title=locale.t(title_key),
|
||||
span=6 if percentages is not None else 5,
|
||||
span=7 if hour_percentages is not None else 5,
|
||||
)
|
||||
header_row = worksheet.max_row + 1
|
||||
headers = [
|
||||
locale.t("name"),
|
||||
locale.t("billable_hours"),
|
||||
*( [locale.t("hour_percentage")] if hour_percentages is not None else [] ),
|
||||
locale.t("non_billable_hours"),
|
||||
locale.t("total_hours"),
|
||||
locale.t("income"),
|
||||
*( [locale.t("income_percentage")] if hour_percentages is not None else [] ),
|
||||
]
|
||||
if percentages is not None:
|
||||
headers.append(locale.t("percentage"))
|
||||
worksheet.append(_excel_table_row(headers))
|
||||
for cell in worksheet[header_row]:
|
||||
_apply_cell_style(cell, bold=True, fill=HEADER_FILL, rtl=locale.is_rtl)
|
||||
@@ -385,15 +472,21 @@ def _append_breakdown_table(
|
||||
values = [
|
||||
row["name"],
|
||||
locale.format_duration(row["billable_duration"], ascii_digits=True),
|
||||
*(
|
||||
[_percentage_display(locale, hour_percentages or [], row, ascii_digits=True)]
|
||||
if hour_percentages is not None
|
||||
else []
|
||||
),
|
||||
locale.format_duration(row["non_billable_duration"], ascii_digits=True),
|
||||
locale.format_duration(row["total_duration"], ascii_digits=True),
|
||||
_money_label_excel(locale, row["income_totals"]),
|
||||
*(
|
||||
[_percentage_display(locale, income_percentages or [], row, ascii_digits=True)]
|
||||
if hour_percentages is not None
|
||||
else []
|
||||
),
|
||||
]
|
||||
if percentages is not None:
|
||||
values.append(_percentage_display(locale, percentages, row, ascii_digits=True))
|
||||
worksheet.append(
|
||||
_excel_table_row(values)
|
||||
)
|
||||
worksheet.append(_excel_table_row(values))
|
||||
for cell in worksheet[worksheet.max_row]:
|
||||
_apply_cell_style(cell, rtl=locale.is_rtl)
|
||||
|
||||
@@ -415,21 +508,24 @@ def _append_user_details_block_excel(
|
||||
locale=locale,
|
||||
title_key="clients",
|
||||
rows=report_data["clients"],
|
||||
percentages=user_summary["client_percentages"],
|
||||
hour_percentages=user_summary["client_percentages"],
|
||||
income_percentages=user_summary["client_income_percentages"],
|
||||
)
|
||||
_append_breakdown_table(
|
||||
worksheet,
|
||||
locale=locale,
|
||||
title_key="projects",
|
||||
rows=report_data["projects"],
|
||||
percentages=user_summary["project_percentages"],
|
||||
hour_percentages=user_summary["project_percentages"],
|
||||
income_percentages=user_summary["project_income_percentages"],
|
||||
)
|
||||
_append_breakdown_table(
|
||||
worksheet,
|
||||
locale=locale,
|
||||
title_key="tags",
|
||||
rows=report_data["tags"],
|
||||
percentages=user_summary["tag_percentages"],
|
||||
hour_percentages=user_summary["tag_percentages"],
|
||||
income_percentages=user_summary["tag_income_percentages"],
|
||||
)
|
||||
|
||||
|
||||
@@ -469,22 +565,28 @@ def _user_summary_row_payload(locale: ExportLocale, summary: dict) -> tuple[int,
|
||||
rate_rows = [
|
||||
[
|
||||
_rate_period_label(locale, row, ascii_digits=True),
|
||||
f"{locale.format_date(row['from_date'], ascii_digits=True)} - {locale.format_date(row['to_date'], ascii_digits=True)}",
|
||||
(
|
||||
f"{locale.format_date(row['from_date'], ascii_digits=True)} - "
|
||||
f"{locale.format_date(row['to_date'], ascii_digits=True)}"
|
||||
),
|
||||
]
|
||||
for row in (summary.get("rate_periods") or [])
|
||||
]
|
||||
client_rows = [
|
||||
[row["name"], f"\u202B{locale.format_amount(row['percentage'], ascii_digits=True)}%\u202C" if locale.is_rtl else f"{locale.format_amount(row['percentage'], ascii_digits=True)}%"]
|
||||
for row in (summary.get("client_percentages") or [])
|
||||
]
|
||||
project_rows = [
|
||||
[row["name"], f"\u202B{locale.format_amount(row['percentage'], ascii_digits=True)}%\u202C" if locale.is_rtl else f"{locale.format_amount(row['percentage'], ascii_digits=True)}%"]
|
||||
for row in (summary.get("project_percentages") or [])
|
||||
]
|
||||
tag_rows = [
|
||||
[row["name"], f"\u202B{locale.format_amount(row['percentage'], ascii_digits=True)}%\u202C" if locale.is_rtl else f"{locale.format_amount(row['percentage'], ascii_digits=True)}%"]
|
||||
for row in (summary.get("tag_percentages") or [])
|
||||
]
|
||||
client_rows = _summary_breakdown_rows(
|
||||
locale,
|
||||
summary.get("client_percentages") or [],
|
||||
summary.get("client_income_percentages") or [],
|
||||
)
|
||||
project_rows = _summary_breakdown_rows(
|
||||
locale,
|
||||
summary.get("project_percentages") or [],
|
||||
summary.get("project_income_percentages") or [],
|
||||
)
|
||||
tag_rows = _summary_breakdown_rows(
|
||||
locale,
|
||||
summary.get("tag_percentages") or [],
|
||||
summary.get("tag_income_percentages") or [],
|
||||
)
|
||||
span = max(len(rate_rows), len(client_rows), len(project_rows), len(tag_rows), 1)
|
||||
rows: list[list[str | None]] = []
|
||||
for index in range(span):
|
||||
@@ -494,11 +596,11 @@ def _user_summary_row_payload(locale: ExportLocale, summary: dict) -> tuple[int,
|
||||
locale.format_number(summary["user"]["mobile"], ascii_digits=True) if index == 0 else None,
|
||||
locale.format_duration(summary["billable_duration"], ascii_digits=True) if index == 0 else None,
|
||||
locale.format_duration(summary["non_billable_duration"], ascii_digits=True) if index == 0 else None,
|
||||
_money_label_excel(locale, summary["income_totals"]) if index == 0 else None,
|
||||
*(rate_rows[index] if index < len(rate_rows) else [None, None]),
|
||||
*(client_rows[index] if index < len(client_rows) else [None, None]),
|
||||
*(project_rows[index] if index < len(project_rows) else [None, None]),
|
||||
*(tag_rows[index] if index < len(tag_rows) else [None, None]),
|
||||
_money_label_excel(locale, summary["income_totals"]) if index == 0 else None,
|
||||
*(client_rows[index] if index < len(client_rows) else [None, None, None]),
|
||||
*(project_rows[index] if index < len(project_rows) else [None, None, None]),
|
||||
*(tag_rows[index] if index < len(tag_rows) else [None, None, None]),
|
||||
],
|
||||
)
|
||||
return span, rows
|
||||
@@ -560,7 +662,7 @@ def _render_all_users_overall_excel_sheet(
|
||||
worksheet,
|
||||
row=15,
|
||||
start_col=1,
|
||||
end_col=13,
|
||||
end_col=16,
|
||||
value=locale.t("users_summary_sheet"),
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
@@ -569,15 +671,18 @@ def _render_all_users_overall_excel_sheet(
|
||||
locale.t("mobile"),
|
||||
locale.t("working_hours"),
|
||||
locale.t("non_working_hours"),
|
||||
locale.t("income"),
|
||||
locale.t("hourly_rate"),
|
||||
locale.t("period"),
|
||||
locale.t("income"),
|
||||
locale.t("clients"),
|
||||
locale.t("percentage"),
|
||||
locale.t("hour_percentage"),
|
||||
locale.t("income_percentage"),
|
||||
locale.t("projects"),
|
||||
locale.t("percentage"),
|
||||
locale.t("hour_percentage"),
|
||||
locale.t("income_percentage"),
|
||||
locale.t("tags"),
|
||||
locale.t("percentage"),
|
||||
locale.t("hour_percentage"),
|
||||
locale.t("income_percentage"),
|
||||
]
|
||||
_write_table_row(
|
||||
worksheet,
|
||||
@@ -599,33 +704,58 @@ def _render_all_users_overall_excel_sheet(
|
||||
values=values,
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
for column in range(1, 6):
|
||||
for column in (1, 2, 3, 4, 7):
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=column)
|
||||
rate_rows = user_summary.get("rate_periods") or []
|
||||
client_rows = user_summary.get("client_percentages") or []
|
||||
project_rows = user_summary.get("project_percentages") or []
|
||||
tag_rows = user_summary.get("tag_percentages") or []
|
||||
if len(rate_rows) == 1:
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=5, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=6, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=7, value_present=True)
|
||||
if len(client_rows) == 1:
|
||||
_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)
|
||||
if len(project_rows) == 1:
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=11, value_present=True)
|
||||
if len(tag_rows) == 1:
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=12, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=13, value_present=True)
|
||||
if len(tag_rows) == 1:
|
||||
_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)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=16, value_present=True)
|
||||
current_row += span
|
||||
|
||||
current_row += 2
|
||||
for title_key, rows, percentages in (
|
||||
("clients", report_data["clients"], report_data.get("client_percentages")),
|
||||
("projects", report_data["projects"], report_data.get("project_percentages")),
|
||||
("tags", report_data["tags"], report_data.get("tag_percentages")),
|
||||
for title_key, rows, hour_percentages, income_percentages in (
|
||||
(
|
||||
"clients",
|
||||
report_data["clients"],
|
||||
report_data.get("client_percentages"),
|
||||
report_data.get("client_income_percentages"),
|
||||
),
|
||||
(
|
||||
"projects",
|
||||
report_data["projects"],
|
||||
report_data.get("project_percentages"),
|
||||
report_data.get("project_income_percentages"),
|
||||
),
|
||||
(
|
||||
"tags",
|
||||
report_data["tags"],
|
||||
report_data.get("tag_percentages"),
|
||||
report_data.get("tag_income_percentages"),
|
||||
),
|
||||
):
|
||||
_merge_and_style(worksheet, row=current_row, start_col=1, end_col=6, value=locale.t(title_key), rtl=locale.is_rtl)
|
||||
_merge_and_style(
|
||||
worksheet,
|
||||
row=current_row,
|
||||
start_col=1,
|
||||
end_col=7,
|
||||
value=locale.t(title_key),
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
current_row += 1
|
||||
_write_table_row(
|
||||
worksheet,
|
||||
@@ -634,10 +764,11 @@ def _render_all_users_overall_excel_sheet(
|
||||
values=[
|
||||
locale.t("name"),
|
||||
locale.t("billable_hours"),
|
||||
locale.t("hour_percentage"),
|
||||
locale.t("non_billable_hours"),
|
||||
locale.t("total_hours"),
|
||||
locale.t("income"),
|
||||
locale.t("percentage"),
|
||||
locale.t("income_percentage"),
|
||||
],
|
||||
rtl=locale.is_rtl,
|
||||
bold=True,
|
||||
@@ -653,10 +784,11 @@ def _render_all_users_overall_excel_sheet(
|
||||
values=[
|
||||
row["name"],
|
||||
locale.format_duration(row["billable_duration"], ascii_digits=True),
|
||||
_percentage_display(locale, hour_percentages or [], row, ascii_digits=True),
|
||||
locale.format_duration(row["non_billable_duration"], ascii_digits=True),
|
||||
locale.format_duration(row["total_duration"], ascii_digits=True),
|
||||
_money_label_excel(locale, row["income_totals"]),
|
||||
_percentage_display(locale, percentages or [], row, ascii_digits=True),
|
||||
_percentage_display(locale, income_percentages or [], row, ascii_digits=True),
|
||||
],
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
@@ -666,7 +798,7 @@ def _render_all_users_overall_excel_sheet(
|
||||
worksheet,
|
||||
row=current_row,
|
||||
start_col=1,
|
||||
values=[locale.t("no_data"), None, None, None, None, None],
|
||||
values=[locale.t("no_data"), None, None, None, None, None, None],
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
current_row += 1
|
||||
@@ -677,15 +809,18 @@ def _render_all_users_overall_excel_sheet(
|
||||
"B": 19.86,
|
||||
"C": 18.0,
|
||||
"D": 17.0,
|
||||
"E": 24.0,
|
||||
"F": 17.57,
|
||||
"G": 32.0,
|
||||
"H": 30.0,
|
||||
"E": 18.0,
|
||||
"F": 26.0,
|
||||
"G": 24.0,
|
||||
"H": 28.0,
|
||||
"I": 14.0,
|
||||
"J": 32.86,
|
||||
"K": 12.0,
|
||||
"L": 22.0,
|
||||
"M": 12.0,
|
||||
"J": 16.0,
|
||||
"K": 28.0,
|
||||
"L": 14.0,
|
||||
"M": 16.0,
|
||||
"N": 24.0,
|
||||
"O": 14.0,
|
||||
"P": 16.0,
|
||||
}
|
||||
for column, width in overall_widths.items():
|
||||
worksheet.column_dimensions[column].width = width
|
||||
@@ -715,21 +850,48 @@ def _render_excel_sheet(worksheet, *, locale: ExportLocale, report_data: dict) -
|
||||
locale=locale,
|
||||
title_key="clients",
|
||||
rows=report_data["clients"],
|
||||
percentages=user_summary["client_percentages"] if user_summary else None,
|
||||
hour_percentages=(
|
||||
user_summary["client_percentages"]
|
||||
if user_summary
|
||||
else report_data.get("client_percentages")
|
||||
),
|
||||
income_percentages=(
|
||||
user_summary["client_income_percentages"]
|
||||
if user_summary
|
||||
else report_data.get("client_income_percentages")
|
||||
),
|
||||
)
|
||||
_append_breakdown_table(
|
||||
worksheet,
|
||||
locale=locale,
|
||||
title_key="projects",
|
||||
rows=report_data["projects"],
|
||||
percentages=user_summary["project_percentages"] if user_summary else None,
|
||||
hour_percentages=(
|
||||
user_summary["project_percentages"]
|
||||
if user_summary
|
||||
else report_data.get("project_percentages")
|
||||
),
|
||||
income_percentages=(
|
||||
user_summary["project_income_percentages"]
|
||||
if user_summary
|
||||
else report_data.get("project_income_percentages")
|
||||
),
|
||||
)
|
||||
_append_breakdown_table(
|
||||
worksheet,
|
||||
locale=locale,
|
||||
title_key="tags",
|
||||
rows=report_data["tags"],
|
||||
percentages=user_summary["tag_percentages"] if user_summary else None,
|
||||
hour_percentages=(
|
||||
user_summary["tag_percentages"]
|
||||
if user_summary
|
||||
else report_data.get("tag_percentages")
|
||||
),
|
||||
income_percentages=(
|
||||
user_summary["tag_income_percentages"]
|
||||
if user_summary
|
||||
else report_data.get("tag_income_percentages")
|
||||
),
|
||||
)
|
||||
_autosize_columns(worksheet)
|
||||
|
||||
@@ -764,7 +926,14 @@ def build_excel_report(*, report_data: dict, locale: ExportLocale, per_user_repo
|
||||
_render_all_users_overall_excel_sheet(overall_sheet, locale=locale, report_data=report_data)
|
||||
used_titles.add(overall_sheet.title)
|
||||
for user_report in per_user_reports:
|
||||
user_title = safe_sheet_title(user_label(user_report["scope"].get("user"), locale, ascii_digits=True), used_titles)
|
||||
user_title = safe_sheet_title(
|
||||
user_label(
|
||||
user_report["scope"].get("user"),
|
||||
locale,
|
||||
ascii_digits=True,
|
||||
),
|
||||
used_titles,
|
||||
)
|
||||
worksheet = workbook.create_sheet(title=user_title)
|
||||
_render_excel_sheet(worksheet, locale=locale, report_data=user_report)
|
||||
used_titles.add(user_title)
|
||||
@@ -924,61 +1093,77 @@ def _append_pdf_report_sections(
|
||||
for title_key, rows, is_daily in sections:
|
||||
story.append(_paragraph(locale.t(title_key), section_style, locale))
|
||||
story.append(Spacer(1, 2 * mm))
|
||||
header_values = [
|
||||
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"),
|
||||
]
|
||||
percentage_rows = None
|
||||
header_values = (
|
||||
[
|
||||
locale.t("date"),
|
||||
locale.t("billable_hours"),
|
||||
locale.t("non_billable_hours"),
|
||||
locale.t("total_hours"),
|
||||
locale.t("hourly_rate"),
|
||||
locale.t("income"),
|
||||
]
|
||||
if is_daily
|
||||
else [
|
||||
locale.t("name"),
|
||||
locale.t("billable_hours"),
|
||||
locale.t("hour_percentage"),
|
||||
locale.t("non_billable_hours"),
|
||||
locale.t("total_hours"),
|
||||
locale.t("income"),
|
||||
locale.t("income_percentage"),
|
||||
]
|
||||
)
|
||||
hour_percentage_rows = None
|
||||
income_percentage_rows = None
|
||||
if user_summary and not is_daily:
|
||||
percentage_rows = user_summary[f"{title_key[:-1]}_percentages"] if title_key != "clients" else user_summary["client_percentages"]
|
||||
header_values.append(locale.t("percentage"))
|
||||
prefix = title_key[:-1] if title_key != "clients" else "client"
|
||||
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)
|
||||
if percentage_rows is not None:
|
||||
if hour_percentage_rows is not None:
|
||||
body_rows = [
|
||||
_rtl_row(
|
||||
locale,
|
||||
[
|
||||
row["name"],
|
||||
locale.format_duration(row["billable_duration"]),
|
||||
_percentage_display(locale, hour_percentage_rows, row),
|
||||
locale.format_duration(row["non_billable_duration"]),
|
||||
locale.format_duration(row["total_duration"]),
|
||||
_money_label(locale, row["income_totals"]),
|
||||
_percentage_display(locale, percentage_rows, row),
|
||||
_percentage_display(locale, income_percentage_rows or [], row),
|
||||
],
|
||||
)
|
||||
for row in rows
|
||||
] or [_rtl_row(locale, [locale.t("no_data"), "", "", "", "", ""])]
|
||||
] or [_rtl_row(locale, [locale.t("no_data"), "", "", "", "", "", ""])]
|
||||
table = _styled_table(
|
||||
[header, *body_rows],
|
||||
locale=locale,
|
||||
column_widths=(
|
||||
[
|
||||
doc_width * 0.21,
|
||||
doc_width * 0.13,
|
||||
doc_width * 0.20,
|
||||
doc_width * 0.12,
|
||||
doc_width * 0.15,
|
||||
doc_width * 0.13,
|
||||
doc_width * 0.16,
|
||||
doc_width * 0.22,
|
||||
doc_width * 0.24,
|
||||
]
|
||||
if is_daily
|
||||
else [
|
||||
*(
|
||||
[
|
||||
doc_width * 0.24,
|
||||
doc_width * 0.13,
|
||||
doc_width * 0.15,
|
||||
doc_width * 0.20,
|
||||
doc_width * 0.11,
|
||||
doc_width * 0.11,
|
||||
doc_width * 0.12,
|
||||
doc_width * 0.2,
|
||||
doc_width * 0.16,
|
||||
doc_width * 0.12,
|
||||
doc_width * 0.19,
|
||||
doc_width * 0.15,
|
||||
]
|
||||
if percentage_rows is not None
|
||||
if hour_percentage_rows is not None
|
||||
else [
|
||||
doc_width * 0.26,
|
||||
doc_width * 0.13,
|
||||
doc_width * 0.15,
|
||||
doc_width * 0.17,
|
||||
doc_width * 0.14,
|
||||
@@ -1063,7 +1248,14 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale, per_user_report
|
||||
[locale.t("from_date"), locale.format_date(scope["from_date"])],
|
||||
[locale.t("to_date"), locale.format_date(scope["to_date"])],
|
||||
[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("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:
|
||||
@@ -1122,6 +1314,8 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale, per_user_report
|
||||
locale.t("mobile"),
|
||||
locale.t("working_hours"),
|
||||
locale.t("non_working_hours"),
|
||||
locale.t("hourly_rate"),
|
||||
locale.t("period"),
|
||||
locale.t("income"),
|
||||
],
|
||||
)
|
||||
@@ -1133,6 +1327,8 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale, per_user_report
|
||||
locale.format_number(summary["user"]["mobile"]),
|
||||
locale.format_duration(summary["billable_duration"]),
|
||||
locale.format_duration(summary["non_billable_duration"]),
|
||||
_rates_label(locale, summary.get("hourly_rates") or []),
|
||||
_summary_period_label(locale, summary.get("rate_periods") or []),
|
||||
_money_label(locale, summary["income_totals"]),
|
||||
],
|
||||
)
|
||||
@@ -1143,11 +1339,13 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale, per_user_report
|
||||
[user_summary_header, *user_summary_rows],
|
||||
locale=locale,
|
||||
column_widths=[
|
||||
doc.width * 0.24,
|
||||
doc.width * 0.18,
|
||||
doc.width * 0.18,
|
||||
doc.width * 0.18,
|
||||
doc.width * 0.22,
|
||||
doc.width * 0.13,
|
||||
doc.width * 0.13,
|
||||
doc.width * 0.13,
|
||||
doc.width * 0.13,
|
||||
doc.width * 0.16,
|
||||
doc.width * 0.14,
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user