mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-03-24 02:11:18 +01:00
@@ -1,3 +1,5 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from rest_framework import viewsets
|
||||
|
||||
from apps.api.custom.pagination import CustomPageNumberPagination
|
||||
@@ -30,8 +32,9 @@ class TransactionViewSet(viewsets.ModelViewSet):
|
||||
transaction_created.send(sender=instance)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
old_data = deepcopy(Transaction.objects.get(pk=serializer.data["pk"]))
|
||||
instance = serializer.save()
|
||||
transaction_updated.send(sender=instance)
|
||||
transaction_updated.send(sender=instance, old_data=old_data)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
kwargs["partial"] = True
|
||||
|
||||
@@ -9,5 +9,8 @@ def truncate_decimal(value, decimal_places):
|
||||
:param decimal_places: The number of decimal places to keep
|
||||
:return: Truncated Decimal value
|
||||
"""
|
||||
if isinstance(value, (int, float)):
|
||||
value = Decimal(str(value))
|
||||
|
||||
multiplier = Decimal(10**decimal_places)
|
||||
return (value * multiplier).to_integral_value(rounding=ROUND_DOWN) / multiplier
|
||||
|
||||
@@ -40,12 +40,6 @@ class CurrencyTests(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
currency.full_clean()
|
||||
|
||||
def test_currency_unique_code(self):
|
||||
"""Test that currency codes must be unique"""
|
||||
Currency.objects.create(code="USD", name="US Dollar", decimal_places=2)
|
||||
with self.assertRaises(IntegrityError):
|
||||
Currency.objects.create(code="USD", name="Another Dollar", decimal_places=2)
|
||||
|
||||
def test_currency_unique_name(self):
|
||||
"""Test that currency names must be unique"""
|
||||
Currency.objects.create(code="USD", name="US Dollar", decimal_places=2)
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
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.tom_select import TomSelect
|
||||
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
|
||||
|
||||
|
||||
class TransactionRuleForm(forms.ModelForm):
|
||||
@@ -40,6 +44,8 @@ class TransactionRuleForm(forms.ModelForm):
|
||||
Column(Switch("on_create")),
|
||||
Column(Switch("on_delete")),
|
||||
),
|
||||
"order",
|
||||
Switch("sequenced"),
|
||||
"description",
|
||||
"trigger",
|
||||
)
|
||||
@@ -149,6 +155,7 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
"search_category_operator": TomSelect(clear_button=False),
|
||||
"search_internal_note_operator": TomSelect(clear_button=False),
|
||||
"search_internal_id_operator": TomSelect(clear_button=False),
|
||||
"search_mute_operator": TomSelect(clear_button=False),
|
||||
}
|
||||
|
||||
labels = {
|
||||
@@ -166,6 +173,7 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
"search_internal_id_operator": _("Operator"),
|
||||
"search_tags_operator": _("Operator"),
|
||||
"search_entities_operator": _("Operator"),
|
||||
"search_mute_operator": _("Operator"),
|
||||
"search_account": _("Account"),
|
||||
"search_type": _("Type"),
|
||||
"search_is_paid": _("Paid"),
|
||||
@@ -179,6 +187,7 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
"search_internal_id": _("Internal ID"),
|
||||
"search_tags": _("Tags"),
|
||||
"search_entities": _("Entities"),
|
||||
"search_mute": _("Mute"),
|
||||
"set_account": _("Account"),
|
||||
"set_type": _("Type"),
|
||||
"set_is_paid": _("Paid"),
|
||||
@@ -192,6 +201,7 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
"set_category": _("Category"),
|
||||
"set_internal_note": _("Internal Note"),
|
||||
"set_internal_id": _("Internal ID"),
|
||||
"set_mute": _("Mute"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -228,6 +238,16 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_mute_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_mute", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_account_operator"),
|
||||
@@ -344,6 +364,7 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
_("Set Values"),
|
||||
Field("set_type", rows=1),
|
||||
Field("set_is_paid", rows=1),
|
||||
Field("set_mute", rows=1),
|
||||
Field("set_account", rows=1),
|
||||
Field("set_entities", rows=1),
|
||||
Field("set_date", rows=1),
|
||||
@@ -385,3 +406,112 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class DryRunCreatedTransacion(forms.Form):
|
||||
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 = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
self.helper.layout = Layout(
|
||||
"transaction",
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class DryRunDeletedTransacion(forms.Form):
|
||||
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 = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
self.helper.layout = Layout(
|
||||
"transaction",
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
18
app/apps/rules/migrations/0016_transactionrule_sequenced.py
Normal file
18
app/apps/rules/migrations/0016_transactionrule_sequenced.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-31 18:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rules', '0015_alter_transactionruleaction_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transactionrule',
|
||||
name='sequenced',
|
||||
field=models.BooleanField(default=False, verbose_name='Sequenced'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-31 19:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rules', '0016_transactionrule_sequenced'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='updateorcreatetransactionruleaction',
|
||||
name='search_mute',
|
||||
field=models.TextField(blank=True, verbose_name='Search Mute'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='updateorcreatetransactionruleaction',
|
||||
name='search_mute_operator',
|
||||
field=models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Mute Operator'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='updateorcreatetransactionruleaction',
|
||||
name='set_mute',
|
||||
field=models.TextField(blank=True, verbose_name='Mute'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactionruleaction',
|
||||
name='field',
|
||||
field=models.CharField(choices=[('account', 'Account'), ('type', 'Type'), ('is_paid', 'Paid'), ('date', 'Date'), ('reference_date', 'Reference Date'), ('mute', 'Mute'), ('amount', 'Amount'), ('description', 'Description'), ('notes', 'Notes'), ('category', 'Category'), ('tags', 'Tags'), ('entities', 'Entities'), ('internal_nome', 'Internal Note'), ('internal_id', 'Internal ID')], max_length=50, verbose_name='Field'),
|
||||
),
|
||||
]
|
||||
18
app/apps/rules/migrations/0018_transactionrule_order.py
Normal file
18
app/apps/rules/migrations/0018_transactionrule_order.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.5 on 2025-09-02 14:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rules', '0017_updateorcreatetransactionruleaction_search_mute_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transactionrule',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Order'),
|
||||
),
|
||||
]
|
||||
@@ -13,6 +13,11 @@ class TransactionRule(SharedObject):
|
||||
name = models.CharField(max_length=100, verbose_name=_("Name"))
|
||||
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
|
||||
trigger = models.TextField(verbose_name=_("Trigger"))
|
||||
sequenced = models.BooleanField(
|
||||
verbose_name=_("Sequenced"),
|
||||
default=False,
|
||||
)
|
||||
order = models.PositiveIntegerField(default=0, verbose_name=_("Order"))
|
||||
|
||||
objects = SharedObjectManager()
|
||||
all_objects = models.Manager() # Unfiltered manager
|
||||
@@ -32,12 +37,15 @@ class TransactionRuleAction(models.Model):
|
||||
is_paid = "is_paid", _("Paid")
|
||||
date = "date", _("Date")
|
||||
reference_date = "reference_date", _("Reference Date")
|
||||
mute = "mute", _("Mute")
|
||||
amount = "amount", _("Amount")
|
||||
description = "description", _("Description")
|
||||
notes = "notes", _("Notes")
|
||||
category = "category", _("Category")
|
||||
tags = "tags", _("Tags")
|
||||
entities = "entities", _("Entities")
|
||||
internal_note = "internal_nome", _("Internal Note")
|
||||
internal_id = "internal_id", _("Internal ID")
|
||||
|
||||
rule = models.ForeignKey(
|
||||
TransactionRule,
|
||||
@@ -243,6 +251,17 @@ class UpdateOrCreateTransactionRuleAction(models.Model):
|
||||
verbose_name="Internal ID Operator",
|
||||
)
|
||||
|
||||
search_mute = models.TextField(
|
||||
verbose_name="Search Mute",
|
||||
blank=True,
|
||||
)
|
||||
search_mute_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name="Mute Operator",
|
||||
)
|
||||
|
||||
# Set fields
|
||||
set_account = models.TextField(
|
||||
verbose_name=_("Account"),
|
||||
@@ -296,6 +315,11 @@ class UpdateOrCreateTransactionRuleAction(models.Model):
|
||||
verbose_name=_("Tags"),
|
||||
blank=True,
|
||||
)
|
||||
set_mute = models.TextField(
|
||||
verbose_name=_("Mute"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
order = models.PositiveIntegerField(default=0, verbose_name=_("Order"))
|
||||
|
||||
class Meta:
|
||||
@@ -337,6 +361,10 @@ class UpdateOrCreateTransactionRuleAction(models.Model):
|
||||
value = simple.eval(self.search_is_paid)
|
||||
search_query &= add_to_query("is_paid", value, self.search_is_paid_operator)
|
||||
|
||||
if self.search_mute:
|
||||
value = simple.eval(self.search_mute)
|
||||
search_query &= add_to_query("mute", value, self.search_mute_operator)
|
||||
|
||||
if self.search_date:
|
||||
value = simple.eval(self.search_date)
|
||||
search_query &= add_to_query("date", value, self.search_date_operator)
|
||||
|
||||
@@ -9,40 +9,17 @@ from apps.transactions.models import (
|
||||
)
|
||||
from apps.rules.tasks import check_for_transaction_rules
|
||||
from apps.common.middleware.thread_local import get_current_user
|
||||
from apps.rules.utils.transactions import serialize_transaction
|
||||
|
||||
|
||||
@receiver(transaction_created)
|
||||
@receiver(transaction_updated)
|
||||
@receiver(transaction_deleted)
|
||||
def transaction_changed_receiver(sender: Transaction, signal, **kwargs):
|
||||
old_data = kwargs.get("old_data")
|
||||
if signal is transaction_deleted:
|
||||
# Serialize transaction data for processing
|
||||
transaction_data = {
|
||||
"id": sender.id,
|
||||
"account": (sender.account.id, sender.account.name),
|
||||
"account_group": (
|
||||
sender.account.group.id if sender.account.group else None,
|
||||
sender.account.group.name if sender.account.group else None,
|
||||
),
|
||||
"type": str(sender.type),
|
||||
"is_paid": sender.is_paid,
|
||||
"is_asset": sender.account.is_asset,
|
||||
"is_archived": sender.account.is_archived,
|
||||
"category": (
|
||||
sender.category.id if sender.category else None,
|
||||
sender.category.name if sender.category else None,
|
||||
),
|
||||
"date": sender.date.isoformat(),
|
||||
"reference_date": sender.reference_date.isoformat(),
|
||||
"amount": str(sender.amount),
|
||||
"description": sender.description,
|
||||
"notes": sender.notes,
|
||||
"tags": list(sender.tags.values_list("id", "name")),
|
||||
"entities": list(sender.entities.values_list("id", "name")),
|
||||
"deleted": True,
|
||||
"internal_note": sender.internal_note,
|
||||
"internal_id": sender.internal_id,
|
||||
}
|
||||
transaction_data = serialize_transaction(sender, deleted=True)
|
||||
|
||||
check_for_transaction_rules.defer(
|
||||
transaction_data=transaction_data,
|
||||
@@ -59,6 +36,9 @@ def transaction_changed_receiver(sender: Transaction, signal, **kwargs):
|
||||
dca_entry.amount_received = sender.amount
|
||||
dca_entry.save()
|
||||
|
||||
if signal is transaction_updated and old_data:
|
||||
old_data = serialize_transaction(old_data, deleted=False)
|
||||
|
||||
check_for_transaction_rules.defer(
|
||||
instance_id=sender.id,
|
||||
user_id=get_current_user().id,
|
||||
@@ -67,4 +47,5 @@ def transaction_changed_receiver(sender: Transaction, signal, **kwargs):
|
||||
if signal is transaction_created
|
||||
else "transaction_updated"
|
||||
),
|
||||
old_data=old_data,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,21 @@ urlpatterns = [
|
||||
views.transaction_rule_take_ownership,
|
||||
name="transaction_rule_take_ownership",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/<int:pk>/dry-run/created/",
|
||||
views.dry_run_rule_created,
|
||||
name="transaction_rule_dry_run_created",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/<int:pk>/dry-run/deleted/",
|
||||
views.dry_run_rule_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(
|
||||
"rules/transaction/<int:pk>/share/",
|
||||
views.transaction_rule_share,
|
||||
|
||||
@@ -56,3 +56,33 @@ class TransactionsGetter:
|
||||
output_field=DecimalField(),
|
||||
)
|
||||
)["balance"]
|
||||
|
||||
|
||||
def serialize_transaction(sender: Transaction, deleted: bool):
|
||||
return {
|
||||
"id": sender.id,
|
||||
"account": (sender.account.id, sender.account.name),
|
||||
"account_group": (
|
||||
sender.account.group.id if sender.account.group else None,
|
||||
sender.account.group.name if sender.account.group else None,
|
||||
),
|
||||
"type": str(sender.type),
|
||||
"is_paid": sender.is_paid,
|
||||
"is_asset": sender.account.is_asset,
|
||||
"is_archived": sender.account.is_archived,
|
||||
"category": (
|
||||
sender.category.id if sender.category else None,
|
||||
sender.category.name if sender.category else None,
|
||||
),
|
||||
"date": sender.date.isoformat(),
|
||||
"reference_date": sender.reference_date.isoformat(),
|
||||
"amount": str(sender.amount),
|
||||
"description": sender.description,
|
||||
"notes": sender.notes,
|
||||
"tags": list(sender.tags.values_list("id", "name")),
|
||||
"entities": list(sender.entities.values_list("id", "name")),
|
||||
"deleted": deleted,
|
||||
"internal_note": sender.internal_note,
|
||||
"internal_id": sender.internal_id,
|
||||
"mute": sender.mute,
|
||||
}
|
||||
|
||||
@@ -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 _
|
||||
@@ -12,6 +15,9 @@ from apps.rules.forms import (
|
||||
TransactionRuleForm,
|
||||
TransactionRuleActionForm,
|
||||
UpdateOrCreateTransactionRuleActionForm,
|
||||
DryRunCreatedTransacion,
|
||||
DryRunDeletedTransacion,
|
||||
DryRunUpdatedTransactionForm,
|
||||
)
|
||||
from apps.rules.models import (
|
||||
TransactionRule,
|
||||
@@ -21,6 +27,11 @@ from apps.rules.models import (
|
||||
from apps.common.models import SharedObject
|
||||
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
|
||||
@@ -38,7 +49,7 @@ def rules_index(request):
|
||||
@disabled_on_demo
|
||||
@require_http_methods(["GET"])
|
||||
def rules_list(request):
|
||||
transaction_rules = TransactionRule.objects.all().order_by("id")
|
||||
transaction_rules = TransactionRule.objects.all().order_by("order", "id")
|
||||
return render(
|
||||
request,
|
||||
"rules/fragments/list.html",
|
||||
@@ -143,7 +154,9 @@ 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()
|
||||
update_or_create_actions = (
|
||||
transaction_rule.update_or_create_transaction_actions.all()
|
||||
)
|
||||
|
||||
all_actions = sorted(
|
||||
chain(edit_actions, update_or_create_actions),
|
||||
@@ -416,3 +429,156 @@ def update_or_create_transaction_rule_action_delete(request, pk):
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@disabled_on_demo
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def dry_run_rule_created(request, pk):
|
||||
rule = get_object_or_404(TransactionRule, id=pk)
|
||||
logs = None
|
||||
results = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = DryRunCreatedTransacion(request.POST)
|
||||
if form.is_valid():
|
||||
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()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"rules/fragments/transaction_rule/dry_run/created.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_deleted(request, pk):
|
||||
rule = get_object_or_404(TransactionRule, id=pk)
|
||||
logs = None
|
||||
results = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = DryRunDeletedTransacion(request.POST)
|
||||
if form.is_valid():
|
||||
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()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"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},
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
|
||||
from crispy_forms.bootstrap import FormActions, AccordionGroup, AppendedText
|
||||
from crispy_forms.helper import FormHelper
|
||||
@@ -239,11 +241,16 @@ class TransactionForm(forms.ModelForm):
|
||||
def save(self, **kwargs):
|
||||
is_new = not self.instance.id
|
||||
|
||||
if not is_new:
|
||||
old_data = deepcopy(Transaction.objects.get(pk=self.instance.id))
|
||||
else:
|
||||
old_data = None
|
||||
|
||||
instance = super().save(**kwargs)
|
||||
if is_new:
|
||||
transaction_created.send(sender=instance)
|
||||
else:
|
||||
transaction_updated.send(sender=instance)
|
||||
transaction_updated.send(sender=instance, old_data=old_data)
|
||||
|
||||
return instance
|
||||
|
||||
@@ -382,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"
|
||||
@@ -418,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(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.conf import settings
|
||||
@@ -33,13 +34,13 @@ transaction_deleted = Signal()
|
||||
|
||||
class SoftDeleteQuerySet(models.QuerySet):
|
||||
@staticmethod
|
||||
def _emit_signals(instances, created=False):
|
||||
def _emit_signals(instances, created=False, old_data=None):
|
||||
"""Helper to emit signals for multiple instances"""
|
||||
for instance in instances:
|
||||
for i, instance in enumerate(instances):
|
||||
if created:
|
||||
transaction_created.send(sender=instance)
|
||||
else:
|
||||
transaction_updated.send(sender=instance)
|
||||
transaction_updated.send(sender=instance, old_data=old_data[i])
|
||||
|
||||
def bulk_create(self, objs, emit_signal=True, **kwargs):
|
||||
instances = super().bulk_create(objs, **kwargs)
|
||||
@@ -50,22 +51,25 @@ class SoftDeleteQuerySet(models.QuerySet):
|
||||
return instances
|
||||
|
||||
def bulk_update(self, objs, fields, emit_signal=True, **kwargs):
|
||||
old_data = deepcopy(objs)
|
||||
result = super().bulk_update(objs, fields, **kwargs)
|
||||
|
||||
if emit_signal:
|
||||
self._emit_signals(objs, created=False)
|
||||
self._emit_signals(objs, created=False, old_data=old_data)
|
||||
|
||||
return result
|
||||
|
||||
def update(self, emit_signal=True, **kwargs):
|
||||
# Get instances before update
|
||||
instances = list(self)
|
||||
old_data = deepcopy(instances)
|
||||
|
||||
result = super().update(**kwargs)
|
||||
|
||||
if emit_signal:
|
||||
# Refresh instances to get new values
|
||||
refreshed = self.model.objects.filter(pk__in=[obj.pk for obj in instances])
|
||||
self._emit_signals(refreshed, created=False)
|
||||
self._emit_signals(refreshed, created=False, old_data=old_data)
|
||||
|
||||
return result
|
||||
|
||||
@@ -376,7 +380,7 @@ class Transaction(OwnedObject):
|
||||
db_table = "transactions"
|
||||
default_manager_name = "objects"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
def clean_fields(self, *args, **kwargs):
|
||||
self.amount = truncate_decimal(
|
||||
value=self.amount, decimal_places=self.account.currency.decimal_places
|
||||
)
|
||||
@@ -386,6 +390,11 @@ class Transaction(OwnedObject):
|
||||
elif not self.reference_date and self.date:
|
||||
self.reference_date = self.date.replace(day=1)
|
||||
|
||||
super().clean_fields(*args, **kwargs)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# This is not recommended as it will run twice on some cases like form and API saves.
|
||||
# We only do this here because we forgot to independently call it on multiple places.
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@@ -443,12 +452,58 @@ class Transaction(OwnedObject):
|
||||
type_display = self.get_type_display()
|
||||
frmt_date = date(self.date, "SHORT_DATE_FORMAT")
|
||||
account = self.account
|
||||
tags = ", ".join([x.name for x in self.tags.all()]) or _("No tags")
|
||||
tags = (
|
||||
", ".join([x.name for x in self.tags.all()])
|
||||
if self.id
|
||||
else None or _("No tags")
|
||||
)
|
||||
category = self.category or _("No category")
|
||||
amount = localize_number(drop_trailing_zeros(self.amount))
|
||||
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):
|
||||
|
||||
@@ -213,6 +213,7 @@ def transactions_bulk_edit(request):
|
||||
if form.is_valid():
|
||||
# Apply changes from the form to all selected transactions
|
||||
for transaction in transactions:
|
||||
old_data = deepcopy(transaction)
|
||||
for field_name, value in form.cleaned_data.items():
|
||||
if value or isinstance(
|
||||
value, bool
|
||||
@@ -225,7 +226,7 @@ def transactions_bulk_edit(request):
|
||||
setattr(transaction, field_name, value)
|
||||
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
transaction_updated.send(sender=transaction, old_data=old_data)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
@@ -373,10 +374,13 @@ def transactions_transfer(request):
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_pay(request, transaction_id):
|
||||
transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||
old_data = deepcopy(transaction)
|
||||
|
||||
new_is_paid = False if transaction.is_paid else True
|
||||
transaction.is_paid = new_is_paid
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
|
||||
transaction_updated.send(sender=transaction, old_data=old_data)
|
||||
|
||||
response = render(
|
||||
request,
|
||||
@@ -394,11 +398,12 @@ def transaction_pay(request, transaction_id):
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_mute(request, transaction_id):
|
||||
transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||
old_data = deepcopy(transaction)
|
||||
|
||||
new_mute = False if transaction.mute else True
|
||||
transaction.mute = new_mute
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
transaction_updated.send(sender=transaction, old_data=old_data)
|
||||
|
||||
response = render(
|
||||
request,
|
||||
@@ -414,19 +419,20 @@ def transaction_mute(request, transaction_id):
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_change_month(request, transaction_id, change_type):
|
||||
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||
old_data = deepcopy(transaction)
|
||||
|
||||
if change_type == "next":
|
||||
transaction.reference_date = transaction.reference_date + relativedelta(
|
||||
months=1
|
||||
)
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
transaction_updated.send(sender=transaction, old_data=old_data)
|
||||
elif change_type == "previous":
|
||||
transaction.reference_date = transaction.reference_date - relativedelta(
|
||||
months=1
|
||||
)
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
transaction_updated.send(sender=transaction, old_data=old_data)
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
@@ -440,9 +446,11 @@ def transaction_change_month(request, transaction_id, change_type):
|
||||
def transaction_move_to_today(request, transaction_id):
|
||||
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||
|
||||
old_data = deepcopy(transaction)
|
||||
|
||||
transaction.date = timezone.localdate(timezone.now())
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
transaction_updated.send(sender=transaction, old_data=old_data)
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% load markdown %}
|
||||
{% load i18n %}
|
||||
<div class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %} tw:group/transaction tw:relative tw:hover:z-10">
|
||||
<div
|
||||
class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %} tw:group/transaction tw:relative tw:hover:z-10">
|
||||
<div class="d-flex my-1">
|
||||
{% if not disable_selection %}
|
||||
{% if not disable_selection or not dummy %}
|
||||
<label class="px-3 d-flex align-items-center justify-content-center">
|
||||
<input class="form-check-input" type="checkbox" name="transactions" value="{{ transaction.id }}"
|
||||
id="check-{{ transaction.id }}" aria-label="{% translate 'Select' %}" hx-preserve>
|
||||
@@ -19,9 +20,10 @@
|
||||
<a class="text-decoration-none p-3 tw:text-gray-500!"
|
||||
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}"
|
||||
role="button"
|
||||
{% if not dummy %}
|
||||
hx-get="{% url 'transaction_pay' transaction_id=transaction.id %}"
|
||||
hx-target="closest .transaction"
|
||||
hx-swap="outerHTML">
|
||||
hx-swap="outerHTML"{% endif %}>
|
||||
{% if transaction.is_paid %}<i class="fa-regular fa-circle-check"></i>{% else %}<i
|
||||
class="fa-regular fa-circle"></i>{% endif %}
|
||||
</a>
|
||||
@@ -33,7 +35,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg col-12 {% if transaction.account.is_untracked_by or transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
|
||||
<div
|
||||
class="col-lg col-12 {% if transaction.account.is_untracked_by or transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
|
||||
{# Date#}
|
||||
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div>
|
||||
@@ -58,14 +61,20 @@
|
||||
</div>
|
||||
<div class="tw:text-gray-400 tw:text-sm">
|
||||
{# Entities #}
|
||||
{% with transaction.entities.all as entities %}
|
||||
{% if entities %}
|
||||
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
|
||||
<div class="col ps-0">{{ entities|join:", " }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% comment %} First, check for the highest priority: a valid 'overriden_entities' list. {% endcomment %}
|
||||
{% if overriden_entities %}
|
||||
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
|
||||
<div class="col ps-0">{{ overriden_entities|join:", " }}</div>
|
||||
</div>
|
||||
|
||||
{% comment %} If no override, fall back to transaction entities, but ONLY if the transaction has an ID. {% endcomment %}
|
||||
{% elif transaction.id and transaction.entities.all %}
|
||||
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
|
||||
<div class="col ps-0">{{ transaction.entities.all|join:", " }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Notes#}
|
||||
{% if transaction.notes %}
|
||||
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||
@@ -81,17 +90,24 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Tags#}
|
||||
{% with transaction.tags.all as tags %}
|
||||
{% if tags %}
|
||||
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-hashtag fa-fw me-1 fa-xs"></i></div>
|
||||
<div class="col ps-0">{{ tags|join:", " }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% comment %} First, check for the highest priority: a valid 'overriden_tags' list. {% endcomment %}
|
||||
{% if overriden_tags %}
|
||||
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-hashtag fa-fw me-1 fa-xs"></i></div>
|
||||
<div class="col ps-0">{{ overriden_tags|join:", " }}</div>
|
||||
</div>
|
||||
|
||||
{% comment %} If no override, fall back to transaction tags, but ONLY if the transaction has an ID. {% endcomment %}
|
||||
{% elif transaction.id and transaction.tags.all %}
|
||||
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-hashtag fa-fw me-1 fa-xs"></i></div>
|
||||
<div class="col ps-0">{{ transaction.tags.all|join:", " }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-auto col-12 text-lg-end align-self-end {% if transaction.account.is_untracked_by or transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
|
||||
<div
|
||||
class="col-lg-auto col-12 text-lg-end align-self-end {% if transaction.account.is_untracked_by or transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
|
||||
<div class="main-amount mb-2 mb-lg-0">
|
||||
<c-amount.display
|
||||
:amount="transaction.amount"
|
||||
@@ -101,107 +117,136 @@
|
||||
color="{% if transaction.type == "EX" %}red{% else %}green{% endif %}"></c-amount.display>
|
||||
</div>
|
||||
{# Exchange Rate#}
|
||||
{% with exchanged=transaction.exchanged_amount %}
|
||||
{% if exchanged %}
|
||||
<div class="exchanged-amount mb-2 mb-lg-0">
|
||||
<c-amount.display
|
||||
:amount="exchanged.amount"
|
||||
:prefix="exchanged.prefix"
|
||||
:suffix="exchanged.suffix"
|
||||
:decimal_places="exchanged.decimal_places"
|
||||
color="grey"></c-amount.display>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if not dummy %}
|
||||
{% with exchanged=transaction.exchanged_amount %}
|
||||
{% if exchanged %}
|
||||
<div class="exchanged-amount mb-2 mb-lg-0">
|
||||
<c-amount.display
|
||||
:amount="exchanged.amount"
|
||||
:prefix="exchanged.prefix"
|
||||
:suffix="exchanged.suffix"
|
||||
:decimal_places="exchanged.decimal_places"
|
||||
color="grey"></c-amount.display>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if transaction.account.group %}{{ transaction.account.group.name }} • {% endif %}{{ transaction.account.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{# Item actions#}
|
||||
<div
|
||||
class="transaction-actions tw:absolute! tw:left-1/2 tw:top-0 tw:-translate-x-1/2 tw:-translate-y-1/2 tw:invisible tw:group-hover/transaction:visible d-flex flex-row card">
|
||||
<div class="card-body p-1 shadow-lg d-flex flex-row gap-1">
|
||||
{% if not transaction.deleted %}
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Edit" %}"
|
||||
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
|
||||
hx-target="#generic-offcanvas" hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Delete" %}"
|
||||
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
|
||||
hx-trigger='confirmed'
|
||||
data-bypass-on-ctrl="true"
|
||||
data-title="{% translate "Are you sure?" %}"
|
||||
data-text="{% translate "You won't be able to revert this!" %}"
|
||||
data-confirm-text="{% translate "Yes, delete it!" %}"
|
||||
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
|
||||
</a>
|
||||
<button class="btn btn-secondary btn-sm transaction-action" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-ellipsis fa-fw"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-md-start">
|
||||
{% if transaction.account.is_untracked_by %}
|
||||
<li>
|
||||
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
|
||||
<i class="fa-solid fa-eye fa-fw me-2"></i>
|
||||
<div>
|
||||
{% translate 'Show on summaries' %}
|
||||
<div class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by account' %}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% if not dummy %}
|
||||
<div>
|
||||
{# Item actions#}
|
||||
<div
|
||||
class="transaction-actions tw:absolute! tw:left-1/2 tw:top-0 tw:-translate-x-1/2 tw:-translate-y-1/2 tw:invisible tw:group-hover/transaction:visible d-flex flex-row card">
|
||||
<div class="card-body p-1 shadow-lg d-flex flex-row gap-1">
|
||||
{% if not transaction.deleted %}
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Edit" %}"
|
||||
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
|
||||
hx-target="#generic-offcanvas" hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Delete" %}"
|
||||
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
|
||||
hx-trigger='confirmed'
|
||||
data-bypass-on-ctrl="true"
|
||||
data-title="{% translate "Are you sure?" %}"
|
||||
data-text="{% translate "You won't be able to revert this!" %}"
|
||||
data-confirm-text="{% translate "Yes, delete it!" %}"
|
||||
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
|
||||
</a>
|
||||
<button class="btn btn-secondary btn-sm transaction-action" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="fa-solid fa-ellipsis fa-fw"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-md-start">
|
||||
{% if transaction.account.is_untracked_by %}
|
||||
<li>
|
||||
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
|
||||
<i class="fa-solid fa-eye fa-fw me-2"></i>
|
||||
<div>
|
||||
{% translate 'Show on summaries' %}
|
||||
<div
|
||||
class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by account' %}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% elif transaction.category.mute %}
|
||||
<li>
|
||||
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
|
||||
<i class="fa-solid fa-eye fa-fw me-2"></i>
|
||||
<div>
|
||||
{% translate 'Show on summaries' %}
|
||||
<div
|
||||
class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by category' %}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% elif transaction.mute %}
|
||||
<li><a class="dropdown-item" href="#"
|
||||
hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}"
|
||||
hx-target="closest .transaction" hx-swap="outerHTML"><i
|
||||
class="fa-solid fa-eye fa-fw me-2"></i>{% translate 'Show on summaries' %}</a></li>
|
||||
{% else %}
|
||||
<li><a class="dropdown-item" href="#"
|
||||
hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}"
|
||||
hx-target="closest .transaction" hx-swap="outerHTML"><i
|
||||
class="fa-solid fa-eye-slash fa-fw me-2"></i>{% translate 'Hide from summaries' %}</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="#"
|
||||
hx-get="{% url 'quick_transaction_add_as_quick_transaction' transaction_id=transaction.id %}"><i
|
||||
class="fa-solid fa-person-running fa-fw me-2"></i>{% translate 'Add as quick transaction' %}</a>
|
||||
</li>
|
||||
{% elif transaction.category.mute %}
|
||||
<li>
|
||||
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
|
||||
<i class="fa-solid fa-eye fa-fw me-2"></i>
|
||||
<div>
|
||||
{% translate 'Show on summaries' %}
|
||||
<div class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by category' %}</div>
|
||||
</div>
|
||||
</a>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
{% elif transaction.mute %}
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye fa-fw me-2"></i>{% translate 'Show on summaries' %}</a></li>
|
||||
{% else %}
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye-slash fa-fw me-2"></i>{% translate 'Hide from summaries' %}</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'quick_transaction_add_as_quick_transaction' transaction_id=transaction.id %}"><i class="fa-solid fa-person-running fa-fw me-2"></i>{% translate 'Add as quick transaction' %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_change_month' transaction_id=transaction.id change_type='previous' %}"><i class="fa-solid fa-calendar-minus fa-fw me-2"></i>{% translate 'Move to previous month' %}</a></li>
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_change_month' transaction_id=transaction.id change_type='next' %}"><i class="fa-solid fa-calendar-plus fa-fw me-2"></i>{% translate 'Move to next month' %}</a></li>
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_move_to_today' transaction_id=transaction.id %}"><i class="fa-solid fa-calendar-day fa-fw me-2"></i>{% translate 'Move to today' %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"><i class="fa-solid fa-clone fa-fw me-2"></i>{% translate 'Duplicate' %}</a></li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Restore" %}"
|
||||
hx-get="{% url 'transaction_undelete' transaction_id=transaction.id %}"><i
|
||||
class="fa-solid fa-trash-arrow-up"></i></a>
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Delete" %}"
|
||||
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
|
||||
hx-trigger='confirmed'
|
||||
data-bypass-on-ctrl="true"
|
||||
data-title="{% translate "Are you sure?" %}"
|
||||
data-text="{% translate "You won't be able to revert this!" %}"
|
||||
data-confirm-text="{% translate "Yes, delete it!" %}"
|
||||
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="#"
|
||||
hx-get="{% url 'transaction_change_month' transaction_id=transaction.id change_type='previous' %}"><i
|
||||
class="fa-solid fa-calendar-minus fa-fw me-2"></i>{% translate 'Move to previous month' %}</a>
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#"
|
||||
hx-get="{% url 'transaction_change_month' transaction_id=transaction.id change_type='next' %}"><i
|
||||
class="fa-solid fa-calendar-plus fa-fw me-2"></i>{% translate 'Move to next month' %}</a></li>
|
||||
<li><a class="dropdown-item" href="#"
|
||||
hx-get="{% url 'transaction_move_to_today' transaction_id=transaction.id %}"><i
|
||||
class="fa-solid fa-calendar-day fa-fw me-2"></i>{% translate 'Move to today' %}</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#"
|
||||
hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"><i
|
||||
class="fa-solid fa-clone fa-fw me-2"></i>{% translate 'Duplicate' %}</a></li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Restore" %}"
|
||||
hx-get="{% url 'transaction_undelete' transaction_id=transaction.id %}"><i
|
||||
class="fa-solid fa-trash-arrow-up"></i></a>
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Delete" %}"
|
||||
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
|
||||
hx-trigger='confirmed'
|
||||
data-bypass-on-ctrl="true"
|
||||
data-title="{% translate "Are you sure?" %}"
|
||||
data-text="{% translate "You won't be able to revert this!" %}"
|
||||
data-confirm-text="{% translate "Yes, delete it!" %}"
|
||||
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<tr>
|
||||
<th scope="col" class="col-auto"></th>
|
||||
<th scope="col" class="col-auto"></th>
|
||||
<th scope="col" class="col-auto">{% translate 'Order' %}</th>
|
||||
<th scope="col" class="col">{% translate 'Name' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -80,6 +81,9 @@
|
||||
<i class="fa-solid fa-toggle-off tw:text-red-400"></i>{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class="col text-center">
|
||||
<div>{{ rule.order }}</div>
|
||||
</td>
|
||||
<td class="col">
|
||||
<div>{{ rule.name }}</div>
|
||||
<div class="tw:text-gray-400">{{ rule.description }}</div>
|
||||
|
||||
@@ -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_created' pk=rule.id %}" hx-target="#generic-offcanvas"
|
||||
hx-indicator="#dry-run-created-result, closest form" class="show-loading" novalidate>
|
||||
{% crispy form %}
|
||||
</form>
|
||||
<hr>
|
||||
<div id="dry-run-created-result" class="show-loading">
|
||||
{% include 'rules/fragments/transaction_rule/dry_run/visual.html' with logs=logs results=results %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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_deleted' pk=rule.id %}" hx-target="#generic-offcanvas"
|
||||
hx-indicator="#dry-run-deleted-result, closest form" class="show-loading" novalidate>
|
||||
{% crispy form %}
|
||||
</form>
|
||||
<hr>
|
||||
<div id="dry-run-deleted-result" class="show-loading">
|
||||
{% include 'rules/fragments/transaction_rule/dry_run/visual.html' with logs=logs results=results %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,101 @@
|
||||
{% load i18n %}
|
||||
<div class="card tw:max-h-full tw:overflow-auto tw:overflow-x-auto">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" id="visual-tab" data-bs-toggle="tab" data-bs-target="#visual-tab-pane"
|
||||
type="button" role="tab" aria-controls="visual-tab-pane"
|
||||
aria-selected="true">{% translate 'Visual' %}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="logs-tab" data-bs-toggle="tab" data-bs-target="#logs-tab-pane" type="button"
|
||||
role="tab" aria-controls="logs-tab-pane" aria-selected="false">{% translate 'Logs' %}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="visual-tab-pane" role="tabpanel" aria-labelledby="home-tab"
|
||||
tabindex="0">
|
||||
{% if not results %}
|
||||
{% translate 'Run a test to see...' %}
|
||||
{% else %}
|
||||
{% for result in results %}
|
||||
|
||||
{% if result.type == 'header' %}
|
||||
<div class="my-3">
|
||||
<h6 class="text-center mb-3">
|
||||
<span class="badge text-bg-secondary">
|
||||
{% if result.header_type == "edit_transaction" %}
|
||||
{% translate 'Edit transaction' %}
|
||||
{% elif result.header_type == "update_or_create_transaction" %}
|
||||
{% translate 'Update or create transaction' %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if result.type == 'triggering_transaction' %}
|
||||
<div class="mt-4">
|
||||
<h6 class="text-center mb-3"><span class="badge text-bg-secondary">{% translate 'Start' %}</span></h6>
|
||||
<c-transaction.item :transaction="result.transaction" :dummy="True"
|
||||
:disable-selection="True"></c-transaction.item>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result.type == 'edit_transaction' %}
|
||||
<div>
|
||||
<div>
|
||||
{% translate 'Set' %} <span
|
||||
class="badge text-bg-secondary">{{ result.field }}</span> {% translate 'to' %}
|
||||
<span class="badge text-bg-secondary">{{ result.new_value }}</span>
|
||||
</div>
|
||||
<c-transaction.item :transaction="result.transaction" :dummy="True"
|
||||
:disable-selection="True"></c-transaction.item>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result.type == 'update_or_create_transaction' %}
|
||||
<div>
|
||||
<div class="alert alert-info" role="alert">
|
||||
{% translate 'Search' %}: {{ result.query }}
|
||||
</div>
|
||||
{% if result.start_transaction %}
|
||||
<c-transaction.item :transaction="result.start_transaction" :dummy="True"
|
||||
:disable-selection="True"></c-transaction.item>
|
||||
{% else %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% translate 'No transaction found, a new one will be created' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<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"
|
||||
:disable-selection="True"></c-transaction.item>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result.type == 'error' %}
|
||||
<div>
|
||||
<div class="alert alert-{% if result.level == 'error' %}danger{% elif result.level == 'warning' %}warning{% else %}info{% endif %}" role="alert">
|
||||
{{ result.error }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="logs-tab-pane" role="tabpanel" aria-labelledby="logs-tab" tabindex="0">
|
||||
{% if not logs %}
|
||||
{% translate 'Run a test to see...' %}
|
||||
{% else %}
|
||||
<pre>
|
||||
{{ logs|linebreaks }}
|
||||
</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@
|
||||
data-text="{% translate "You won't be able to revert this!" %}"
|
||||
data-confirm-text="{% translate 'Yes, delete it!' %}"
|
||||
_="install prompt_swal">
|
||||
<i class="fa-solid fa-trash fa-fw"></i>
|
||||
<i class="fa-solid fa-trash fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,33 +101,57 @@
|
||||
data-text="{% translate "You won't be able to revert this!" %}"
|
||||
data-confirm-text="{% translate 'Yes, delete it!' %}"
|
||||
_="install prompt_swal">
|
||||
<i class="fa-solid fa-trash fa-fw"></i>
|
||||
<i class="fa-solid fa-trash fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not all_actions %}
|
||||
{% empty %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% translate 'This rule has no actions' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-primary text-decoration-none w-100" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="fa-solid fa-circle-plus me-2"></i>{% translate 'Add new' %}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end w-100">
|
||||
<li><a class="dropdown-item" role="link"
|
||||
hx-get="{% url 'transaction_rule_action_add' transaction_rule_id=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Edit Transaction' %}</a></li>
|
||||
<li><a class="dropdown-item" role="link"
|
||||
hx-get="{% url 'update_or_create_transaction_rule_action_add' transaction_rule_id=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Update or Create Transaction' %}</a></li>
|
||||
</ul>
|
||||
<div class="d-grid d-lg-flex gap-2">
|
||||
<div class="dropdown flex-fill">
|
||||
<button class="btn btn-outline-primary text-decoration-none w-100" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="fa-solid fa-flask-vial me-2"></i>{% translate 'Test' %}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if transaction_rule.on_create %}
|
||||
<li><a class="dropdown-item" role="link" href="#"
|
||||
hx-get="{% url 'transaction_rule_dry_run_created' pk=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Create' %}</a></li>
|
||||
{% endif %}
|
||||
{% if transaction_rule.on_update %}
|
||||
<li><a class="dropdown-item" role="link" href="#"
|
||||
hx-get="{% url 'transaction_rule_dry_run_updated' pk=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Update' %}</a></li>
|
||||
{% endif %}
|
||||
{% if transaction_rule.on_delete %}
|
||||
<li><a class="dropdown-item" role="link" href="#"
|
||||
hx-get="{% url 'transaction_rule_dry_run_deleted' pk=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Delete' %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown flex-fill">
|
||||
<button class="btn btn-outline-primary text-decoration-none w-100" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="fa-solid fa-circle-plus me-2"></i>{% translate 'Add new' %}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" role="link" href="#"
|
||||
hx-get="{% url 'transaction_rule_action_add' transaction_rule_id=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Edit Transaction' %}</a></li>
|
||||
<li><a class="dropdown-item" role="link" href="#"
|
||||
hx-get="{% url 'update_or_create_transaction_rule_action_add' transaction_rule_id=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Update or Create Transaction' %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user