Как получить период платежного цикла между 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