diff --git a/app/apps/rules/forms.py b/app/apps/rules/forms.py
index f3989c2..64c6201 100644
--- a/app/apps/rules/forms.py
+++ b/app/apps/rules/forms.py
@@ -1,16 +1,18 @@
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
from crispy_forms.bootstrap import FormActions, AccordionGroup
from crispy_forms.helper import FormHelper
-from crispy_forms.layout import Layout, Field, Row, Column
+from crispy_forms.layout import Layout, Field, Row, Column, HTML
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
+from apps.common.widgets.crispy.submit import NoClassSubmit
from apps.common.widgets.crispy.submit import NoClassSubmit
from apps.common.widgets.tom_select import TomSelect, TransactionSelect
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
from apps.rules.models import TransactionRuleAction
from apps.common.fields.forms.dynamic_select import DynamicModelChoiceField
+from apps.transactions.forms import BulkEditTransactionForm
from apps.transactions.models import Transaction
@@ -431,6 +433,17 @@ class DryRunCreatedTransacion(forms.Form):
),
)
+ if self.data.get("transaction"):
+ try:
+ transaction = Transaction.objects.get(id=self.data.get("transaction"))
+ except Transaction.DoesNotExist:
+ transaction = None
+
+ if transaction:
+ self.fields["transaction"].queryset = Transaction.objects.filter(
+ id=transaction.id
+ )
+
class DryRunDeletedTransacion(forms.Form):
transaction = DynamicModelChoiceField(
@@ -456,3 +469,49 @@ class DryRunDeletedTransacion(forms.Form):
),
),
)
+
+ if self.data.get("transaction"):
+ try:
+ transaction = Transaction.objects.get(id=self.data.get("transaction"))
+ except Transaction.DoesNotExist:
+ transaction = None
+
+ if transaction:
+ self.fields["transaction"].queryset = Transaction.objects.filter(
+ id=transaction.id
+ )
+
+
+class DryRunUpdatedTransactionForm(BulkEditTransactionForm):
+ transaction = DynamicModelChoiceField(
+ model=Transaction,
+ to_field_name="id",
+ label=_("Transaction"),
+ required=True,
+ queryset=Transaction.objects.none(),
+ widget=TransactionSelect(clear_button=False, income=True, expense=True),
+ help_text=_("Type to search for a transaction"),
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.helper.layout.insert(0, "transaction")
+ self.helper.layout.insert(1, HTML("
"))
+
+ # Change submit button
+ self.helper.layout[-1] = FormActions(
+ NoClassSubmit(
+ "submit", _("Test"), css_class="btn btn-outline-primary w-100"
+ )
+ )
+
+ if self.data.get("transaction"):
+ try:
+ transaction = Transaction.objects.get(id=self.data.get("transaction"))
+ except Transaction.DoesNotExist:
+ transaction = None
+
+ if transaction:
+ self.fields["transaction"].queryset = Transaction.objects.filter(
+ id=transaction.id
+ )
diff --git a/app/apps/rules/tasks.py b/app/apps/rules/tasks.py
index 6e259e8..ca06b7e 100644
--- a/app/apps/rules/tasks.py
+++ b/app/apps/rules/tasks.py
@@ -1,7 +1,6 @@
import decimal
import logging
import traceback
-from copy import deepcopy
from datetime import datetime, date
from decimal import Decimal
from itertools import chain
@@ -53,7 +52,7 @@ class DryRunResults:
):
result = {
"type": "edit_transaction",
- "transaction": deepcopy(instance),
+ "transaction": instance.deepcopy(),
"action": action,
"old_value": old_value,
"new_value": new_value,
@@ -248,7 +247,7 @@ def check_for_transaction_rules(
if searched_transactions.exists():
transaction = searched_transactions.first()
existing = True
- starting_instance = deepcopy(transaction)
+ starting_instance = transaction.deepcopy()
_log("Found at least one matching transaction, using latest:")
_log("{}".format(pformat(model_to_dict(transaction))))
else:
@@ -322,82 +321,62 @@ def check_for_transaction_rules(
else:
transaction.category = TransactionCategory.objects.get(name=value)
- if dry_run:
- if not transaction.id:
- _log("Transaction would be created as:")
- else:
- _log("Trasanction would be updated as:")
-
- _log(
- "{}".format(
- pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
- )
- )
+ if not transaction.id:
+ _log("Transaction will be created as:")
else:
- if not transaction.id:
- _log("Transaction will be created as:")
- else:
- _log("Trasanction will be updated as:")
+ _log("Trasanction will be updated as:")
- _log(
- "{}".format(
- pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
- )
+ _log(
+ "{}".format(
+ pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
)
- transaction.save()
+ )
+ transaction.save()
# Handle M2M fields after save
tags = []
if processed_action.set_tags:
tags = simple.eval(processed_action.set_tags)
- if dry_run:
- _log(f" And tags would be set as: {tags}")
- else:
- _log(f" And tags will be set as: {tags}")
- transaction.tags.clear()
- if isinstance(tags, (list, tuple)):
- for tag in tags:
- if isinstance(tag, int):
- transaction.tags.add(TransactionTag.objects.get(id=tag))
- else:
- transaction.tags.add(TransactionTag.objects.get(name=tag))
- elif isinstance(tags, (int, str)):
- if isinstance(tags, int):
- transaction.tags.add(TransactionTag.objects.get(id=tags))
+ _log(f" And tags will be set as: {tags}")
+ transaction.tags.clear()
+ if isinstance(tags, (list, tuple)):
+ for tag in tags:
+ if isinstance(tag, int):
+ transaction.tags.add(TransactionTag.objects.get(id=tag))
else:
- transaction.tags.add(TransactionTag.objects.get(name=tags))
+ transaction.tags.add(TransactionTag.objects.get(name=tag))
+ elif isinstance(tags, (int, str)):
+ if isinstance(tags, int):
+ transaction.tags.add(TransactionTag.objects.get(id=tags))
+ else:
+ transaction.tags.add(TransactionTag.objects.get(name=tags))
entities = []
if processed_action.set_entities:
entities = simple.eval(processed_action.set_entities)
- if dry_run:
- _log(f" And entities would be set as: {entities}")
- else:
- _log(f" And entities will be set as: {entities}")
- transaction.entities.clear()
- if isinstance(entities, (list, tuple)):
- for entity in entities:
- if isinstance(entity, int):
- transaction.entities.add(
- TransactionEntity.objects.get(id=entity)
- )
- else:
- transaction.entities.add(
- TransactionEntity.objects.get(name=entity)
- )
- elif isinstance(entities, (int, str)):
- if isinstance(entities, int):
+ _log(f" And entities will be set as: {entities}")
+ transaction.entities.clear()
+ if isinstance(entities, (list, tuple)):
+ for entity in entities:
+ if isinstance(entity, int):
transaction.entities.add(
- TransactionEntity.objects.get(id=entities)
+ TransactionEntity.objects.get(id=entity)
)
else:
transaction.entities.add(
- TransactionEntity.objects.get(name=entities)
+ TransactionEntity.objects.get(name=entity)
)
+ elif isinstance(entities, (int, str)):
+ if isinstance(entities, int):
+ transaction.entities.add(TransactionEntity.objects.get(id=entities))
+ else:
+ transaction.entities.add(
+ TransactionEntity.objects.get(name=entities)
+ )
dry_run_results.update_or_create_transaction(
start_instance=starting_instance,
- end_instance=deepcopy(transaction),
+ end_instance=transaction.deepcopy(),
updated=existing,
action=processed_action,
query=search_query,
@@ -438,19 +417,19 @@ def check_for_transaction_rules(
transaction.category = category
elif field == TransactionRuleAction.Field.tags:
- if not dry_run:
- transaction.tags.clear()
+ transaction.tags.clear()
+
if isinstance(new_value, list):
for tag_value in new_value:
if isinstance(tag_value, int):
tag = TransactionTag.objects.get(id=tag_value)
- if not dry_run:
- transaction.tags.add(tag)
+
+ transaction.tags.add(tag)
tags.append(tag)
elif isinstance(tag_value, str):
tag = TransactionTag.objects.get(name=tag_value)
- if not dry_run:
- transaction.tags.add(tag)
+
+ transaction.tags.add(tag)
tags.append(tag)
elif isinstance(new_value, (int, str)):
@@ -459,24 +438,22 @@ def check_for_transaction_rules(
else:
tag = TransactionTag.objects.get(name=new_value)
- if not dry_run:
- transaction.tags.add(tag)
+ transaction.tags.add(tag)
tags.append(tag)
elif field == TransactionRuleAction.Field.entities:
- if not dry_run:
- transaction.entities.clear()
+ transaction.entities.clear()
if isinstance(new_value, list):
for entity_value in new_value:
if isinstance(entity_value, int):
entity = TransactionEntity.objects.get(id=entity_value)
- if not dry_run:
- transaction.entities.add(entity)
+
+ transaction.entities.add(entity)
entities.append(entity)
elif isinstance(entity_value, str):
entity = TransactionEntity.objects.get(name=entity_value)
- if not dry_run:
- transaction.entities.add(entity)
+
+ transaction.entities.add(entity)
entities.append(entity)
elif isinstance(new_value, (int, str)):
@@ -484,8 +461,8 @@ def check_for_transaction_rules(
entity = TransactionEntity.objects.get(id=new_value)
else:
entity = TransactionEntity.objects.get(name=new_value)
- if not dry_run:
- transaction.entities.add(entity)
+
+ transaction.entities.add(entity)
entities.append(entity)
else:
@@ -496,7 +473,7 @@ def check_for_transaction_rules(
)
dry_run_results.edit_transaction(
- instance=deepcopy(transaction),
+ instance=transaction.deepcopy(),
action=processed_action,
old_value=original_value,
new_value=new_value,
@@ -530,7 +507,7 @@ def check_for_transaction_rules(
# Regular transaction processing for creates and updates
instance = Transaction.objects.get(id=instance_id)
- dry_run_results.triggering_transaction(deepcopy(instance))
+ dry_run_results.triggering_transaction(instance.deepcopy())
functions = {
"relativedelta": relativedelta,
@@ -667,7 +644,7 @@ def check_for_transaction_rules(
level="error",
)
# Save at the end
- if not dry_run and signal != "transaction_deleted":
+ if signal != "transaction_deleted":
instance.save()
else:
_log(
@@ -702,7 +679,7 @@ def check_for_transaction_rules(
if rule.sequenced:
# Update names for next actions
simple.names.update(_get_names(instance))
- if not dry_run and signal != "transaction_deleted":
+ if signal != "transaction_deleted":
instance.save()
for action in update_or_create_actions:
@@ -741,7 +718,6 @@ def check_for_transaction_rules(
delete_current_user()
- if dry_run:
- return logs, dry_run_results.results
+ return logs, dry_run_results.results
return None
diff --git a/app/apps/rules/urls.py b/app/apps/rules/urls.py
index 99d272d..9ee0fb1 100644
--- a/app/apps/rules/urls.py
+++ b/app/apps/rules/urls.py
@@ -52,6 +52,11 @@ urlpatterns = [
views.dry_run_rule_deleted,
name="transaction_rule_dry_run_deleted",
),
+ path(
+ "rules/transaction//dry-run/updated/",
+ views.dry_run_rule_updated,
+ name="transaction_rule_dry_run_updated",
+ ),
path(
"rules/transaction//share/",
views.transaction_rule_share,
diff --git a/app/apps/rules/views.py b/app/apps/rules/views.py
index e02b85e..da49954 100644
--- a/app/apps/rules/views.py
+++ b/app/apps/rules/views.py
@@ -1,7 +1,10 @@
from itertools import chain
+from copy import deepcopy
+
from django.contrib import messages
from django.contrib.auth.decorators import login_required
+from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.utils.translation import gettext_lazy as _
@@ -14,6 +17,7 @@ from apps.rules.forms import (
UpdateOrCreateTransactionRuleActionForm,
DryRunCreatedTransacion,
DryRunDeletedTransacion,
+ DryRunUpdatedTransactionForm,
)
from apps.rules.models import (
TransactionRule,
@@ -25,6 +29,9 @@ from apps.common.forms import SharedObjectForm
from apps.common.decorators.demo import disabled_on_demo
from apps.rules.tasks import check_for_transaction_rules
from apps.common.middleware.thread_local import get_current_user
+from apps.rules.signals import transaction_created, transaction_updated
+from apps.rules.utils.transactions import serialize_transaction
+from apps.transactions.models import Transaction
@login_required
@@ -436,14 +443,28 @@ def dry_run_rule_created(request, pk):
if request.method == "POST":
form = DryRunCreatedTransacion(request.POST)
if form.is_valid():
- logs, results = check_for_transaction_rules(
- instance_id=form.cleaned_data["transaction"].id,
- signal="transaction_created",
- dry_run=True,
- rule_id=rule.id,
- user_id=get_current_user().id,
- )
- logs = "\n".join(logs)
+ try:
+ with transaction.atomic():
+ logs, results = check_for_transaction_rules(
+ instance_id=form.cleaned_data["transaction"].id,
+ signal="transaction_created",
+ dry_run=True,
+ rule_id=rule.id,
+ user_id=get_current_user().id,
+ )
+ logs = "\n".join(logs)
+
+ response = render(
+ request,
+ "rules/fragments/transaction_rule/dry_run/created.html",
+ {"form": form, "rule": rule, "logs": logs, "results": results},
+ )
+
+ raise Exception("ROLLBACK")
+ except Exception:
+ pass
+
+ return response
else:
form = DryRunCreatedTransacion()
@@ -467,14 +488,28 @@ def dry_run_rule_deleted(request, pk):
if request.method == "POST":
form = DryRunDeletedTransacion(request.POST)
if form.is_valid():
- logs, results = check_for_transaction_rules(
- instance_id=form.cleaned_data["transaction"].id,
- signal="transaction_deleted",
- dry_run=True,
- rule_id=rule.id,
- user_id=get_current_user().id,
- )
- logs = "\n".join(logs)
+ try:
+ with transaction.atomic():
+ logs, results = check_for_transaction_rules(
+ instance_id=form.cleaned_data["transaction"].id,
+ signal="transaction_deleted",
+ dry_run=True,
+ rule_id=rule.id,
+ user_id=get_current_user().id,
+ )
+ logs = "\n".join(logs)
+
+ response = render(
+ request,
+ "rules/fragments/transaction_rule/dry_run/created.html",
+ {"form": form, "rule": rule, "logs": logs, "results": results},
+ )
+
+ raise Exception("ROLLBACK")
+ except Exception:
+ pass
+
+ return response
else:
form = DryRunDeletedTransacion()
@@ -484,3 +519,66 @@ def dry_run_rule_deleted(request, pk):
"rules/fragments/transaction_rule/dry_run/deleted.html",
{"form": form, "rule": rule, "logs": logs, "results": results},
)
+
+
+@only_htmx
+@login_required
+@disabled_on_demo
+@require_http_methods(["GET", "POST"])
+def dry_run_rule_updated(request, pk):
+ rule = get_object_or_404(TransactionRule, id=pk)
+ logs = None
+ results = None
+
+ if request.method == "POST":
+ form = DryRunUpdatedTransactionForm(request.POST)
+ if form.is_valid():
+ base_transaction = Transaction.objects.get(
+ id=request.POST.get("transaction")
+ )
+ old_data = deepcopy(base_transaction)
+ try:
+ with transaction.atomic():
+ for field_name, value in form.cleaned_data.items():
+ if value or isinstance(
+ value, bool
+ ): # Only update fields that have been filled in the form
+ if field_name == "tags":
+ base_transaction.tags.set(value)
+ elif field_name == "entities":
+ base_transaction.entities.set(value)
+ else:
+ setattr(base_transaction, field_name, value)
+
+ base_transaction.save()
+
+ logs, results = check_for_transaction_rules(
+ instance_id=base_transaction.id,
+ signal="transaction_updated",
+ dry_run=True,
+ rule_id=rule.id,
+ user_id=get_current_user().id,
+ old_data=old_data,
+ )
+ logs = "\n".join(logs) if logs else ""
+
+ response = render(
+ request,
+ "rules/fragments/transaction_rule/dry_run/created.html",
+ {"form": form, "rule": rule, "logs": logs, "results": results},
+ )
+
+ # This will rollback the transaction
+ raise Exception("ROLLBACK")
+ except Exception:
+ pass
+
+ return response
+ else:
+ form = DryRunUpdatedTransactionForm(initial={"is_paid": None, "type": None})
+
+ return render(
+ request,
+ "rules/fragments/transaction_rule/dry_run/updated.html",
+ {"form": form, "rule": rule, "logs": logs, "results": results},
+ )
diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py
index 8fd9231..684e6d8 100644
--- a/app/apps/transactions/forms.py
+++ b/app/apps/transactions/forms.py
@@ -389,35 +389,115 @@ class QuickTransactionForm(forms.ModelForm):
)
-class BulkEditTransactionForm(TransactionForm):
- is_paid = forms.NullBooleanField(required=False)
+class BulkEditTransactionForm(forms.Form):
+ type = forms.ChoiceField(
+ choices=(Transaction.Type.choices),
+ required=False,
+ label=_("Type"),
+ )
+ is_paid = forms.NullBooleanField(
+ required=False,
+ label=_("Paid"),
+ )
+ account = DynamicModelChoiceField(
+ model=Account,
+ required=False,
+ label=_("Account"),
+ queryset=Account.objects.filter(is_archived=False),
+ widget=TomSelect(clear_button=False, group_by="group"),
+ )
+ date = forms.DateField(
+ label=_("Date"),
+ required=False,
+ widget=AirDatePickerInput(clear_button=False),
+ )
+ reference_date = forms.DateField(
+ widget=AirMonthYearPickerInput(),
+ label=_("Reference Date"),
+ required=False,
+ )
+ amount = forms.DecimalField(
+ max_digits=42,
+ decimal_places=30,
+ required=False,
+ label=_("Amount"),
+ widget=ArbitraryDecimalDisplayNumberInput(),
+ )
+ description = forms.CharField(
+ max_length=500, required=False, label=_("Description")
+ )
+ notes = forms.CharField(
+ required=False,
+ widget=forms.Textarea(attrs={"rows": 3}),
+ label=_("Notes"),
+ )
+ category = DynamicModelChoiceField(
+ create_field="name",
+ model=TransactionCategory,
+ required=False,
+ label=_("Category"),
+ queryset=TransactionCategory.objects.filter(active=True),
+ )
+ tags = DynamicModelMultipleChoiceField(
+ model=TransactionTag,
+ to_field_name="name",
+ create_field="name",
+ required=False,
+ label=_("Tags"),
+ queryset=TransactionTag.objects.filter(active=True),
+ )
+ entities = DynamicModelMultipleChoiceField(
+ model=TransactionEntity,
+ to_field_name="name",
+ create_field="name",
+ required=False,
+ label=_("Entities"),
+ queryset=TransactionEntity.objects.all(),
+ )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- # Make all fields optional
- for field_name, field in self.fields.items():
- field.required = False
- del self.helper.layout[-1] # Remove button
- del self.helper.layout[0:2] # Remove type, is_paid field
+ self.fields["account"].queryset = Account.objects.filter(
+ is_archived=False,
+ )
- self.helper.layout.insert(
- 0,
+ self.fields["category"].queryset = TransactionCategory.objects.filter(
+ active=True
+ )
+ self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
+ self.fields["entities"].queryset = TransactionEntity.objects.all()
+
+ self.helper = FormHelper()
+ self.helper.form_tag = False
+ self.helper.form_method = "post"
+ self.helper.layout = Layout(
Field(
"type",
template="transactions/widgets/unselectable_income_expense_toggle_buttons.html",
),
- )
-
- self.helper.layout.insert(
- 1,
Field(
"is_paid",
template="transactions/widgets/unselectable_paid_toggle_button.html",
),
- )
-
- self.helper.layout.append(
+ Row(
+ Column("account", css_class="form-group col-md-6 mb-0"),
+ Column("entities", css_class="form-group col-md-6 mb-0"),
+ css_class="form-row",
+ ),
+ Row(
+ Column(Field("date"), css_class="form-group col-md-6 mb-0"),
+ Column(Field("reference_date"), css_class="form-group col-md-6 mb-0"),
+ css_class="form-row",
+ ),
+ "description",
+ Field("amount", inputmode="decimal"),
+ Row(
+ Column("category", css_class="form-group col-md-6 mb-0"),
+ Column("tags", css_class="form-group col-md-6 mb-0"),
+ css_class="form-row",
+ ),
+ "notes",
FormActions(
NoClassSubmit(
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
@@ -425,6 +505,9 @@ class BulkEditTransactionForm(TransactionForm):
),
)
+ self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
+ self.fields["date"].widget = AirDatePickerInput(clear_button=False)
+
class TransferForm(forms.Form):
from_account = forms.ModelChoiceField(
diff --git a/app/apps/transactions/models.py b/app/apps/transactions/models.py
index f969997..cdbb069 100644
--- a/app/apps/transactions/models.py
+++ b/app/apps/transactions/models.py
@@ -462,6 +462,48 @@ class Transaction(OwnedObject):
description = self.description or _("No description")
return f"[{frmt_date}][{type_display}][{account}] {description} • {category} • {tags} • {amount}"
+ def deepcopy(self, memo=None):
+ """
+ Creates a deep copy of the transaction instance.
+
+ This method returns a new, unsaved Transaction instance with the same
+ values as the original, including its many-to-many relationships.
+ The primary key and any other unique fields are reset to avoid
+ database integrity errors upon saving.
+ """
+ if memo is None:
+ memo = {}
+
+ # Create a new instance of the class
+ new_obj = self.__class__()
+ memo[id(self)] = new_obj
+
+ # Copy all concrete fields from the original to the new object
+ for field in self._meta.concrete_fields:
+ # Skip the primary key to allow the database to generate a new one
+ if field.primary_key:
+ continue
+
+ # Reset any unique fields to None to avoid constraint violations
+ if field.unique and field.name == "internal_id":
+ setattr(new_obj, field.name, None)
+ continue
+
+ # Copy the value of the field
+ setattr(new_obj, field.name, getattr(self, field.name))
+
+ # Save the new object to the database to get a primary key
+ new_obj.save()
+
+ # Copy the many-to-many relationships
+ for field in self._meta.many_to_many:
+ source_manager = getattr(self, field.name)
+ destination_manager = getattr(new_obj, field.name)
+ # Set the M2M relationships for the new object
+ destination_manager.set(source_manager.all())
+
+ return new_obj
+
class InstallmentPlan(models.Model):
class Recurrence(models.TextChoices):
diff --git a/app/templates/rules/fragments/transaction_rule/dry_run/logs.html b/app/templates/rules/fragments/transaction_rule/dry_run/logs.html
deleted file mode 100644
index 8acb25c..0000000
--- a/app/templates/rules/fragments/transaction_rule/dry_run/logs.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
- {{ logs|linebreaks }}
-
-
-
diff --git a/app/templates/rules/fragments/transaction_rule/dry_run/updated.html b/app/templates/rules/fragments/transaction_rule/dry_run/updated.html
new file mode 100644
index 0000000..9e50bd6
--- /dev/null
+++ b/app/templates/rules/fragments/transaction_rule/dry_run/updated.html
@@ -0,0 +1,16 @@
+{% extends 'extends/offcanvas.html' %}
+{% load i18n %}
+{% load crispy_forms_tags %}
+
+{% block title %}{% translate 'Edit transaction rule' %}{% endblock %}
+
+{% block body %}
+
+
+
+ {% include 'rules/fragments/transaction_rule/dry_run/visual.html' with logs=logs results=results %}
+
+{% endblock %}
diff --git a/app/templates/rules/fragments/transaction_rule/dry_run/visual.html b/app/templates/rules/fragments/transaction_rule/dry_run/visual.html
index b7fcc59..c3cd50f 100644
--- a/app/templates/rules/fragments/transaction_rule/dry_run/visual.html
+++ b/app/templates/rules/fragments/transaction_rule/dry_run/visual.html
@@ -53,8 +53,7 @@
{{ result.new_value }}
+ :disable-selection="True">
{% endif %}
@@ -73,8 +72,7 @@
{% endif %}
+ :disable-selection="True">
{% endif %}
diff --git a/app/templates/rules/fragments/transaction_rule/view.html b/app/templates/rules/fragments/transaction_rule/view.html
index a8c5265..ebf19c5 100644
--- a/app/templates/rules/fragments/transaction_rule/view.html
+++ b/app/templates/rules/fragments/transaction_rule/view.html
@@ -124,17 +124,17 @@
{% if transaction_rule.on_create %}
{% trans 'On creation' %}
+ hx-target="#generic-offcanvas">{% trans 'Create' %}
{% endif %}
{% if transaction_rule.on_update %}
{% trans 'On update' %}
+ hx-get="{% url 'transaction_rule_dry_run_updated' pk=transaction_rule.id %}"
+ hx-target="#generic-offcanvas">{% trans 'Update' %}
{% endif %}
{% if transaction_rule.on_delete %}
{% trans 'On delete' %}
+ hx-target="#generic-offcanvas">{% trans 'Delete' %}
{% endif %}