arc/hesabixAPI/app/core/datetime_utils.py

132 lines
4.6 KiB
Python

"""ابزارهای مرکزی تاریخ/زمان — قرارداد: ذخیره UTC naive، نمایش بر اساس timezone کسب‌وکار/سیستم."""
from __future__ import annotations
from datetime import datetime, timezone as dt_timezone
from typing import Optional
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
def utc_now() -> datetime:
"""لحظهٔ فعلی UTC به‌صورت naive (سازگار با ستون‌های DateTime بدون tz)."""
return datetime.now(dt_timezone.utc).replace(tzinfo=None)
def utc_naive_to_aware(dt: datetime) -> datetime:
if dt.tzinfo is None:
return dt.replace(tzinfo=dt_timezone.utc)
return dt.astimezone(dt_timezone.utc)
def utc_naive_to_iso_z(dt: datetime) -> str:
"""ISO 8601 با پسوند Z برای پارس امن در کلاینت."""
return utc_naive_to_aware(dt).isoformat().replace("+00:00", "Z")
def localize_assumed_utc_naive_for_display(dt: datetime, tz_name: str) -> datetime:
"""
قرارداد: datetime بدون tz در DB به‌عنوان UTC ذخیره شده است.
خروجی: همان لحظه به‌صورت «ساعت دیوار» در منطقهٔ نمایش (naive برای تبدیل تقویم).
"""
name = (tz_name or "Asia/Tehran").strip() or "Asia/Tehran"
try:
tz = ZoneInfo(name)
except ZoneInfoNotFoundError:
tz = ZoneInfo("Asia/Tehran")
utc_dt = utc_naive_to_aware(dt)
return utc_dt.astimezone(tz).replace(tzinfo=None)
def resolve_display_timezone_name(business_id: Optional[int] = None) -> str:
"""منطقهٔ زمانی نمایش: ابتدا کسب‌وکار، سپس پیش‌فرض سیستم."""
if business_id is not None:
from app.services.business_timezone_service import get_business_display_timezone_cached
return get_business_display_timezone_cached(int(business_id))
from app.services.system_settings_service import get_system_display_timezone_cached
return get_system_display_timezone_cached()
def resolve_display_timezone_from_request(request) -> str:
"""timezone نمایش از request.state.business_id (در صورت وجود)."""
business_id: Optional[int] = None
try:
if request is not None and hasattr(request.state, "business_id"):
raw = getattr(request.state, "business_id", None)
if raw is not None:
business_id = int(raw)
except Exception:
business_id = None
return resolve_display_timezone_name(business_id)
def format_datetime_for_calendar(
dt: datetime,
calendar_type: str,
tz_name: str,
) -> dict:
from app.core.calendar import CalendarConverter
local_dt = localize_assumed_utc_naive_for_display(dt, tz_name)
return CalendarConverter.format_datetime(local_dt, calendar_type)
def utc_now_aware() -> datetime:
"""لحظهٔ فعلی UTC با timezone (برای ستون‌های TIMESTAMPTZ)."""
return datetime.now(dt_timezone.utc)
def business_wall_clock_now(business_id: Optional[int] = None) -> datetime:
"""ساعت دیوار در منطقهٔ زمانی کسب‌وکار (naive برای قالب‌ها)."""
from app.core.business_calendar import business_now
return business_now(business_id).replace(tzinfo=None)
def resolve_calendar_type_for_request(request, is_fa: bool = False) -> str:
"""تقویم نمایش از middleware یا هدر X-Calendar-Type."""
try:
if request is not None and hasattr(request.state, "calendar_type") and request.state.calendar_type:
return str(request.state.calendar_type)
except Exception:
pass
try:
from app.core.calendar import get_calendar_type_from_header
cal_header = request.headers.get("X-Calendar-Type") if request is not None else None
if cal_header:
return get_calendar_type_from_header(cal_header)
except Exception:
pass
return "jalali" if is_fa else "gregorian"
def _trim_seconds_display(s: str) -> str:
if len(s) >= 3 and s[-3] == ":" and s[-2:].isdigit():
return s[:-3]
return s
def format_generated_at_for_pdf(
business_id: Optional[int],
calendar_type: str = "jalali",
) -> str:
"""زمان تولید PDF/گزارش در timezone کسب‌وکار و تقویم انتخابی."""
from app.core.calendar import CalendarConverter
now_dt = business_wall_clock_now(business_id)
try:
out = CalendarConverter.format_datetime(now_dt, calendar_type)
s = str(out.get("formatted") or out.get("date_time") or "")
if s:
return _trim_seconds_display(s)
except Exception:
pass
return now_dt.strftime("%Y/%m/%d %H:%M")
def export_filename_timestamp(business_id: Optional[int] = None) -> str:
"""پسوند نام فایل export بر اساس ساعت دیوار کسب‌وکار."""
return business_wall_clock_now(business_id).strftime("%Y%m%d_%H%M%S")