From cd6069f857c202ac36aeae07581deaa688af6a47 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Fri, 9 Oct 2026 23:25:19 -0300 Subject: [PATCH] feat(transactions): let new recurring transactions and installment plans adopt an existing transaction --- app/apps/transactions/forms.py | 14 ++++++++++---- app/apps/transactions/models.py | 27 ++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py index a5ef7049..5a2a582a 100644 --- a/app/apps/transactions/forms.py +++ b/app/apps/transactions/forms.py @@ -887,11 +887,17 @@ class InstallmentPlanForm(forms.ModelForm): ), ) - def save(self, **kwargs): + def save(self, source_transaction=None, **kwargs): is_new = not self.instance.id instance = super().save(**kwargs) - if is_new: + if is_new and source_transaction: + # The converted transaction becomes the first installment + source_transaction.installment_plan = instance + source_transaction.installment_id = instance.installment_start + source_transaction.save() + instance.update_transactions() + elif is_new: instance.create_transactions() else: instance.update_transactions() @@ -1145,12 +1151,12 @@ class RecurringTransactionForm(forms.ModelForm): return cleaned_data - def save(self, **kwargs): + def save(self, source_transaction=None, **kwargs): is_new = not self.instance.id instance = super().save(**kwargs) if is_new: - instance.create_upcoming_transactions() + instance.create_upcoming_transactions(first_transaction=source_transaction) else: instance.update_unpaid_transactions() instance.generate_upcoming_transactions() diff --git a/app/apps/transactions/models.py b/app/apps/transactions/models.py index f462a35e..8fd0285b 100644 --- a/app/apps/transactions/models.py +++ b/app/apps/transactions/models.py @@ -915,7 +915,7 @@ class RecurringTransaction(models.Model): instance = super().save(*args, **kwargs) return instance - def create_upcoming_transactions(self): + def create_upcoming_transactions(self, first_transaction=None): current_date = self.start_date reference_date = self.reference_date end_date = min( @@ -925,6 +925,12 @@ class RecurringTransaction(models.Model): timezone.now().date() + (self.get_recurrence_delta() * self.keep_at_most), ) + # When converting an existing transaction, it becomes the first occurrence + if first_transaction: + self.adopt_transaction(first_transaction, current_date, reference_date) + current_date = self.get_next_date(current_date) + reference_date = self.get_next_date(reference_date) + while current_date <= end_date: self.create_transaction(current_date, reference_date) current_date = self.get_next_date(current_date) @@ -959,6 +965,25 @@ class RecurringTransaction(models.Model): created_transaction.tags.set(self.tags(manager="all_objects").all()) created_transaction.entities.set(self.entities(manager="all_objects").all()) + def adopt_transaction(self, existing_transaction, date, reference_date): + existing_transaction.account = self.account + existing_transaction.type = self.type + existing_transaction.date = date + existing_transaction.reference_date = reference_date.replace(day=1) + existing_transaction.description = ( + self.description if self.add_description_to_transaction else "" + ) + existing_transaction.category = self.category + existing_transaction.notes = self.notes if self.add_notes_to_transaction else "" + existing_transaction.recurring_transaction = self + + if not existing_transaction.is_paid: # Don't update value for paid transactions + existing_transaction.amount = self.amount + + existing_transaction.save() + existing_transaction.tags.set(self.tags(manager="all_objects").all()) + existing_transaction.entities.set(self.entities(manager="all_objects").all()) + def get_recurrence_delta(self): if self.recurrence_type == self.RecurrenceType.DAY: return relativedelta(days=self.recurrence_interval)