How to get a billing cycle period between the 26th of the previous month and the 25th of the current month using Python (timezone-aware)?

The Problem

I'm building a billing system in Django, and I need to calculate the billing period for each invoice.

Our business rule is simple:

  • The billing cycle starts on the 26th of the previous month at midnight (00:00:00);
  • And ends on the 25th of the current month at 23:59:59.

For example, if the current date is 2025-07-23, the result should be:

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

We're using Django, so the dates must be timezone-aware (UTC preferred), as Django stores all datetime fields in UTC.

The problem is: when I run my current code (below), the values saved in the database are shifted, like 2025-06-26T03:00:00Z instead of 2025-06-26T00:00:00Z.

What We Tried

We tried the following function:

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

But this causes timezone problems, and datetime.now() is not timezone-aware in Django. So when we save these values to the database, Django converts them to UTC, shifting the time (e.g., +3 hours).

We looked at:

But none of them solves the business logic and works well with Django timezone-aware datetimes.

What We Expected

We expect a function that:

  • Returns a tuple with two datetime objects;
  • The first is the 26th of the previous month, at midnight;
  • The second is the 25th of the current month, at 23:59:59;
  • Both datetimes are timezone-aware (pytz.UTC or Django's timezone.now() preferred).

Have you considered edge cases where the timezone shifts due to DST or other regional rules? Using Python’s zoneinfo (Python 3.9+) or pytz can help ensure the billing cycle consistently aligns with the 26th–25th range across different timezones.

Not familiar with Django, but don't know why you couldn't use time-zone aware times. Then use datetime.datetime.replace to adjust your start/end of the billing period:

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')

Output with comment:

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
Вернуться на верх