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:
- this answer from Mitghi
- and this question, which was closed
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'stimezone.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