This commit is contained in:
Herculino Trotta
2025-09-02 23:17:04 -03:00
parent d724300513
commit 2235bdeabb
10 changed files with 398 additions and 128 deletions

View File

@@ -1,16 +1,18 @@
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
from crispy_forms.bootstrap import FormActions, AccordionGroup from crispy_forms.bootstrap import FormActions, AccordionGroup
from crispy_forms.helper import FormHelper 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 import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ 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.crispy.submit import NoClassSubmit
from apps.common.widgets.tom_select import TomSelect, TransactionSelect from apps.common.widgets.tom_select import TomSelect, TransactionSelect
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
from apps.rules.models import TransactionRuleAction from apps.rules.models import TransactionRuleAction
from apps.common.fields.forms.dynamic_select import DynamicModelChoiceField from apps.common.fields.forms.dynamic_select import DynamicModelChoiceField
from apps.transactions.forms import BulkEditTransactionForm
from apps.transactions.models import Transaction 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): class DryRunDeletedTransacion(forms.Form):
transaction = DynamicModelChoiceField( 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("<hr/>"))
# 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
)

View File

@@ -1,7 +1,6 @@
import decimal import decimal
import logging import logging
import traceback import traceback
from copy import deepcopy
from datetime import datetime, date from datetime import datetime, date
from decimal import Decimal from decimal import Decimal
from itertools import chain from itertools import chain
@@ -53,7 +52,7 @@ class DryRunResults:
): ):
result = { result = {
"type": "edit_transaction", "type": "edit_transaction",
"transaction": deepcopy(instance), "transaction": instance.deepcopy(),
"action": action, "action": action,
"old_value": old_value, "old_value": old_value,
"new_value": new_value, "new_value": new_value,
@@ -248,7 +247,7 @@ def check_for_transaction_rules(
if searched_transactions.exists(): if searched_transactions.exists():
transaction = searched_transactions.first() transaction = searched_transactions.first()
existing = True existing = True
starting_instance = deepcopy(transaction) starting_instance = transaction.deepcopy()
_log("Found at least one matching transaction, using latest:") _log("Found at least one matching transaction, using latest:")
_log("{}".format(pformat(model_to_dict(transaction)))) _log("{}".format(pformat(model_to_dict(transaction))))
else: else:
@@ -322,82 +321,62 @@ def check_for_transaction_rules(
else: else:
transaction.category = TransactionCategory.objects.get(name=value) transaction.category = TransactionCategory.objects.get(name=value)
if dry_run: if not transaction.id:
if not transaction.id: _log("Transaction will be created as:")
_log("Transaction would be created as:")
else:
_log("Trasanction would be updated as:")
_log(
"{}".format(
pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
)
)
else: else:
if not transaction.id: _log("Trasanction will be updated as:")
_log("Transaction will be created as:")
else:
_log("Trasanction will be updated as:")
_log( _log(
"{}".format( "{}".format(
pformat(model_to_dict(transaction, exclude=["tags", "entities"])), pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
)
) )
transaction.save() )
transaction.save()
# Handle M2M fields after save # Handle M2M fields after save
tags = [] tags = []
if processed_action.set_tags: if processed_action.set_tags:
tags = simple.eval(processed_action.set_tags) tags = simple.eval(processed_action.set_tags)
if dry_run: _log(f" And tags will be set as: {tags}")
_log(f" And tags would be set as: {tags}") transaction.tags.clear()
else: if isinstance(tags, (list, tuple)):
_log(f" And tags will be set as: {tags}") for tag in tags:
transaction.tags.clear() if isinstance(tag, int):
if isinstance(tags, (list, tuple)): transaction.tags.add(TransactionTag.objects.get(id=tag))
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))
else: 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 = [] entities = []
if processed_action.set_entities: if processed_action.set_entities:
entities = simple.eval(processed_action.set_entities) entities = simple.eval(processed_action.set_entities)
if dry_run: _log(f" And entities will be set as: {entities}")
_log(f" And entities would be set as: {entities}") transaction.entities.clear()
else: if isinstance(entities, (list, tuple)):
_log(f" And entities will be set as: {entities}") for entity in entities:
transaction.entities.clear() if isinstance(entity, int):
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):
transaction.entities.add( transaction.entities.add(
TransactionEntity.objects.get(id=entities) TransactionEntity.objects.get(id=entity)
) )
else: else:
transaction.entities.add( 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( dry_run_results.update_or_create_transaction(
start_instance=starting_instance, start_instance=starting_instance,
end_instance=deepcopy(transaction), end_instance=transaction.deepcopy(),
updated=existing, updated=existing,
action=processed_action, action=processed_action,
query=search_query, query=search_query,
@@ -438,19 +417,19 @@ def check_for_transaction_rules(
transaction.category = category transaction.category = category
elif field == TransactionRuleAction.Field.tags: elif field == TransactionRuleAction.Field.tags:
if not dry_run: transaction.tags.clear()
transaction.tags.clear()
if isinstance(new_value, list): if isinstance(new_value, list):
for tag_value in new_value: for tag_value in new_value:
if isinstance(tag_value, int): if isinstance(tag_value, int):
tag = TransactionTag.objects.get(id=tag_value) tag = TransactionTag.objects.get(id=tag_value)
if not dry_run:
transaction.tags.add(tag) transaction.tags.add(tag)
tags.append(tag) tags.append(tag)
elif isinstance(tag_value, str): elif isinstance(tag_value, str):
tag = TransactionTag.objects.get(name=tag_value) tag = TransactionTag.objects.get(name=tag_value)
if not dry_run:
transaction.tags.add(tag) transaction.tags.add(tag)
tags.append(tag) tags.append(tag)
elif isinstance(new_value, (int, str)): elif isinstance(new_value, (int, str)):
@@ -459,24 +438,22 @@ def check_for_transaction_rules(
else: else:
tag = TransactionTag.objects.get(name=new_value) tag = TransactionTag.objects.get(name=new_value)
if not dry_run: transaction.tags.add(tag)
transaction.tags.add(tag)
tags.append(tag) tags.append(tag)
elif field == TransactionRuleAction.Field.entities: elif field == TransactionRuleAction.Field.entities:
if not dry_run: transaction.entities.clear()
transaction.entities.clear()
if isinstance(new_value, list): if isinstance(new_value, list):
for entity_value in new_value: for entity_value in new_value:
if isinstance(entity_value, int): if isinstance(entity_value, int):
entity = TransactionEntity.objects.get(id=entity_value) entity = TransactionEntity.objects.get(id=entity_value)
if not dry_run:
transaction.entities.add(entity) transaction.entities.add(entity)
entities.append(entity) entities.append(entity)
elif isinstance(entity_value, str): elif isinstance(entity_value, str):
entity = TransactionEntity.objects.get(name=entity_value) entity = TransactionEntity.objects.get(name=entity_value)
if not dry_run:
transaction.entities.add(entity) transaction.entities.add(entity)
entities.append(entity) entities.append(entity)
elif isinstance(new_value, (int, str)): elif isinstance(new_value, (int, str)):
@@ -484,8 +461,8 @@ def check_for_transaction_rules(
entity = TransactionEntity.objects.get(id=new_value) entity = TransactionEntity.objects.get(id=new_value)
else: else:
entity = TransactionEntity.objects.get(name=new_value) entity = TransactionEntity.objects.get(name=new_value)
if not dry_run:
transaction.entities.add(entity) transaction.entities.add(entity)
entities.append(entity) entities.append(entity)
else: else:
@@ -496,7 +473,7 @@ def check_for_transaction_rules(
) )
dry_run_results.edit_transaction( dry_run_results.edit_transaction(
instance=deepcopy(transaction), instance=transaction.deepcopy(),
action=processed_action, action=processed_action,
old_value=original_value, old_value=original_value,
new_value=new_value, new_value=new_value,
@@ -530,7 +507,7 @@ def check_for_transaction_rules(
# Regular transaction processing for creates and updates # Regular transaction processing for creates and updates
instance = Transaction.objects.get(id=instance_id) instance = Transaction.objects.get(id=instance_id)
dry_run_results.triggering_transaction(deepcopy(instance)) dry_run_results.triggering_transaction(instance.deepcopy())
functions = { functions = {
"relativedelta": relativedelta, "relativedelta": relativedelta,
@@ -667,7 +644,7 @@ def check_for_transaction_rules(
level="error", level="error",
) )
# Save at the end # Save at the end
if not dry_run and signal != "transaction_deleted": if signal != "transaction_deleted":
instance.save() instance.save()
else: else:
_log( _log(
@@ -702,7 +679,7 @@ def check_for_transaction_rules(
if rule.sequenced: if rule.sequenced:
# Update names for next actions # Update names for next actions
simple.names.update(_get_names(instance)) simple.names.update(_get_names(instance))
if not dry_run and signal != "transaction_deleted": if signal != "transaction_deleted":
instance.save() instance.save()
for action in update_or_create_actions: for action in update_or_create_actions:
@@ -741,7 +718,6 @@ def check_for_transaction_rules(
delete_current_user() delete_current_user()
if dry_run: return logs, dry_run_results.results
return logs, dry_run_results.results
return None return None

View File

@@ -52,6 +52,11 @@ urlpatterns = [
views.dry_run_rule_deleted, views.dry_run_rule_deleted,
name="transaction_rule_dry_run_deleted", name="transaction_rule_dry_run_deleted",
), ),
path(
"rules/transaction/<int:pk>/dry-run/updated/",
views.dry_run_rule_updated,
name="transaction_rule_dry_run_updated",
),
path( path(
"rules/transaction/<int:pk>/share/", "rules/transaction/<int:pk>/share/",
views.transaction_rule_share, views.transaction_rule_share,

View File

@@ -1,7 +1,10 @@
from itertools import chain from itertools import chain
from copy import deepcopy
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -14,6 +17,7 @@ from apps.rules.forms import (
UpdateOrCreateTransactionRuleActionForm, UpdateOrCreateTransactionRuleActionForm,
DryRunCreatedTransacion, DryRunCreatedTransacion,
DryRunDeletedTransacion, DryRunDeletedTransacion,
DryRunUpdatedTransactionForm,
) )
from apps.rules.models import ( from apps.rules.models import (
TransactionRule, TransactionRule,
@@ -25,6 +29,9 @@ from apps.common.forms import SharedObjectForm
from apps.common.decorators.demo import disabled_on_demo from apps.common.decorators.demo import disabled_on_demo
from apps.rules.tasks import check_for_transaction_rules from apps.rules.tasks import check_for_transaction_rules
from apps.common.middleware.thread_local import get_current_user 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 @login_required
@@ -436,14 +443,28 @@ def dry_run_rule_created(request, pk):
if request.method == "POST": if request.method == "POST":
form = DryRunCreatedTransacion(request.POST) form = DryRunCreatedTransacion(request.POST)
if form.is_valid(): if form.is_valid():
logs, results = check_for_transaction_rules( try:
instance_id=form.cleaned_data["transaction"].id, with transaction.atomic():
signal="transaction_created", logs, results = check_for_transaction_rules(
dry_run=True, instance_id=form.cleaned_data["transaction"].id,
rule_id=rule.id, signal="transaction_created",
user_id=get_current_user().id, dry_run=True,
) rule_id=rule.id,
logs = "\n".join(logs) 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: else:
form = DryRunCreatedTransacion() form = DryRunCreatedTransacion()
@@ -467,14 +488,28 @@ def dry_run_rule_deleted(request, pk):
if request.method == "POST": if request.method == "POST":
form = DryRunDeletedTransacion(request.POST) form = DryRunDeletedTransacion(request.POST)
if form.is_valid(): if form.is_valid():
logs, results = check_for_transaction_rules( try:
instance_id=form.cleaned_data["transaction"].id, with transaction.atomic():
signal="transaction_deleted", logs, results = check_for_transaction_rules(
dry_run=True, instance_id=form.cleaned_data["transaction"].id,
rule_id=rule.id, signal="transaction_deleted",
user_id=get_current_user().id, dry_run=True,
) rule_id=rule.id,
logs = "\n".join(logs) 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: else:
form = DryRunDeletedTransacion() form = DryRunDeletedTransacion()
@@ -484,3 +519,66 @@ def dry_run_rule_deleted(request, pk):
"rules/fragments/transaction_rule/dry_run/deleted.html", "rules/fragments/transaction_rule/dry_run/deleted.html",
{"form": form, "rule": rule, "logs": logs, "results": results}, {"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},
)

View File

@@ -389,35 +389,115 @@ class QuickTransactionForm(forms.ModelForm):
) )
class BulkEditTransactionForm(TransactionForm): class BulkEditTransactionForm(forms.Form):
is_paid = forms.NullBooleanField(required=False) 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): def __init__(self, *args, **kwargs):
super().__init__(*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 self.fields["account"].queryset = Account.objects.filter(
del self.helper.layout[0:2] # Remove type, is_paid field is_archived=False,
)
self.helper.layout.insert( self.fields["category"].queryset = TransactionCategory.objects.filter(
0, 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( Field(
"type", "type",
template="transactions/widgets/unselectable_income_expense_toggle_buttons.html", template="transactions/widgets/unselectable_income_expense_toggle_buttons.html",
), ),
)
self.helper.layout.insert(
1,
Field( Field(
"is_paid", "is_paid",
template="transactions/widgets/unselectable_paid_toggle_button.html", template="transactions/widgets/unselectable_paid_toggle_button.html",
), ),
) Row(
Column("account", css_class="form-group col-md-6 mb-0"),
self.helper.layout.append( 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( FormActions(
NoClassSubmit( NoClassSubmit(
"submit", _("Update"), css_class="btn btn-outline-primary w-100" "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): class TransferForm(forms.Form):
from_account = forms.ModelChoiceField( from_account = forms.ModelChoiceField(

View File

@@ -462,6 +462,48 @@ class Transaction(OwnedObject):
description = self.description or _("No description") description = self.description or _("No description")
return f"[{frmt_date}][{type_display}][{account}] {description}{category}{tags}{amount}" 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 InstallmentPlan(models.Model):
class Recurrence(models.TextChoices): class Recurrence(models.TextChoices):

View File

@@ -1,7 +0,0 @@
<div class="card tw:max-h-full tw:overflow-auto tw:overflow-x-auto">
<div class="card-body">
<pre>
{{ logs|linebreaks }}
</pre>
</div>
</div>

View File

@@ -0,0 +1,16 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit transaction rule' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'transaction_rule_dry_run_updated' pk=rule.id %}" hx-target="#generic-offcanvas"
hx-indicator="#dry-run-updated-result, closest form" class="show-loading" novalidate>
{% crispy form %}
</form>
<hr>
<div id="dry-run-updated-result" class="show-loading">
{% include 'rules/fragments/transaction_rule/dry_run/visual.html' with logs=logs results=results %}
</div>
{% endblock %}

View File

@@ -53,8 +53,7 @@
<span class="badge text-bg-secondary">{{ result.new_value }}</span> <span class="badge text-bg-secondary">{{ result.new_value }}</span>
</div> </div>
<c-transaction.item :transaction="result.transaction" :dummy="True" <c-transaction.item :transaction="result.transaction" :dummy="True"
:disable-selection="True" :overriden_tags="result.tags" :disable-selection="True"></c-transaction.item>
:overriden_entities="result.entities"></c-transaction.item>
</div> </div>
{% endif %} {% endif %}
@@ -73,8 +72,7 @@
{% endif %} {% endif %}
<div class="text-center h3 my-2"><i class="fa-solid fa-arrow-down"></i></div> <div class="text-center h3 my-2"><i class="fa-solid fa-arrow-down"></i></div>
<c-transaction.item :transaction="result.end_transaction" :dummy="True" <c-transaction.item :transaction="result.end_transaction" :dummy="True"
:disable-selection="True" :overriden_tags="result.tags" :disable-selection="True"></c-transaction.item>
:overriden_entities="result.entities"></c-transaction.item>
</div> </div>
{% endif %} {% endif %}

View File

@@ -124,17 +124,17 @@
{% if transaction_rule.on_create %} {% if transaction_rule.on_create %}
<li><a class="dropdown-item" role="link" href="#" <li><a class="dropdown-item" role="link" href="#"
hx-get="{% url 'transaction_rule_dry_run_created' pk=transaction_rule.id %}" hx-get="{% url 'transaction_rule_dry_run_created' pk=transaction_rule.id %}"
hx-target="#generic-offcanvas">{% trans 'On creation' %}</a></li> hx-target="#generic-offcanvas">{% trans 'Create' %}</a></li>
{% endif %} {% endif %}
{% if transaction_rule.on_update %} {% if transaction_rule.on_update %}
<li><a class="dropdown-item" role="link" href="#" <li><a class="dropdown-item" role="link" href="#"
hx-get="{% url 'transaction_rule_dry_run_deleted' pk=transaction_rule.id %}" hx-get="{% url 'transaction_rule_dry_run_updated' pk=transaction_rule.id %}"
hx-target="#generic-offcanvas">{% trans 'On update' %}</a></li> hx-target="#generic-offcanvas">{% trans 'Update' %}</a></li>
{% endif %} {% endif %}
{% if transaction_rule.on_delete %} {% if transaction_rule.on_delete %}
<li><a class="dropdown-item" role="link" href="#" <li><a class="dropdown-item" role="link" href="#"
hx-get="{% url 'transaction_rule_dry_run_deleted' pk=transaction_rule.id %}" hx-get="{% url 'transaction_rule_dry_run_deleted' pk=transaction_rule.id %}"
hx-target="#generic-offcanvas">{% trans 'On delete' %}</a></li> hx-target="#generic-offcanvas">{% trans 'Delete' %}</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>