From 7bd60fd641234f815401c8a6d251e599726858fe Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Mon, 27 Apr 2026 21:14:02 +0330 Subject: [PATCH] fix(reports): localize and group exported income values --- apps/reports/services/export_i18n.py | 39 +++++++++++++++++++++++++++- apps/reports/services/exporters.py | 25 ++++++++++-------- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/apps/reports/services/export_i18n.py b/apps/reports/services/export_i18n.py index f4eefea..593fe14 100644 --- a/apps/reports/services/export_i18n.py +++ b/apps/reports/services/export_i18n.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import date +from decimal import Decimal, InvalidOperation from pathlib import Path from typing import Iterable @@ -89,6 +90,16 @@ PERIOD_LABELS = { }, } +CURRENCY_LABELS = { + "USD": {"en": "USD", "fa": "دلار آمریکا"}, + "EUR": {"en": "EUR", "fa": "یورو"}, + "GBP": {"en": "GBP", "fa": "پوند"}, + "IRR": {"en": "IRR", "fa": "ریال"}, + "IRT": {"en": "IRT", "fa": "تومان"}, + "AED": {"en": "AED", "fa": "درهم"}, + "TRY": {"en": "TRY", "fa": "لیر"}, +} + @dataclass(frozen=True) class ExportLocale: @@ -122,14 +133,40 @@ class ExportLocale: def format_duration(self, value: str, *, ascii_digits: bool = False) -> str: return self.format_number(value, ascii_digits=ascii_digits) + def format_amount(self, value: object, *, ascii_digits: bool = False) -> str: + raw = str(value).strip() + if not raw: + return raw + try: + decimal_value = Decimal(raw) + except InvalidOperation: + return self.format_number(raw, ascii_digits=ascii_digits) + + sign = "-" if decimal_value < 0 else "" + unsigned = abs(decimal_value) + normalized = format(unsigned, "f") + integer_part, _, fractional_part = normalized.partition(".") + grouped_integer = f"{int(integer_part):,}" + formatted = f"{sign}{grouped_integer}" + if fractional_part: + trimmed_fraction = fractional_part.rstrip("0") + if trimmed_fraction: + formatted = f"{formatted}.{trimmed_fraction}" + return self.format_number(formatted, ascii_digits=ascii_digits) + def format_money_label(self, income_totals: list[dict], *, ascii_digits: bool = False) -> str: if not income_totals: return "-" parts = [] for item in income_totals: - parts.append(f"{self.format_number(item['amount'], ascii_digits=ascii_digits)} {item['currency']}") + currency = self.currency_label(item["currency"]) + parts.append(f"{self.format_amount(item['amount'], ascii_digits=ascii_digits)} {currency}") return " | ".join(parts) + def currency_label(self, code: str | None) -> str: + raw = str(code or "").upper() + return CURRENCY_LABELS.get(raw, {}).get(self.language, raw) + def shape(self, text: object) -> str: raw = str(text) if not any(start <= ord(char) <= end for char in raw for start, end in ARABIC_RANGES): diff --git a/apps/reports/services/exporters.py b/apps/reports/services/exporters.py index 845598f..a4915e9 100644 --- a/apps/reports/services/exporters.py +++ b/apps/reports/services/exporters.py @@ -80,19 +80,19 @@ def _append_meta_block(worksheet, *, locale: ExportLocale, report_data: dict) -> scope = report_data["scope"] summary = report_data["summary"] - worksheet.append([locale.t("report_title"), scope["workspace"]["name"]]) - worksheet.append([locale.t("workspace"), scope["workspace"]["name"]]) - worksheet.append([locale.t("period"), locale.period_label(scope["period"])]) - worksheet.append([locale.t("from_date"), locale.format_date(scope["from_date"], ascii_digits=True)]) - worksheet.append([locale.t("to_date"), locale.format_date(scope["to_date"], ascii_digits=True)]) - worksheet.append([locale.t("user"), user_label(scope.get("user"), locale, ascii_digits=True)]) - worksheet.append([locale.t("generated_at"), locale.format_date(datetime.now().date(), ascii_digits=True)]) + worksheet.append(_rtl_row(locale, [locale.t("report_title"), scope["workspace"]["name"]])) + worksheet.append(_rtl_row(locale, [locale.t("workspace"), scope["workspace"]["name"]])) + 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("generated_at"), locale.format_date(datetime.now().date(), ascii_digits=True)])) worksheet.append([]) worksheet.append([locale.t("summary")]) - worksheet.append([locale.t("total_hours"), locale.format_duration(summary["total_duration"], ascii_digits=True)]) - worksheet.append([locale.t("billable_hours"), locale.format_duration(summary["billable_duration"], ascii_digits=True)]) - worksheet.append([locale.t("non_billable_hours"), locale.format_duration(summary["non_billable_duration"], ascii_digits=True)]) - worksheet.append([locale.t("income"), _money_label_excel(locale, summary["income_totals"])]) + worksheet.append(_rtl_row(locale, [locale.t("total_hours"), locale.format_duration(summary["total_duration"], ascii_digits=True)])) + worksheet.append(_rtl_row(locale, [locale.t("billable_hours"), locale.format_duration(summary["billable_duration"], ascii_digits=True)])) + worksheet.append(_rtl_row(locale, [locale.t("non_billable_hours"), locale.format_duration(summary["non_billable_duration"], ascii_digits=True)])) + worksheet.append(_rtl_row(locale, [locale.t("income"), _money_label_excel(locale, summary["income_totals"])])) for row_index in range(1, worksheet.max_row + 1): first_cell = worksheet.cell(row=row_index, column=1) @@ -196,6 +196,9 @@ def _append_breakdown_table(worksheet, *, locale: ExportLocale, title_key: str, def _render_excel_sheet(worksheet, *, locale: ExportLocale, report_data: dict) -> None: if locale.is_rtl: worksheet.sheet_view.rightToLeft = True + worksheet.freeze_panes = "E4" + else: + worksheet.freeze_panes = "A4" _append_meta_block(worksheet, locale=locale, report_data=report_data) _append_daily_table(worksheet, locale=locale, report_data=report_data) _append_breakdown_table(worksheet, locale=locale, title_key="clients", rows=report_data["clients"])