Как получить период платежного цикла между 26-м числом предыдущего месяца и 25-м числом текущего месяца с помощью Python (с учетом часового пояса)?

Проблема

Я создаю биллинговую систему на Django, и мне нужно рассчитать расчетный период для каждого счета-фактуры.

Наше бизнес-правило простое:

  • Платежный цикл начинается 26 числа предыдущего месяца в полночь (00:00:00);
  • И заканчивается 25 числа текущего месяца в 23:59:59.

Например, если текущая дата равна 2025-07-23, результатом должно быть:

start = datetime(2025, 6, 26, 0, 0, 0)
end   = datetime(2025, 7, 25, 23, 59, 59)

Мы используем Django, поэтому даты должны соответствовать часовому поясу (предпочтительно UTC), поскольку Django хранит все поля даты и времени в UTC.

Проблема в том, что когда я запускаю свой текущий код (приведенный ниже), значения, сохраненные в базе данных, сдвигаются, например, 2025-06-26T03:00:00Z вместо 2025-06-26T00:00:00Z.

Что мы пробовали

Мы попробовали следующую функцию:

from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

def get_invoice_period(reference_date: datetime = None) -> tuple[datetime, datetime]:
    if reference_date is None:
        reference_date = datetime.now()

    end = (reference_date - timedelta(days=1)).replace(hour=23, minute=59, second=59, microsecond=0)
    start = (reference_date - relativedelta(months=1)).replace(day=26, hour=0, minute=0, second=0, microsecond=0)

    return start, end

Но это вызывает проблемы с часовым поясом, а datetime.now() не учитывает часовой пояс в Django. Поэтому, когда мы сохраняем эти значения в базе данных, Django преобразует их в UTC, сдвигая время (например, на +3 часа).

Мы рассмотрели:

Но ни один из них не решает бизнес-логику и хорошо работает с датами и временем, зависящими от часового пояса Django.

Чего мы ожидали

Мы ожидаем функцию, которая:

  • Возвращает кортеж с двумя datetime объектами;
  • Первое - 26-го числа предыдущего месяца, в полночь;
  • Второе - 25-е число текущего месяца, в 23:59:59;
  • Оба значения времени зависят от часового пояса (предпочтительнее pytz.UTC или timezone.now() в Django).

Рассматривали ли вы крайние случаи, когда часовой пояс меняется из-за перехода на летнее время или других региональных правил? Использование Python zoneinfo (Python 3.9+) или pytz может помочь обеспечить постоянное соответствие цикла выставления счетов диапазону с 26–го по 25-е число в разных часовых поясах.

Не знаком с Django, но не понимаю, почему вы не могли использовать время с учетом часовых поясов. Затем используйте datetime.datetime.replace, чтобы настроить начало/окончание расчетного периода:

import datetime as dt
import zoneinfo as zi

def billing_period(date):
    start = date.replace(month=date.month - 1, day=26, hour=0, minute=0, second=0, microsecond=0)
    end = date.replace(day=25, hour=23, minute=59, second=59, microsecond=0)
    return start, end

# Example dates
timezone = zi.ZoneInfo('US/Pacific')
date1 = dt.datetime(2025, 7, 23, tzinfo=timezone)
date2 = dt.datetime(2025, 3, 9, 12, 30, 15, 500, tzinfo=timezone)

start, end = billing_period(date1)
print(date1, start, end, sep='\n')

start, end = billing_period(date2)
print(date2, start, end, sep='\n')

Вывод с комментарием:

2025-07-23 00:00:00-07:00
2025-06-26 00:00:00-07:00
2025-07-25 23:59:59-07:00
2025-03-09 12:30:15.000500-07:00  # first day of PDT
2025-02-26 00:00:00-08:00         # Note shift to PST
2025-03-25 23:59:59-07:00
Вернуться на верх