I'm building a billing system in Django, and I need to calculate the billing period for each invoice.
Our business rule is simple:
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
.
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.
We expect a function that:
datetime
objects;pytz.UTC
or Django's timezone.now()
preferred).Edit - Solution
from django.utils import timezone
from dateutil.relativedelta import relativedelta
def get_invoice_period(reference_date=None):
if reference_date is None:
reference_date = timezone.localtime()
start = reference_date - relativedelta(months=1)
start = start.replace(day=26, hour=0, minute=0, second=0, microsecond=0)
end = reference_date.replace(day=25, hour=23, minute=59, second=59, microsecond=0)
return start, end
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):
# To handle January, move to the first day
# of the current month, then back up a day to
# compute the previous month.
prev_month = date.replace(day=1) - dt.timedelta(days=1)
# now adjust to midnight of day 26.
start = prev_month.replace(day=26, hour=0, minute=0, second=0, microsecond=0)
# move current date to the last microsecond at end of day 25.
end = date.replace(day=25, hour=23, minute=59, second=59, microsecond=999999)
return start, end
timezone = zi.ZoneInfo('US/Pacific')
dates = [dt.datetime(2025, 7, 23, tzinfo=timezone), # OP example
dt.datetime(2025, 3, 9, 12, 30, 15, 500, tzinfo=timezone), # start of DST
dt.datetime(2025, 1, 9, 12, 30, 15, 500, tzinfo=timezone)] # test January
for date in dates:
start, end = billing_period(date)
print(date, start, end, sep='\n', end='\n\n')
Output with comments:
2025-07-23 00:00:00-07:00
2025-06-26 00:00:00-07:00
2025-07-25 23:59:59.999999-07:00
2025-03-09 12:30:15.000500-07:00
2025-02-26 00:00:00-08:00 # Previous day is PST
2025-03-25 23:59:59.999999-07:00 # PDT
2025-01-09 12:30:15.000500-08:00
2024-12-26 00:00:00-08:00 # December of previous year
2025-01-25 23:59:59.999999-08:00