fix(transactions): keep a recurrence on its day after a short month

A monthly recurrence on the 31st clamped to Feb 28 and stayed on the
28th for every later month.
This commit is contained in:
Jay Hemnani committed 2026-11-10 03:51:15 +05:30
1 parent bdc0603327
commit 6a289327b6
2 files changed
+40 -1

No files matched your search

+16 -1
View File
@@ -1,3 +1,4 @@
import calendar
import decimal
import logging
import uuid
@@ -1183,7 +1184,21 @@ class RecurringTransaction(TransferTemplate):
return relativedelta(years=self.recurrence_interval)
def get_next_date(self, current_date):
return current_date + self.get_recurrence_delta()
next_date = current_date + self.get_recurrence_delta()
# A start on the 29th-31st is clamped in shorter months; return to that day
# afterwards instead of keeping the clamped one.
if (
self.recurrence_type
in (self.RecurrenceType.MONTH, self.RecurrenceType.YEAR)
and self.start_date.day > current_date.day
and current_date.day
== calendar.monthrange(current_date.year, current_date.month)[1]
):
last_day = calendar.monthrange(next_date.year, next_date.month)[1]
next_date = next_date.replace(day=min(self.start_date.day, last_day))
return next_date
@classmethod
def generate_upcoming_transactions(cls):
@@ -242,6 +242,30 @@ class RecurringTransactionTests(TestCase):
self.assertEqual(recurring.recurrence_interval, 1)
self.assertEqual(recurring.account.currency.code, "USD")
def test_month_end_recurrence_returns_to_its_day(self):
"""A recurrence on the 31st goes back to it after a short month"""
recurring = RecurringTransaction.objects.create(
account=self.account,
type=Transaction.Type.EXPENSE,
amount=Decimal("100.00"),
description="Rent",
start_date=datetime.date(2026, 1, 31),
recurrence_type=RecurringTransaction.RecurrenceType.MONTH,
recurrence_interval=1,
)
feb = recurring.get_next_date(datetime.date(2026, 1, 31))
march = recurring.get_next_date(feb)
april = recurring.get_next_date(march)
self.assertEqual(feb, datetime.date(2026, 2, 28))
self.assertEqual(march, datetime.date(2026, 3, 31))
self.assertEqual(april, datetime.date(2026, 4, 30))
self.assertEqual(
recurring.get_next_date(datetime.date(2026, 2, 1)),
datetime.date(2026, 3, 1),
)
def test_generate_upcoming_transactions_keeps_tags_and_entities(self):
"""Generation must copy tags/entities even with no current user"""
tag = TransactionTag.objects.create(name="Essential")