Andrey Epifancev bc89789026 fix(stages): нормализовать aware-даты этапов к поясу оргединицы
asyncpg не принимает aware-datetime для CAST AS TIMESTAMP WITHOUT TIME ZONE (400).
Даты переводятся в пояс оргединицы и становятся naive, SQL приписывает тот же offset.
timezone_from_offset вынесен в src/core/timezones.py, добавлен to_org_local_naive.
Aware-дата при NULL utc_offset оргединицы даёт 422.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:13:13 +04:00

41 lines
1.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import datetime
from fastapi import HTTPException
def timezone_from_offset(offset: str) -> datetime.timezone | None:
"""Строка вида '+03' / '-05' (org_unit.utc_offset) -> datetime.timezone."""
if not offset:
return None
hours = int(offset[1:])
return datetime.timezone(
datetime.timedelta(
hours=hours if offset[0] == '+' else -hours
)
)
def to_org_local_naive(
dt: datetime.datetime, utc_offset: str | None
) -> datetime.datetime:
"""Приводит дату к местному naive-времени оргединицы.
Naive-значение возвращается без изменений (оно уже местное). Aware-значение
переводится в пояс оргединицы, после чего tzinfo отбрасывается: SQL-функции
(v3.add_form_phase и др.) затем приписывают тот же offset, и момент времени
сохраняется. Если у оргединицы нет utc_offset, а дата aware - 422.
"""
if dt.tzinfo is None:
return dt
tz = timezone_from_offset(utc_offset) if utc_offset else None
if tz is None:
raise HTTPException(
status_code=422,
detail=(
"Дата передана с часовым поясом, но у оргединицы формы не "
"задан utc_offset. Передайте дату без часового пояса."
),
)
return dt.astimezone(tz).replace(tzinfo=None)