"""Jalali (Shamsi) date rendering for output that never passes through the API. The REST layer keeps speaking ISO-8601 Gregorian on purpose — django-filter lookups, the Odoo integration and the stored report payloads all depend on it. Conversion belongs where a human actually reads a date: the generated PDF and the customer-facing SMS. Numerals stay ASCII. ReportLab's built-in fonts have no Persian glyphs, so emitting Persian digits would render as black boxes in the PDF. """ from __future__ import annotations from datetime import date, datetime, time from typing import Any, Optional import jdatetime from django.utils import timezone __all__ = ["jalali_date", "jalali_datetime", "jalali_time"] def _parse_iso(value: str) -> Optional[datetime]: """Read back what ``datetime.isoformat()`` wrote, tolerating a bare date.""" try: return datetime.fromisoformat(value) except ValueError: try: return datetime.combine(date.fromisoformat(value), time()) except ValueError: return None def _localize(value: datetime) -> datetime: """Move an aware datetime into the project's ``TIME_ZONE``. Naive values are returned untouched — they are already wall-clock local, and ``timezone.localtime`` refuses them outright under ``USE_TZ = True``. """ if not timezone.is_aware(value): return value try: return timezone.localtime(value) except (ValueError, OverflowError): # Timestamps at the epoch boundary overflow when shifted west of UTC. return value def _coerce(value: Any) -> Any: """Accept a date, a datetime, or an ISO string; return date/datetime/None.""" if isinstance(value, str): return _parse_iso(value) return value def jalali_date(value: Any) -> str: """Render a ``date``/``datetime``/ISO string as ``YYYY/MM/DD`` (Jalali).""" value = _coerce(value) if isinstance(value, datetime): value = _localize(value).date() if not isinstance(value, date): return "" j = jdatetime.date.fromgregorian(date=value) return f"{j.year:04d}/{j.month:02d}/{j.day:02d}" def jalali_datetime(value: Any) -> str: """Render a datetime/ISO string as ``YYYY/MM/DD HH:MM`` (Jalali, local time).""" value = _coerce(value) if not isinstance(value, datetime): return jalali_date(value) j = jdatetime.datetime.fromgregorian(datetime=_localize(value)) return f"{j.year:04d}/{j.month:02d}/{j.day:02d} {j.hour:02d}:{j.minute:02d}" def jalali_time(value: Any) -> str: """Render a ``time``/``datetime``/``HH:MM[:SS]`` string as ``HH:MM``.""" if isinstance(value, str): parts = value.strip().split(":") if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit(): return f"{int(parts[0]):02d}:{parts[1]}" return "" if isinstance(value, datetime): value = _localize(value).time() if isinstance(value, time): return f"{value.hour:02d}:{value.minute:02d}" return ""