87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
"""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 "" |