mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-03-23 18:01:16 +01:00
feat(rules): add optional rules ordering
This commit is contained in:
@@ -65,10 +65,11 @@ class TransactionRuleForm(forms.ModelForm):
|
||||
class TransactionRuleActionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = TransactionRuleAction
|
||||
fields = ("value", "field")
|
||||
fields = ("value", "field", "order")
|
||||
labels = {
|
||||
"field": _("Set field"),
|
||||
"value": _("To"),
|
||||
"order": _("Order"),
|
||||
}
|
||||
widgets = {"field": TomSelect(clear_button=False)}
|
||||
|
||||
@@ -82,6 +83,7 @@ class TransactionRuleActionForm(forms.ModelForm):
|
||||
self.helper.form_method = "post"
|
||||
# TO-DO: Add helper with available commands
|
||||
self.helper.layout = Layout(
|
||||
"order",
|
||||
"field",
|
||||
"value",
|
||||
)
|
||||
@@ -150,6 +152,7 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
labels = {
|
||||
"order": _("Order"),
|
||||
"search_account_operator": _("Operator"),
|
||||
"search_type_operator": _("Operator"),
|
||||
"search_is_paid_operator": _("Operator"),
|
||||
@@ -200,6 +203,7 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
self.helper.form_method = "post"
|
||||
|
||||
self.helper.layout = Layout(
|
||||
"order",
|
||||
BS5Accordion(
|
||||
AccordionGroup(
|
||||
_("Search Criteria"),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Generated by Django 5.2 on 2025-08-30 18:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rules", "0014_alter_transactionrule_owner_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="transactionruleaction",
|
||||
options={
|
||||
"ordering": ["order"],
|
||||
"verbose_name": "Edit transaction action",
|
||||
"verbose_name_plural": "Edit transaction actions",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="updateorcreatetransactionruleaction",
|
||||
options={
|
||||
"ordering": ["order"],
|
||||
"verbose_name": "Update or create transaction action",
|
||||
"verbose_name_plural": "Update or create transaction actions",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transactionruleaction",
|
||||
name="order",
|
||||
field=models.PositiveIntegerField(default=0, verbose_name="Order"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="updateorcreatetransactionruleaction",
|
||||
name="order",
|
||||
field=models.PositiveIntegerField(default=0, verbose_name="Order"),
|
||||
),
|
||||
]
|
||||
@@ -51,6 +51,7 @@ class TransactionRuleAction(models.Model):
|
||||
verbose_name=_("Field"),
|
||||
)
|
||||
value = models.TextField(verbose_name=_("Value"))
|
||||
order = models.PositiveIntegerField(default=0, verbose_name=_("Order"))
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.rule} - {self.field} - {self.value}"
|
||||
@@ -59,6 +60,11 @@ class TransactionRuleAction(models.Model):
|
||||
verbose_name = _("Edit transaction action")
|
||||
verbose_name_plural = _("Edit transaction actions")
|
||||
unique_together = (("rule", "field"),)
|
||||
ordering = ["order"]
|
||||
|
||||
@property
|
||||
def action_type(self):
|
||||
return "edit_transaction"
|
||||
|
||||
|
||||
class UpdateOrCreateTransactionRuleAction(models.Model):
|
||||
@@ -290,10 +296,16 @@ class UpdateOrCreateTransactionRuleAction(models.Model):
|
||||
verbose_name=_("Tags"),
|
||||
blank=True,
|
||||
)
|
||||
order = models.PositiveIntegerField(default=0, verbose_name=_("Order"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Update or create transaction action")
|
||||
verbose_name_plural = _("Update or create transaction actions")
|
||||
ordering = ["order"]
|
||||
|
||||
@property
|
||||
def action_type(self):
|
||||
return "update_or_create_transaction"
|
||||
|
||||
def __str__(self):
|
||||
return f"Update or create transaction action for {self.rule}"
|
||||
|
||||
@@ -2,6 +2,7 @@ import decimal
|
||||
import logging
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from itertools import chain
|
||||
|
||||
from cachalot.api import cachalot_disabled
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@@ -87,7 +88,9 @@ def check_for_transaction_rules(
|
||||
# For deleted transactions, we might want to limit what actions can be performed
|
||||
if signal == "transaction_deleted":
|
||||
# Process only create/update actions, not edit actions
|
||||
for action in rule.update_or_create_transaction_actions.all():
|
||||
for (
|
||||
action
|
||||
) in rule.update_or_create_transaction_actions.all():
|
||||
try:
|
||||
_process_update_or_create_transaction_action(
|
||||
action=action, simple_eval=simple
|
||||
@@ -99,31 +102,74 @@ def check_for_transaction_rules(
|
||||
)
|
||||
else:
|
||||
# Normal processing for non-deleted transactions
|
||||
for action in rule.transaction_actions.all():
|
||||
try:
|
||||
instance = _process_edit_transaction_action(
|
||||
instance=instance, action=action, simple_eval=simple
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing edit transaction action {action.id}",
|
||||
exc_info=True,
|
||||
)
|
||||
edit_actions = list(rule.transaction_actions.all())
|
||||
update_or_create_actions = list(
|
||||
rule.update_or_create_transaction_actions.all()
|
||||
)
|
||||
|
||||
simple.names.update(_get_names(instance))
|
||||
if signal != "transaction_deleted":
|
||||
instance.save()
|
||||
# Check if any action has a non-zero order
|
||||
has_custom_order = any(
|
||||
a.order > 0 for a in edit_actions
|
||||
) or any(a.order > 0 for a in update_or_create_actions)
|
||||
|
||||
for action in rule.update_or_create_transaction_actions.all():
|
||||
try:
|
||||
_process_update_or_create_transaction_action(
|
||||
action=action, simple_eval=simple
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing update or create transaction action {action.id}",
|
||||
exc_info=True,
|
||||
)
|
||||
if has_custom_order:
|
||||
# Combine and sort actions by order
|
||||
all_actions = sorted(
|
||||
chain(edit_actions, update_or_create_actions),
|
||||
key=lambda a: a.order,
|
||||
)
|
||||
|
||||
for action in all_actions:
|
||||
try:
|
||||
if isinstance(action, TransactionRuleAction):
|
||||
instance = _process_edit_transaction_action(
|
||||
instance=instance,
|
||||
action=action,
|
||||
simple_eval=simple,
|
||||
)
|
||||
# Update names for next actions
|
||||
simple.names.update(_get_names(instance))
|
||||
else:
|
||||
_process_update_or_create_transaction_action(
|
||||
action=action, simple_eval=simple
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing action {action.id}",
|
||||
exc_info=True,
|
||||
)
|
||||
# Save at the end
|
||||
if signal != "transaction_deleted":
|
||||
instance.save()
|
||||
else:
|
||||
# Original behavior
|
||||
for action in edit_actions:
|
||||
try:
|
||||
instance = _process_edit_transaction_action(
|
||||
instance=instance,
|
||||
action=action,
|
||||
simple_eval=simple,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing edit transaction action {action.id}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
simple.names.update(_get_names(instance))
|
||||
if signal != "transaction_deleted":
|
||||
instance.save()
|
||||
|
||||
for action in update_or_create_actions:
|
||||
try:
|
||||
_process_update_or_create_transaction_action(
|
||||
action=action, simple_eval=simple
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing update or create transaction action {action.id}",
|
||||
exc_info=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error while executing 'check_for_transaction_rules' task",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from itertools import chain
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
@@ -140,10 +142,18 @@ def transaction_rule_edit(request, transaction_rule_id):
|
||||
def transaction_rule_view(request, transaction_rule_id):
|
||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
||||
|
||||
edit_actions = transaction_rule.transaction_actions.all()
|
||||
update_or_create_actions = transaction_rule.update_or_create_transaction_actions.all()
|
||||
|
||||
all_actions = sorted(
|
||||
chain(edit_actions, update_or_create_actions),
|
||||
key=lambda a: a.order,
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"rules/fragments/transaction_rule/view.html",
|
||||
{"transaction_rule": transaction_rule},
|
||||
{"transaction_rule": transaction_rule, "all_actions": all_actions},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -175,6 +175,6 @@ class RecurringTransactionTests(TestCase):
|
||||
recurrence_type=RecurringTransaction.RecurrenceType.MONTH,
|
||||
recurrence_interval=1,
|
||||
)
|
||||
self.assertFalse(recurring.paused)
|
||||
self.assertFalse(recurring.is_paused)
|
||||
self.assertEqual(recurring.recurrence_interval, 1)
|
||||
self.assertEqual(recurring.account.currency.code, "USD")
|
||||
|
||||
@@ -30,75 +30,84 @@
|
||||
|
||||
<div class="my-3">
|
||||
<div class="tw:text-xl mb-2">{% translate 'Then...' %}</div>
|
||||
{% for action in transaction_rule.transaction_actions.all %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div><span class="badge text-bg-primary">{% trans 'Edit transaction' %}</span></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div>{% translate 'Set' %} <span
|
||||
class="badge text-bg-secondary">{{ action.get_field_display }}</span> {% translate 'to' %}</div>
|
||||
<div class="text-bg-secondary rounded-3 mt-3 p-2">{{ action.value }}</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<a class="text-decoration-none tw:text-gray-400 p-1"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Edit" %}"
|
||||
hx-get="{% url 'transaction_rule_action_edit' transaction_rule_action_id=action.id %}"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-pencil fa-fw"></i>
|
||||
</a>
|
||||
<a class="text-danger text-decoration-none p-1"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Delete" %}"
|
||||
hx-delete="{% url 'transaction_rule_action_delete' transaction_rule_action_id=action.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">
|
||||
{% for action in all_actions %}
|
||||
{% if action.action_type == "edit_transaction" %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
{% if action.order != 0 %}<span class="badge text-bg-secondary">{{ action.order }}</span>{% endif %}
|
||||
<span class="badge text-bg-primary">{% trans 'Edit transaction' %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div>
|
||||
{% translate 'Set' %} <span
|
||||
class="badge text-bg-secondary">{{ action.get_field_display }}</span> {% translate 'to' %}
|
||||
</div>
|
||||
<div class="text-bg-secondary rounded-3 mt-3 p-2">{{ action.value }}</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<a class="text-decoration-none tw:text-gray-400 p-1"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate 'Edit' %}"
|
||||
hx-get="{% url 'transaction_rule_action_edit' transaction_rule_action_id=action.id %}"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-pencil fa-fw"></i>
|
||||
</a>
|
||||
<a class="text-danger text-decoration-none p-1"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate 'Delete' %}"
|
||||
hx-delete="{% url 'transaction_rule_action_delete' transaction_rule_action_id=action.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"></i>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for action in transaction_rule.update_or_create_transaction_actions.all %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div><span class="badge text-bg-primary">{% trans 'Update or create transaction' %}</span></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div>{% trans 'Edit to view' %}</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<a class="text-decoration-none tw:text-gray-400 p-1"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Edit" %}"
|
||||
hx-get="{% url 'update_or_create_transaction_rule_action_edit' pk=action.id %}"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-pencil fa-fw"></i>
|
||||
</a>
|
||||
<a class="text-danger text-decoration-none p-1"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Delete" %}"
|
||||
hx-delete="{% url 'update_or_create_transaction_rule_action_delete' pk=action.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">
|
||||
{% elif action.action_type == "update_or_create_transaction" %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
{% if action.order != 0 %}<span class="badge text-bg-secondary">{{ action.order }}</span>{% endif %}
|
||||
<span class="badge text-bg-primary">{% trans 'Update or create transaction' %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div>{% trans 'Edit to view' %}</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<a class="text-decoration-none tw:text-gray-400 p-1"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate 'Edit' %}"
|
||||
hx-get="{% url 'update_or_create_transaction_rule_action_edit' pk=action.id %}"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-pencil fa-fw"></i>
|
||||
</a>
|
||||
<a class="text-danger text-decoration-none p-1"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate 'Delete' %}"
|
||||
hx-delete="{% url 'update_or_create_transaction_rule_action_delete' pk=action.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"></i>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not transaction_rule.update_or_create_transaction_actions.all and not transaction_rule.transaction_actions.all %}
|
||||
{% if not all_actions %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% translate 'This rule has no actions' %}
|
||||
|
||||
Reference in New Issue
Block a user