feat(reports): add uncategorized dual-share exports

This commit is contained in:
2026-05-21 19:10:33 +03:30
parent e234eac26d
commit 8d2f876c82
5 changed files with 838 additions and 319 deletions

View File

@@ -1,16 +1,16 @@
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
from datetime import date
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Iterable
import jdatetime
from arabic_reshaper import reshape
from bidi.algorithm import get_display
PERSIAN_DIGITS = str.maketrans("0123456789", "۰۱۲۳۴۵۶۷۸۹")
PERSIAN_DIGITS = str.maketrans("0123456789", "\u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9")
ARABIC_RANGES = (
(0x0600, 0x06FF),
(0x0750, 0x077F),
@@ -50,6 +50,8 @@ TRANSLATIONS = {
"from": "From",
"to": "To",
"percentage": "Percentage",
"hour_percentage": "Hour %",
"income_percentage": "Income %",
"none": "None",
"daily_summary": "Daily Summary",
"clients": "Clients",
@@ -59,45 +61,53 @@ TRANSLATIONS = {
"name": "Name",
"total": "Total",
"no_data": "No data",
"uncategorized_client": "No client",
"uncategorized_project": "No project",
"uncategorized_tag": "No tag",
},
"fa": {
"report_title": "گزارش فضای کاری",
"overall_sheet": "گزارش کلی",
"users_summary_sheet": "خلاصه کاربران",
"workspace": "فضای کاری",
"period": "بازه",
"from_date": "از تاریخ",
"to_date": "تا تاریخ",
"user": "کاربر",
"mobile": "موبایل",
"all_users": "همه کاربران",
"generated_at": "تاریخ تولید",
"summary": "خلاصه",
"total_hours": "کل ساعات",
"billable_hours": "ساعات کاری",
"non_billable_hours": "ساعات غیر کاری",
"hourly_rate": "نرخ ساعتی",
"income": "کارکرد",
"working_hours": "ساعات کاری",
"non_working_hours": "ساعات غیرکاری",
"hourly_rates": "نرخ‌های ساعتی",
"project_percentages": "درصد پروژه‌ها",
"client_percentages": "درصد مشتری‌ها",
"tag_percentages": "درصد تگ‌ها",
"summary_by_user": "خلاصه کاربران",
"rate_history": "تاریخچه نرخ ساعتی",
"from": "از",
"to": "تا",
"percentage": "درصد",
"none": "بدون مورد",
"daily_summary": "خلاصه روزانه",
"clients": "مشتریان",
"projects": "پروژه‌ها",
"tags": "تگ‌ها",
"date": "تاریخ",
"name": "نام",
"total": "جمع",
"no_data": "بدون داده",
"report_title": "\u06af\u0632\u0627\u0631\u0634 \u0641\u0636\u0627\u06cc \u06a9\u0627\u0631\u06cc",
"overall_sheet": "\u06af\u0632\u0627\u0631\u0634 \u06a9\u0644\u06cc",
"users_summary_sheet": "\u062e\u0644\u0627\u0635\u0647 \u06a9\u0627\u0631\u0628\u0631\u0627\u0646",
"workspace": "\u0641\u0636\u0627\u06cc \u06a9\u0627\u0631\u06cc",
"period": "\u0628\u0627\u0632\u0647",
"from_date": "\u0627\u0632 \u062a\u0627\u0631\u06cc\u062e",
"to_date": "\u062a\u0627 \u062a\u0627\u0631\u06cc\u062e",
"user": "\u06a9\u0627\u0631\u0628\u0631",
"mobile": "\u0645\u0648\u0628\u0627\u06cc\u0644",
"all_users": "\u0647\u0645\u0647 \u06a9\u0627\u0631\u0628\u0631\u0627\u0646",
"generated_at": "\u062a\u0627\u0631\u06cc\u062e \u062a\u0648\u0644\u06cc\u062f",
"summary": "\u062e\u0644\u0627\u0635\u0647",
"total_hours": "\u06a9\u0644 \u0633\u0627\u0639\u0627\u062a",
"billable_hours": "\u0633\u0627\u0639\u0627\u062a \u06a9\u0627\u0631\u06cc",
"non_billable_hours": "\u0633\u0627\u0639\u0627\u062a \u063a\u06cc\u0631 \u06a9\u0627\u0631\u06cc",
"hourly_rate": "\u0646\u0631\u062e \u0633\u0627\u0639\u062a\u06cc",
"income": "\u06a9\u0627\u0631\u06a9\u0631\u062f",
"working_hours": "\u0633\u0627\u0639\u0627\u062a \u06a9\u0627\u0631\u06cc",
"non_working_hours": "\u0633\u0627\u0639\u0627\u062a \u063a\u06cc\u0631\u06a9\u0627\u0631\u06cc",
"hourly_rates": "\u0646\u0631\u062e\u200c\u0647\u0627\u06cc \u0633\u0627\u0639\u062a\u06cc",
"project_percentages": "\u062f\u0631\u0635\u062f \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627",
"client_percentages": "\u062f\u0631\u0635\u062f \u0645\u0634\u062a\u0631\u06cc\u200c\u0647\u0627",
"tag_percentages": "\u062f\u0631\u0635\u062f \u062a\u06af\u200c\u0647\u0627",
"summary_by_user": "\u062e\u0644\u0627\u0635\u0647 \u06a9\u0627\u0631\u0628\u0631\u0627\u0646",
"rate_history": "\u062a\u0627\u0631\u06cc\u062e\u0686\u0647 \u0646\u0631\u062e \u0633\u0627\u0639\u062a\u06cc",
"from": "\u0627\u0632",
"to": "\u062a\u0627",
"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",
"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",
"projects": "\u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627",
"tags": "\u062a\u06af\u200c\u0647\u0627",
"date": "\u062a\u0627\u0631\u06cc\u062e",
"name": "\u0646\u0627\u0645",
"total": "\u062c\u0645\u0639",
"no_data": "\u0628\u062f\u0648\u0646 \u062f\u0627\u062f\u0647",
"uncategorized_client": "\u0628\u062f\u0648\u0646 \u0645\u0634\u062a\u0631\u06cc",
"uncategorized_project": "\u0628\u062f\u0648\u0646 \u067e\u0631\u0648\u0698\u0647",
"uncategorized_tag": "\u0628\u062f\u0648\u0646 \u062a\u06af",
},
}
@@ -111,23 +121,23 @@ PERIOD_LABELS = {
"period": "Custom period",
},
"fa": {
"this_week": "این هفته",
"this_month": "این ماه",
"this_year": "امسال",
"half_year_first": "نیمه اول سال",
"half_year_second": "نیمه دوم سال",
"period": "بازه دلخواه",
"this_week": "\u0627\u06cc\u0646 \u0647\u0641\u062a\u0647",
"this_month": "\u0627\u06cc\u0646 \u0645\u0627\u0647",
"this_year": "\u0627\u0645\u0633\u0627\u0644",
"half_year_first": "\u0646\u06cc\u0645\u0647 \u0627\u0648\u0644 \u0633\u0627\u0644",
"half_year_second": "\u0646\u06cc\u0645\u0647 \u062f\u0648\u0645 \u0633\u0627\u0644",
"period": "\u0628\u0627\u0632\u0647 \u062f\u0644\u062e\u0648\u0627\u0647",
},
}
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": "لیر"},
"USD": {"en": "USD", "fa": "\u062f\u0644\u0627\u0631 \u0622\u0645\u0631\u06cc\u06a9\u0627"},
"EUR": {"en": "EUR", "fa": "\u06cc\u0648\u0631\u0648"},
"GBP": {"en": "GBP", "fa": "\u067e\u0648\u0646\u062f"},
"IRR": {"en": "IRR", "fa": "\u0631\u06cc\u0627\u0644"},
"IRT": {"en": "IRT", "fa": "\u062a\u0648\u0645\u0627\u0646"},
"AED": {"en": "AED", "fa": "\u062f\u0631\u0647\u0645"},
"TRY": {"en": "TRY", "fa": "\u0644\u06cc\u0631"},
}
@@ -225,7 +235,7 @@ def user_label(user_payload: dict | None, locale: ExportLocale, *, ascii_digits:
def safe_sheet_title(title: str, used: Iterable[str]) -> str:
invalid = set('[]:*?/\\')
invalid = set("[]:*?/\\")
sanitized = "".join("-" if char in invalid else char for char in title).strip() or "Sheet"
base = sanitized[:31]
used_set = set(used)