fix(reports): refine financial export summaries
This commit is contained in:
@@ -49,9 +49,13 @@ TRANSLATIONS = {
|
||||
"rate_history": "Hourly rate history",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"now": "Now",
|
||||
"project": "Project",
|
||||
"percentage": "Percentage",
|
||||
"hour_percentage": "Hour %",
|
||||
"income_percentage": "Income %",
|
||||
"multiple_rates": "Multiple rates - see details",
|
||||
"variable_rate": "Variable rate",
|
||||
"none": "None",
|
||||
"daily_summary": "Daily Summary",
|
||||
"clients": "Clients",
|
||||
@@ -93,9 +97,13 @@ TRANSLATIONS = {
|
||||
"rate_history": "\u062a\u0627\u0631\u06cc\u062e\u0686\u0647 \u0646\u0631\u062e \u0633\u0627\u0639\u062a\u06cc",
|
||||
"from": "\u0627\u0632",
|
||||
"to": "\u062a\u0627",
|
||||
"now": "\u062d\u0627\u0644",
|
||||
"project": "\u067e\u0631\u0648\u0698\u0647",
|
||||
"percentage": "\u062f\u0631\u0635\u062f",
|
||||
"hour_percentage": "\u062f\u0631\u0635\u062f \u0633\u0627\u0639\u062a",
|
||||
"income_percentage": "\u062f\u0631\u0635\u062f \u06a9\u0627\u0631\u06a9\u0631\u062f",
|
||||
"multiple_rates": "\u0686\u0646\u062f \u0646\u0631\u062e - \u062c\u0632\u0626\u06cc\u0627\u062a \u062f\u0631 \u06af\u0632\u0627\u0631\u0634 \u06a9\u0627\u0631\u0628\u0631",
|
||||
"variable_rate": "\u0646\u0631\u062e \u0645\u062a\u063a\u06cc\u0631",
|
||||
"none": "\u0628\u062f\u0648\u0646 \u0645\u0648\u0631\u062f",
|
||||
"daily_summary": "\u062e\u0644\u0627\u0635\u0647 \u0631\u0648\u0632\u0627\u0646\u0647",
|
||||
"clients": "\u0645\u0634\u062a\u0631\u06cc\u0627\u0646",
|
||||
@@ -140,6 +148,8 @@ CURRENCY_LABELS = {
|
||||
"TRY": {"en": "TRY", "fa": "\u0644\u06cc\u0631"},
|
||||
}
|
||||
|
||||
DECIMAL_TRIM_CURRENCIES = {"IRR", "IRT"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExportLocale:
|
||||
@@ -174,6 +184,15 @@ class ExportLocale:
|
||||
return self.format_number(value, ascii_digits=ascii_digits)
|
||||
|
||||
def format_amount(self, value: object, *, ascii_digits: bool = False) -> str:
|
||||
return self.format_amount_for_currency(value, None, ascii_digits=ascii_digits)
|
||||
|
||||
def format_amount_for_currency(
|
||||
self,
|
||||
value: object,
|
||||
currency: str | None,
|
||||
*,
|
||||
ascii_digits: bool = False,
|
||||
) -> str:
|
||||
raw = str(value).strip()
|
||||
if not raw:
|
||||
return raw
|
||||
@@ -189,7 +208,11 @@ class ExportLocale:
|
||||
grouped_integer = f"{int(integer_part):,}"
|
||||
formatted = f"{sign}{grouped_integer}"
|
||||
if fractional_part:
|
||||
trimmed_fraction = fractional_part.rstrip("0")
|
||||
trimmed_fraction = (
|
||||
""
|
||||
if str(currency or "").upper() in DECIMAL_TRIM_CURRENCIES
|
||||
else fractional_part.rstrip("0")
|
||||
)
|
||||
if trimmed_fraction:
|
||||
formatted = f"{formatted}.{trimmed_fraction}"
|
||||
return self.format_number(formatted, ascii_digits=ascii_digits)
|
||||
@@ -200,7 +223,9 @@ class ExportLocale:
|
||||
parts = []
|
||||
for item in income_totals:
|
||||
currency = self.currency_label(item["currency"])
|
||||
parts.append(f"{self.format_amount(item['amount'], ascii_digits=ascii_digits)} {currency}")
|
||||
parts.append(
|
||||
f"{self.format_amount_for_currency(item['amount'], item['currency'], ascii_digits=ascii_digits)} {currency}"
|
||||
)
|
||||
return " | ".join(parts)
|
||||
|
||||
def currency_label(self, code: str | None) -> str:
|
||||
|
||||
@@ -76,9 +76,9 @@ 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 locale.t("none")
|
||||
return "-"
|
||||
items = [
|
||||
f"{locale.format_amount(rate['amount'], ascii_digits=ascii_digits)} {locale.currency_label(rate['currency'])}"
|
||||
f"{locale.format_amount_for_currency(rate['amount'], rate['currency'], ascii_digits=ascii_digits)} {locale.currency_label(rate['currency'])}"
|
||||
for rate in rates
|
||||
]
|
||||
return ", ".join(items)
|
||||
@@ -96,7 +96,7 @@ def _percentages_label(locale: ExportLocale, rows: list[dict], *, ascii_digits:
|
||||
|
||||
def _rate_period_label(locale: ExportLocale, row: dict, *, ascii_digits: bool = False) -> str:
|
||||
return (
|
||||
f"{locale.format_amount(row['amount'], ascii_digits=ascii_digits)} "
|
||||
f"{locale.format_amount_for_currency(row['amount'], row['currency'], ascii_digits=ascii_digits)} "
|
||||
f"{locale.currency_label(row['currency'])}"
|
||||
)
|
||||
|
||||
@@ -104,16 +104,43 @@ 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 f"{locale.format_amount(rate['amount'])} {locale.currency_label(rate['currency'])}"
|
||||
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 "-"
|
||||
value = f"{locale.format_amount(rate['amount'], ascii_digits=True)} {locale.currency_label(rate['currency'])}"
|
||||
value = (
|
||||
f"{locale.format_amount_for_currency(rate['amount'], rate['currency'], ascii_digits=True)} "
|
||||
f"{locale.currency_label(rate['currency'])}"
|
||||
)
|
||||
return f"\u202B{value}\u202C" if locale.is_rtl else value
|
||||
|
||||
|
||||
def _pdf_summary_rate_label(locale: ExportLocale, rates: list[dict]) -> str:
|
||||
if len(rates) > 1:
|
||||
return locale.t("variable_rate")
|
||||
return _rates_label(locale, rates)
|
||||
|
||||
|
||||
def _summary_rate_rows(locale: ExportLocale, summary: dict) -> list[list[str]]:
|
||||
rate_periods = summary.get("rate_periods") or []
|
||||
if not rate_periods:
|
||||
return [[locale.t("none"), locale.t("none")]]
|
||||
if len(rate_periods) > 1:
|
||||
return [[locale.t("variable_rate"), _summary_period_label(locale, rate_periods, ascii_digits=True)]]
|
||||
row = rate_periods[0]
|
||||
return [
|
||||
[
|
||||
_rate_period_label(locale, row, ascii_digits=True),
|
||||
(
|
||||
f"{locale.format_date(row['from_date'], ascii_digits=True)} - "
|
||||
f"{locale.format_date(row['to_date'], ascii_digits=True)}"
|
||||
),
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def _section_headers(locale: ExportLocale) -> list[str]:
|
||||
headers = [
|
||||
locale.t("name"),
|
||||
@@ -227,12 +254,19 @@ def _summary_period_label(locale: ExportLocale, rate_periods: list[dict], *, asc
|
||||
|
||||
first_row = rate_periods[0]
|
||||
last_row = rate_periods[-1]
|
||||
last_to_date = last_row.get("to_date")
|
||||
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)}"
|
||||
f"{(_rate_to_label(locale, last_to_date, ascii_digits=ascii_digits))}"
|
||||
)
|
||||
|
||||
|
||||
def _rate_to_label(locale: ExportLocale, to_date: str | None, *, ascii_digits: bool = False) -> str:
|
||||
if not to_date:
|
||||
return locale.t("now")
|
||||
return locale.format_date(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)
|
||||
@@ -257,7 +291,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),
|
||||
locale.format_date(row["to_date"], ascii_digits=True),
|
||||
_rate_to_label(locale, row.get("to_date"), ascii_digits=True),
|
||||
],
|
||||
)
|
||||
)
|
||||
@@ -441,21 +475,27 @@ def _append_breakdown_table(
|
||||
rows: list[dict],
|
||||
hour_percentages: list[dict] | None = None,
|
||||
income_percentages: list[dict] | None = None,
|
||||
financial_only: bool = False,
|
||||
) -> None:
|
||||
worksheet.append([])
|
||||
_append_merged_heading(
|
||||
worksheet,
|
||||
locale=locale,
|
||||
title=locale.t(title_key),
|
||||
span=7 if hour_percentages is not None else 5,
|
||||
span=(
|
||||
5
|
||||
if hour_percentages is not None and financial_only
|
||||
else 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"),
|
||||
*( [] if financial_only else [locale.t("non_billable_hours"), locale.t("total_hours")] ),
|
||||
locale.t("income"),
|
||||
*( [locale.t("income_percentage")] if hour_percentages is not None else [] ),
|
||||
]
|
||||
@@ -477,8 +517,14 @@ def _append_breakdown_table(
|
||||
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),
|
||||
*(
|
||||
[]
|
||||
if financial_only
|
||||
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)]
|
||||
@@ -562,16 +608,7 @@ def _write_table_row(
|
||||
|
||||
|
||||
def _user_summary_row_payload(locale: ExportLocale, summary: dict) -> tuple[int, list[list[str | None]]]:
|
||||
rate_rows = [
|
||||
[
|
||||
_rate_period_label(locale, row, 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 [])
|
||||
]
|
||||
rate_rows = _summary_rate_rows(locale, summary)
|
||||
client_rows = _summary_breakdown_rows(
|
||||
locale,
|
||||
summary.get("client_percentages") or [],
|
||||
@@ -595,7 +632,6 @@ def _user_summary_row_payload(locale: ExportLocale, summary: dict) -> tuple[int,
|
||||
summary["user"]["name"] if index == 0 else None,
|
||||
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,
|
||||
*(rate_rows[index] if index < len(rate_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]),
|
||||
@@ -662,7 +698,7 @@ def _render_all_users_overall_excel_sheet(
|
||||
worksheet,
|
||||
row=15,
|
||||
start_col=1,
|
||||
end_col=16,
|
||||
end_col=15,
|
||||
value=locale.t("users_summary_sheet"),
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
@@ -670,7 +706,6 @@ def _render_all_users_overall_excel_sheet(
|
||||
locale.t("name"),
|
||||
locale.t("mobile"),
|
||||
locale.t("working_hours"),
|
||||
locale.t("non_working_hours"),
|
||||
locale.t("hourly_rate"),
|
||||
locale.t("period"),
|
||||
locale.t("income"),
|
||||
@@ -704,27 +739,27 @@ def _render_all_users_overall_excel_sheet(
|
||||
values=values,
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
for column in (1, 2, 3, 4, 7):
|
||||
for column in (1, 2, 3, 6):
|
||||
_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=4, value_present=True)
|
||||
_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)
|
||||
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)
|
||||
_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=10, value_present=True)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=11, value_present=True)
|
||||
_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=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)
|
||||
_merge_vertical_if_needed(worksheet, start_row=current_row, span=span, column=16, value_present=True)
|
||||
current_row += span
|
||||
|
||||
current_row += 2
|
||||
@@ -765,8 +800,6 @@ def _render_all_users_overall_excel_sheet(
|
||||
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"),
|
||||
],
|
||||
@@ -785,8 +818,6 @@ def _render_all_users_overall_excel_sheet(
|
||||
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, income_percentages or [], row, ascii_digits=True),
|
||||
],
|
||||
@@ -798,7 +829,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, None],
|
||||
values=[locale.t("no_data"), None, None, None, None],
|
||||
rtl=locale.is_rtl,
|
||||
)
|
||||
current_row += 1
|
||||
@@ -808,25 +839,30 @@ def _render_all_users_overall_excel_sheet(
|
||||
"A": 31.57,
|
||||
"B": 19.86,
|
||||
"C": 18.0,
|
||||
"D": 17.0,
|
||||
"E": 18.0,
|
||||
"F": 26.0,
|
||||
"G": 24.0,
|
||||
"H": 28.0,
|
||||
"I": 14.0,
|
||||
"J": 16.0,
|
||||
"K": 28.0,
|
||||
"L": 14.0,
|
||||
"M": 16.0,
|
||||
"N": 24.0,
|
||||
"O": 14.0,
|
||||
"P": 16.0,
|
||||
"D": 18.0,
|
||||
"E": 26.0,
|
||||
"F": 24.0,
|
||||
"G": 28.0,
|
||||
"H": 14.0,
|
||||
"I": 16.0,
|
||||
"J": 28.0,
|
||||
"K": 14.0,
|
||||
"L": 16.0,
|
||||
"M": 24.0,
|
||||
"N": 14.0,
|
||||
"O": 16.0,
|
||||
}
|
||||
for column, width in overall_widths.items():
|
||||
worksheet.column_dimensions[column].width = width
|
||||
|
||||
|
||||
def _render_excel_sheet(worksheet, *, locale: ExportLocale, report_data: dict) -> None:
|
||||
def _render_excel_sheet(
|
||||
worksheet,
|
||||
*,
|
||||
locale: ExportLocale,
|
||||
report_data: dict,
|
||||
financial_only_breakdowns: bool = False,
|
||||
) -> None:
|
||||
if locale.is_rtl:
|
||||
worksheet.sheet_view.rightToLeft = True
|
||||
worksheet.freeze_panes = "E4"
|
||||
@@ -860,6 +896,7 @@ def _render_excel_sheet(worksheet, *, locale: ExportLocale, report_data: dict) -
|
||||
if user_summary
|
||||
else report_data.get("client_income_percentages")
|
||||
),
|
||||
financial_only=financial_only_breakdowns,
|
||||
)
|
||||
_append_breakdown_table(
|
||||
worksheet,
|
||||
@@ -876,6 +913,7 @@ def _render_excel_sheet(worksheet, *, locale: ExportLocale, report_data: dict) -
|
||||
if user_summary
|
||||
else report_data.get("project_income_percentages")
|
||||
),
|
||||
financial_only=financial_only_breakdowns,
|
||||
)
|
||||
_append_breakdown_table(
|
||||
worksheet,
|
||||
@@ -892,6 +930,7 @@ def _render_excel_sheet(worksheet, *, locale: ExportLocale, report_data: dict) -
|
||||
if user_summary
|
||||
else report_data.get("tag_income_percentages")
|
||||
),
|
||||
financial_only=financial_only_breakdowns,
|
||||
)
|
||||
_autosize_columns(worksheet)
|
||||
|
||||
@@ -935,7 +974,12 @@ def build_excel_report(*, report_data: dict, locale: ExportLocale, per_user_repo
|
||||
used_titles,
|
||||
)
|
||||
worksheet = workbook.create_sheet(title=user_title)
|
||||
_render_excel_sheet(worksheet, locale=locale, report_data=user_report)
|
||||
_render_excel_sheet(
|
||||
worksheet,
|
||||
locale=locale,
|
||||
report_data=user_report,
|
||||
financial_only_breakdowns=True,
|
||||
)
|
||||
used_titles.add(user_title)
|
||||
else:
|
||||
overall_sheet = workbook.active
|
||||
@@ -1048,7 +1092,7 @@ def _build_pdf_rate_history_table(locale: ExportLocale, summary: dict, doc_width
|
||||
[
|
||||
_rate_period_label(locale, row),
|
||||
locale.format_date(row["from_date"]),
|
||||
locale.format_date(row["to_date"]),
|
||||
_rate_to_label(locale, row.get("to_date")),
|
||||
],
|
||||
)
|
||||
for row in rows
|
||||
@@ -1056,7 +1100,15 @@ def _build_pdf_rate_history_table(locale: ExportLocale, summary: dict, doc_width
|
||||
]
|
||||
if not rows:
|
||||
data.append(_rtl_row(locale, [locale.t("no_data"), "", ""]))
|
||||
return _styled_table(data, locale=locale, column_widths=[doc_width * 0.34, doc_width * 0.33, doc_width * 0.33])
|
||||
fixed_widths = [doc_width * 0.18, doc_width * 0.18]
|
||||
column_widths = [doc_width - sum(fixed_widths), *fixed_widths]
|
||||
if locale.is_rtl:
|
||||
column_widths = list(reversed(column_widths))
|
||||
return _styled_table(
|
||||
data,
|
||||
locale=locale,
|
||||
column_widths=column_widths,
|
||||
)
|
||||
|
||||
|
||||
def _build_pdf_percentage_table(locale: ExportLocale, rows: list[dict], doc_width: float) -> Table:
|
||||
@@ -1083,6 +1135,7 @@ def _append_pdf_report_sections(
|
||||
doc_width: float,
|
||||
section_style: ParagraphStyle,
|
||||
user_summary: dict | None = None,
|
||||
financial_only_breakdowns: bool = False,
|
||||
) -> None:
|
||||
sections = [
|
||||
("daily_summary", report_data["days"], True),
|
||||
@@ -1107,8 +1160,7 @@ def _append_pdf_report_sections(
|
||||
locale.t("name"),
|
||||
locale.t("billable_hours"),
|
||||
locale.t("hour_percentage"),
|
||||
locale.t("non_billable_hours"),
|
||||
locale.t("total_hours"),
|
||||
*( [] if financial_only_breakdowns else [locale.t("non_billable_hours"), locale.t("total_hours")] ),
|
||||
locale.t("income"),
|
||||
locale.t("income_percentage"),
|
||||
]
|
||||
@@ -1129,49 +1181,69 @@ def _append_pdf_report_sections(
|
||||
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"]),
|
||||
*(
|
||||
[]
|
||||
if financial_only_breakdowns
|
||||
else [
|
||||
locale.format_duration(row["non_billable_duration"]),
|
||||
locale.format_duration(row["total_duration"]),
|
||||
]
|
||||
),
|
||||
_money_label(locale, row["income_totals"]),
|
||||
_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"), "", "", *( [] if financial_only_breakdowns else ["", ""] ), "", ""],
|
||||
)
|
||||
]
|
||||
if is_daily:
|
||||
column_widths = [
|
||||
doc_width * 0.20,
|
||||
doc_width * 0.12,
|
||||
doc_width * 0.15,
|
||||
doc_width * 0.13,
|
||||
doc_width * 0.16,
|
||||
doc_width * 0.24,
|
||||
]
|
||||
elif hour_percentage_rows is not None:
|
||||
fixed_widths = (
|
||||
[
|
||||
doc_width * 0.11,
|
||||
doc_width * 0.11,
|
||||
doc_width * 0.19,
|
||||
doc_width * 0.15,
|
||||
]
|
||||
if financial_only_breakdowns
|
||||
else [
|
||||
doc_width * 0.11,
|
||||
doc_width * 0.11,
|
||||
doc_width * 0.12,
|
||||
doc_width * 0.12,
|
||||
doc_width * 0.19,
|
||||
doc_width * 0.15,
|
||||
]
|
||||
)
|
||||
column_widths = [doc_width - sum(fixed_widths), *fixed_widths]
|
||||
if locale.is_rtl:
|
||||
column_widths = list(reversed(column_widths))
|
||||
else:
|
||||
fixed_widths = [
|
||||
doc_width * 0.15,
|
||||
doc_width * 0.17,
|
||||
doc_width * 0.14,
|
||||
doc_width * 0.28,
|
||||
]
|
||||
column_widths = [doc_width - sum(fixed_widths), *fixed_widths]
|
||||
if locale.is_rtl:
|
||||
column_widths = list(reversed(column_widths))
|
||||
table = _styled_table(
|
||||
[header, *body_rows],
|
||||
locale=locale,
|
||||
column_widths=(
|
||||
[
|
||||
doc_width * 0.20,
|
||||
doc_width * 0.12,
|
||||
doc_width * 0.15,
|
||||
doc_width * 0.13,
|
||||
doc_width * 0.16,
|
||||
doc_width * 0.24,
|
||||
]
|
||||
if is_daily
|
||||
else [
|
||||
*(
|
||||
[
|
||||
doc_width * 0.20,
|
||||
doc_width * 0.11,
|
||||
doc_width * 0.11,
|
||||
doc_width * 0.12,
|
||||
doc_width * 0.12,
|
||||
doc_width * 0.19,
|
||||
doc_width * 0.15,
|
||||
]
|
||||
if hour_percentage_rows is not None
|
||||
else [
|
||||
doc_width * 0.13,
|
||||
doc_width * 0.15,
|
||||
doc_width * 0.17,
|
||||
doc_width * 0.14,
|
||||
doc_width * 0.28,
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
column_widths=column_widths,
|
||||
)
|
||||
story.extend([table, Spacer(1, 5 * mm)])
|
||||
|
||||
@@ -1313,9 +1385,7 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale, per_user_report
|
||||
locale.t("name"),
|
||||
locale.t("mobile"),
|
||||
locale.t("working_hours"),
|
||||
locale.t("non_working_hours"),
|
||||
locale.t("hourly_rate"),
|
||||
locale.t("period"),
|
||||
locale.t("income"),
|
||||
],
|
||||
)
|
||||
@@ -1326,9 +1396,7 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale, per_user_report
|
||||
summary["user"]["name"],
|
||||
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 []),
|
||||
_pdf_summary_rate_label(locale, summary.get("hourly_rates") or []),
|
||||
_money_label(locale, summary["income_totals"]),
|
||||
],
|
||||
)
|
||||
@@ -1339,13 +1407,11 @@ 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.18,
|
||||
doc.width * 0.13,
|
||||
doc.width * 0.13,
|
||||
doc.width * 0.13,
|
||||
doc.width * 0.13,
|
||||
doc.width * 0.25,
|
||||
doc.width * 0.16,
|
||||
doc.width * 0.14,
|
||||
doc.width * 0.16,
|
||||
doc.width * 0.19,
|
||||
doc.width * 0.24,
|
||||
],
|
||||
)
|
||||
)
|
||||
@@ -1379,6 +1445,7 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale, per_user_report
|
||||
doc_width=doc.width,
|
||||
section_style=section_style,
|
||||
user_summary=user_report.get("user_summary"),
|
||||
financial_only_breakdowns=True,
|
||||
)
|
||||
else:
|
||||
_append_pdf_report_sections(
|
||||
@@ -1388,6 +1455,7 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale, per_user_report
|
||||
doc_width=doc.width,
|
||||
section_style=section_style,
|
||||
user_summary=report_data.get("user_summary"),
|
||||
financial_only_breakdowns=False,
|
||||
)
|
||||
|
||||
doc.build(story)
|
||||
|
||||
Reference in New Issue
Block a user