fix(reports): localize and group exported income values
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
Reference in New Issue
Block a user