diff --git a/app/apps/rules/forms.py b/app/apps/rules/forms.py index f552beb..fceb802 100644 --- a/app/apps/rules/forms.py +++ b/app/apps/rules/forms.py @@ -65,10 +65,11 @@ class TransactionRuleForm(forms.ModelForm): class TransactionRuleActionForm(forms.ModelForm): class Meta: model = TransactionRuleAction - fields = ("value", "field") + fields = ("value", "field", "order") labels = { "field": _("Set field"), "value": _("To"), + "order": _("Order"), } widgets = {"field": TomSelect(clear_button=False)} @@ -82,6 +83,7 @@ class TransactionRuleActionForm(forms.ModelForm): self.helper.form_method = "post" # TO-DO: Add helper with available commands self.helper.layout = Layout( + "order", "field", "value", ) @@ -150,6 +152,7 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm): } labels = { + "order": _("Order"), "search_account_operator": _("Operator"), "search_type_operator": _("Operator"), "search_is_paid_operator": _("Operator"), @@ -200,6 +203,7 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm): self.helper.form_method = "post" self.helper.layout = Layout( + "order", BS5Accordion( AccordionGroup( _("Search Criteria"), diff --git a/app/apps/rules/migrations/0015_alter_transactionruleaction_options_and_more.py b/app/apps/rules/migrations/0015_alter_transactionruleaction_options_and_more.py new file mode 100644 index 0000000..d8a6911 --- /dev/null +++ b/app/apps/rules/migrations/0015_alter_transactionruleaction_options_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2 on 2025-08-30 18:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("rules", "0014_alter_transactionrule_owner_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="transactionruleaction", + options={ + "ordering": ["order"], + "verbose_name": "Edit transaction action", + "verbose_name_plural": "Edit transaction actions", + }, + ), + migrations.AlterModelOptions( + name="updateorcreatetransactionruleaction", + options={ + "ordering": ["order"], + "verbose_name": "Update or create transaction action", + "verbose_name_plural": "Update or create transaction actions", + }, + ), + migrations.AddField( + model_name="transactionruleaction", + name="order", + field=models.PositiveIntegerField(default=0, verbose_name="Order"), + ), + migrations.AddField( + model_name="updateorcreatetransactionruleaction", + name="order", + field=models.PositiveIntegerField(default=0, verbose_name="Order"), + ), + ] diff --git a/app/apps/rules/models.py b/app/apps/rules/models.py index 5c520e0..edaebe7 100644 --- a/app/apps/rules/models.py +++ b/app/apps/rules/models.py @@ -51,6 +51,7 @@ class TransactionRuleAction(models.Model): verbose_name=_("Field"), ) value = models.TextField(verbose_name=_("Value")) + order = models.PositiveIntegerField(default=0, verbose_name=_("Order")) def __str__(self): return f"{self.rule} - {self.field} - {self.value}" @@ -59,6 +60,11 @@ class TransactionRuleAction(models.Model): verbose_name = _("Edit transaction action") verbose_name_plural = _("Edit transaction actions") unique_together = (("rule", "field"),) + ordering = ["order"] + + @property + def action_type(self): + return "edit_transaction" class UpdateOrCreateTransactionRuleAction(models.Model): @@ -290,10 +296,16 @@ class UpdateOrCreateTransactionRuleAction(models.Model): verbose_name=_("Tags"), blank=True, ) + order = models.PositiveIntegerField(default=0, verbose_name=_("Order")) class Meta: verbose_name = _("Update or create transaction action") verbose_name_plural = _("Update or create transaction actions") + ordering = ["order"] + + @property + def action_type(self): + return "update_or_create_transaction" def __str__(self): return f"Update or create transaction action for {self.rule}" diff --git a/app/apps/rules/tasks.py b/app/apps/rules/tasks.py index 828c635..c8dca41 100644 --- a/app/apps/rules/tasks.py +++ b/app/apps/rules/tasks.py @@ -2,6 +2,7 @@ import decimal import logging from datetime import datetime, date from decimal import Decimal +from itertools import chain from cachalot.api import cachalot_disabled from dateutil.relativedelta import relativedelta @@ -87,7 +88,9 @@ def check_for_transaction_rules( # For deleted transactions, we might want to limit what actions can be performed if signal == "transaction_deleted": # Process only create/update actions, not edit actions - for action in rule.update_or_create_transaction_actions.all(): + for ( + action + ) in rule.update_or_create_transaction_actions.all(): try: _process_update_or_create_transaction_action( action=action, simple_eval=simple @@ -99,31 +102,74 @@ def check_for_transaction_rules( ) else: # Normal processing for non-deleted transactions - for action in rule.transaction_actions.all(): - try: - instance = _process_edit_transaction_action( - instance=instance, action=action, simple_eval=simple - ) - except Exception as e: - logger.error( - f"Error processing edit transaction action {action.id}", - exc_info=True, - ) + edit_actions = list(rule.transaction_actions.all()) + update_or_create_actions = list( + rule.update_or_create_transaction_actions.all() + ) - simple.names.update(_get_names(instance)) - if signal != "transaction_deleted": - instance.save() + # Check if any action has a non-zero order + has_custom_order = any( + a.order > 0 for a in edit_actions + ) or any(a.order > 0 for a in update_or_create_actions) - for action in rule.update_or_create_transaction_actions.all(): - try: - _process_update_or_create_transaction_action( - action=action, simple_eval=simple - ) - except Exception as e: - logger.error( - f"Error processing update or create transaction action {action.id}", - exc_info=True, - ) + if has_custom_order: + # Combine and sort actions by order + all_actions = sorted( + chain(edit_actions, update_or_create_actions), + key=lambda a: a.order, + ) + + for action in all_actions: + try: + if isinstance(action, TransactionRuleAction): + instance = _process_edit_transaction_action( + instance=instance, + action=action, + simple_eval=simple, + ) + # Update names for next actions + simple.names.update(_get_names(instance)) + else: + _process_update_or_create_transaction_action( + action=action, simple_eval=simple + ) + except Exception as e: + logger.error( + f"Error processing action {action.id}", + exc_info=True, + ) + # Save at the end + if signal != "transaction_deleted": + instance.save() + else: + # Original behavior + for action in edit_actions: + try: + instance = _process_edit_transaction_action( + instance=instance, + action=action, + simple_eval=simple, + ) + except Exception as e: + logger.error( + f"Error processing edit transaction action {action.id}", + exc_info=True, + ) + + simple.names.update(_get_names(instance)) + if signal != "transaction_deleted": + instance.save() + + for action in update_or_create_actions: + try: + _process_update_or_create_transaction_action( + action=action, simple_eval=simple + ) + except Exception as e: + logger.error( + f"Error processing update or create transaction action {action.id}", + exc_info=True, + ) except Exception as e: logger.error( "Error while executing 'check_for_transaction_rules' task", diff --git a/app/apps/rules/views.py b/app/apps/rules/views.py index 7ac0013..57408b7 100644 --- a/app/apps/rules/views.py +++ b/app/apps/rules/views.py @@ -1,3 +1,5 @@ +from itertools import chain + from django.contrib import messages from django.contrib.auth.decorators import login_required from django.http import HttpResponse @@ -140,10 +142,18 @@ def transaction_rule_edit(request, transaction_rule_id): def transaction_rule_view(request, transaction_rule_id): transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) + edit_actions = transaction_rule.transaction_actions.all() + update_or_create_actions = transaction_rule.update_or_create_transaction_actions.all() + + all_actions = sorted( + chain(edit_actions, update_or_create_actions), + key=lambda a: a.order, + ) + return render( request, "rules/fragments/transaction_rule/view.html", - {"transaction_rule": transaction_rule}, + {"transaction_rule": transaction_rule, "all_actions": all_actions}, ) diff --git a/app/apps/transactions/tests.py b/app/apps/transactions/tests.py index 4cc8fab..1535120 100644 --- a/app/apps/transactions/tests.py +++ b/app/apps/transactions/tests.py @@ -175,6 +175,6 @@ class RecurringTransactionTests(TestCase): recurrence_type=RecurringTransaction.RecurrenceType.MONTH, recurrence_interval=1, ) - self.assertFalse(recurring.paused) + self.assertFalse(recurring.is_paused) self.assertEqual(recurring.recurrence_interval, 1) self.assertEqual(recurring.account.currency.code, "USD") diff --git a/app/templates/rules/fragments/transaction_rule/view.html b/app/templates/rules/fragments/transaction_rule/view.html index 19c1aa5..d2eebe1 100644 --- a/app/templates/rules/fragments/transaction_rule/view.html +++ b/app/templates/rules/fragments/transaction_rule/view.html @@ -30,75 +30,84 @@