fix(reports): localize and group exported income values

This commit is contained in:
2026-04-27 21:14:02 +03:30
parent 208e81139b
commit 7bd60fd641
2 changed files with 52 additions and 12 deletions

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date from datetime import date
from decimal import Decimal, InvalidOperation
from pathlib import Path from pathlib import Path
from typing import Iterable 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) @dataclass(frozen=True)
class ExportLocale: class ExportLocale:
@@ -122,14 +133,40 @@ class ExportLocale:
def format_duration(self, value: str, *, ascii_digits: bool = False) -> str: def format_duration(self, value: str, *, ascii_digits: bool = False) -> str:
return self.format_number(value, ascii_digits=ascii_digits) 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: def format_money_label(self, income_totals: list[dict], *, ascii_digits: bool = False) -> str:
if not income_totals: if not income_totals:
return "-" return "-"
parts = [] parts = []
for item in income_totals: 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) 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: def shape(self, text: object) -> str:
raw = str(text) raw = str(text)
if not any(start <= ord(char) <= end for char in raw for start, end in ARABIC_RANGES): if not any(start <= ord(char) <= end for char in raw for start, end in ARABIC_RANGES):

View File

@@ -80,19 +80,19 @@ def _append_meta_block(worksheet, *, locale: ExportLocale, report_data: dict) ->
scope = report_data["scope"] scope = report_data["scope"]
summary = report_data["summary"] summary = report_data["summary"]
worksheet.append([locale.t("report_title"), scope["workspace"]["name"]]) worksheet.append(_rtl_row(locale, [locale.t("report_title"), scope["workspace"]["name"]]))
worksheet.append([locale.t("workspace"), scope["workspace"]["name"]]) worksheet.append(_rtl_row(locale, [locale.t("workspace"), scope["workspace"]["name"]]))
worksheet.append([locale.t("period"), locale.period_label(scope["period"])]) worksheet.append(_rtl_row(locale, [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(_rtl_row(locale, [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(_rtl_row(locale, [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(_rtl_row(locale, [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("generated_at"), locale.format_date(datetime.now().date(), ascii_digits=True)]))
worksheet.append([]) worksheet.append([])
worksheet.append([locale.t("summary")]) worksheet.append([locale.t("summary")])
worksheet.append([locale.t("total_hours"), locale.format_duration(summary["total_duration"], ascii_digits=True)]) worksheet.append(_rtl_row(locale, [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(_rtl_row(locale, [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(_rtl_row(locale, [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("income"), _money_label_excel(locale, summary["income_totals"])]))
for row_index in range(1, worksheet.max_row + 1): for row_index in range(1, worksheet.max_row + 1):
first_cell = worksheet.cell(row=row_index, column=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: def _render_excel_sheet(worksheet, *, locale: ExportLocale, report_data: dict) -> None:
if locale.is_rtl: if locale.is_rtl:
worksheet.sheet_view.rightToLeft = True 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_meta_block(worksheet, locale=locale, report_data=report_data)
_append_daily_table(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"]) _append_breakdown_table(worksheet, locale=locale, title_key="clients", rows=report_data["clients"])