mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-03-23 09:51:21 +01:00
Merge pull request #142
feat(rules): add Update or Create Transaction action
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from apps.rules.models import TransactionRule, TransactionRuleAction
|
||||
from apps.rules.models import (
|
||||
TransactionRule,
|
||||
TransactionRuleAction,
|
||||
UpdateOrCreateTransactionRuleAction,
|
||||
)
|
||||
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(TransactionRule)
|
||||
admin.site.register(TransactionRuleAction)
|
||||
admin.site.register(UpdateOrCreateTransactionRuleAction)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from crispy_bootstrap5.bootstrap5 import Switch
|
||||
from crispy_forms.bootstrap import FormActions
|
||||
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 django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.rules.models import TransactionRule
|
||||
from apps.rules.models import TransactionRuleAction
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
from apps.common.widgets.tom_select import TomSelect
|
||||
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
|
||||
from apps.rules.models import TransactionRuleAction
|
||||
|
||||
|
||||
class TransactionRuleForm(forms.ModelForm):
|
||||
@@ -123,3 +123,255 @@ class TransactionRuleActionForm(forms.ModelForm):
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UpdateOrCreateTransactionRuleAction
|
||||
exclude = ("rule",)
|
||||
widgets = {
|
||||
"search_account_operator": TomSelect(clear_button=False),
|
||||
"search_type_operator": TomSelect(clear_button=False),
|
||||
"search_is_paid_operator": TomSelect(clear_button=False),
|
||||
"search_date_operator": TomSelect(clear_button=False),
|
||||
"search_reference_date_operator": TomSelect(clear_button=False),
|
||||
"search_amount_operator": TomSelect(clear_button=False),
|
||||
"search_description_operator": TomSelect(clear_button=False),
|
||||
"search_notes_operator": TomSelect(clear_button=False),
|
||||
"search_category_operator": TomSelect(clear_button=False),
|
||||
"search_internal_note_operator": TomSelect(clear_button=False),
|
||||
"search_internal_id_operator": TomSelect(clear_button=False),
|
||||
}
|
||||
|
||||
labels = {
|
||||
"search_account_operator": _("Operator"),
|
||||
"search_type_operator": _("Operator"),
|
||||
"search_is_paid_operator": _("Operator"),
|
||||
"search_date_operator": _("Operator"),
|
||||
"search_reference_date_operator": _("Operator"),
|
||||
"search_amount_operator": _("Operator"),
|
||||
"search_description_operator": _("Operator"),
|
||||
"search_notes_operator": _("Operator"),
|
||||
"search_category_operator": _("Operator"),
|
||||
"search_internal_note_operator": _("Operator"),
|
||||
"search_internal_id_operator": _("Operator"),
|
||||
"search_tags_operator": _("Operator"),
|
||||
"search_entities_operator": _("Operator"),
|
||||
"search_account": _("Account"),
|
||||
"search_type": _("Type"),
|
||||
"search_is_paid": _("Paid"),
|
||||
"search_date": _("Date"),
|
||||
"search_reference_date": _("Reference Date"),
|
||||
"search_amount": _("Amount"),
|
||||
"search_description": _("Description"),
|
||||
"search_notes": _("Notes"),
|
||||
"search_category": _("Category"),
|
||||
"search_internal_note": _("Internal Note"),
|
||||
"search_internal_id": _("Internal ID"),
|
||||
"search_tags": _("Tags"),
|
||||
"search_entities": _("Entities"),
|
||||
"set_account": _("Account"),
|
||||
"set_type": _("Type"),
|
||||
"set_is_paid": _("Paid"),
|
||||
"set_date": _("Date"),
|
||||
"set_reference_date": _("Reference Date"),
|
||||
"set_amount": _("Amount"),
|
||||
"set_description": _("Description"),
|
||||
"set_tags": _("Tags"),
|
||||
"set_entities": _("Entities"),
|
||||
"set_notes": _("Notes"),
|
||||
"set_category": _("Category"),
|
||||
"set_internal_note": _("Internal Note"),
|
||||
"set_internal_id": _("Internal ID"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.rule = kwargs.pop("rule", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
self.helper.form_method = "post"
|
||||
|
||||
self.helper.layout = Layout(
|
||||
BS5Accordion(
|
||||
AccordionGroup(
|
||||
_("Search Criteria"),
|
||||
Field("filter", rows=1),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_type_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_type", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_is_paid_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_is_paid", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_account_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_account", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_entities_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_entities", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_date_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_date", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_reference_date_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_reference_date", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_description_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_description", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_amount_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_amount", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_category_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_category", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_tags_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_tags", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_notes_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_notes", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_internal_note_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_internal_note", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
Field("search_internal_id_operator"),
|
||||
css_class="form-group col-md-4",
|
||||
),
|
||||
Column(
|
||||
Field("search_internal_id", rows=1),
|
||||
css_class="form-group col-md-8",
|
||||
),
|
||||
),
|
||||
active=True,
|
||||
),
|
||||
AccordionGroup(
|
||||
_("Set Values"),
|
||||
Field("set_type", rows=1),
|
||||
Field("set_is_paid", rows=1),
|
||||
Field("set_account", rows=1),
|
||||
Field("set_entities", rows=1),
|
||||
Field("set_date", rows=1),
|
||||
Field("set_reference_date", rows=1),
|
||||
Field("set_description", rows=1),
|
||||
Field("set_amount", rows=1),
|
||||
Field("set_category", rows=1),
|
||||
Field("set_tags", rows=1),
|
||||
Field("set_notes", rows=1),
|
||||
Field("set_internal_note", rows=1),
|
||||
Field("set_internal_id", rows=1),
|
||||
css_class="mb-3",
|
||||
active=True,
|
||||
),
|
||||
always_open=True,
|
||||
),
|
||||
)
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
self.helper.layout.append(
|
||||
FormActions(
|
||||
NoClassSubmit(
|
||||
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
|
||||
),
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.helper.layout.append(
|
||||
FormActions(
|
||||
NoClassSubmit(
|
||||
"submit", _("Add"), css_class="btn btn-outline-primary w-100"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
instance.rule = self.rule
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-08 03:16
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rules', '0005_alter_transactionruleaction_rule'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UpdateOrCreateTransactionRuleAction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('search_account', models.TextField(blank=True, help_text='Expression to match transaction account (ID or name)', verbose_name='Search Account')),
|
||||
('search_account_operator', 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='Account Operator')),
|
||||
('search_type', models.TextField(blank=True, help_text="Expression to match transaction type ('IN' or 'EX')", verbose_name='Search Type')),
|
||||
('search_type_operator', 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='Type Operator')),
|
||||
('search_is_paid', models.TextField(blank=True, help_text='Expression to match transaction paid status', verbose_name='Search Is Paid')),
|
||||
('search_is_paid_operator', 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='Is Paid Operator')),
|
||||
('search_date', models.TextField(blank=True, help_text='Expression to match transaction date', verbose_name='Search Date')),
|
||||
('search_date_operator', 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='Date Operator')),
|
||||
('search_reference_date', models.TextField(blank=True, help_text='Expression to match transaction reference date', verbose_name='Search Reference Date')),
|
||||
('search_reference_date_operator', 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='Reference Date Operator')),
|
||||
('search_amount', models.TextField(blank=True, help_text='Expression to match transaction amount', verbose_name='Search Amount')),
|
||||
('search_amount_operator', 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='Amount Operator')),
|
||||
('search_description', models.TextField(blank=True, help_text='Expression to match transaction description', verbose_name='Search Description')),
|
||||
('search_description_operator', 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='contains', max_length=10, verbose_name='Description Operator')),
|
||||
('search_notes', models.TextField(blank=True, help_text='Expression to match transaction notes', verbose_name='Search Notes')),
|
||||
('search_notes_operator', 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='contains', max_length=10, verbose_name='Notes Operator')),
|
||||
('search_category', models.TextField(blank=True, help_text='Expression to match transaction category (ID or name)', verbose_name='Search Category')),
|
||||
('search_category_operator', 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='Category Operator')),
|
||||
('search_internal_note', models.TextField(blank=True, help_text='Expression to match transaction internal note', verbose_name='Search Internal Note')),
|
||||
('search_internal_note_operator', 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='Internal Note Operator')),
|
||||
('search_internal_id', models.TextField(blank=True, help_text='Expression to match transaction internal ID', verbose_name='Search Internal ID')),
|
||||
('search_internal_id_operator', 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='Internal ID Operator')),
|
||||
('set_account', models.TextField(blank=True, help_text='Expression for account to set (ID or name)', verbose_name='Set Account')),
|
||||
('set_type', models.TextField(blank=True, help_text="Expression for type to set ('IN' or 'EX')", verbose_name='Set Type')),
|
||||
('set_is_paid', models.TextField(blank=True, help_text='Expression for paid status to set', verbose_name='Set Is Paid')),
|
||||
('set_date', models.TextField(blank=True, help_text='Expression for date to set', verbose_name='Set Date')),
|
||||
('set_reference_date', models.TextField(blank=True, help_text='Expression for reference date to set', verbose_name='Set Reference Date')),
|
||||
('set_amount', models.TextField(blank=True, help_text='Expression for amount to set', verbose_name='Set Amount')),
|
||||
('set_description', models.TextField(blank=True, help_text='Expression for description to set', verbose_name='Set Description')),
|
||||
('set_notes', models.TextField(blank=True, help_text='Expression for notes to set', verbose_name='Set Notes')),
|
||||
('set_internal_note', models.TextField(blank=True, help_text='Expression for internal note to set', verbose_name='Set Internal Note')),
|
||||
('set_internal_id', models.TextField(blank=True, help_text='Expression for internal ID to set', verbose_name='Set Internal ID')),
|
||||
('set_category', models.TextField(blank=True, help_text='Expression for category to set (ID or name)', verbose_name='Set Category')),
|
||||
('set_tags', models.TextField(blank=True, help_text='Expression for tags to set (list of IDs or names)', verbose_name='Set Tags')),
|
||||
('set_entities', models.TextField(blank=True, help_text='Expression for entities to set (list of IDs or names)', verbose_name='Set Entities')),
|
||||
('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='update_or_create_transaction_actions', to='rules.transactionrule', verbose_name='Rule')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'pdate or Create Transaction Action',
|
||||
'verbose_name_plural': 'pdate or Create Transaction Action Actions',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-08 04:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rules', '0006_updateorcreatetransactionruleaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='updateorcreatetransactionruleaction',
|
||||
options={'verbose_name': 'Update or Create Transaction Action', 'verbose_name_plural': 'Update or Create Transaction Action Actions'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='updateorcreatetransactionruleaction',
|
||||
name='filter',
|
||||
field=models.TextField(blank=True, help_text='Generic expression to enable or disable execution. Should evaluate to True or False', verbose_name='Filter'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-08 06:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rules', '0007_alter_updateorcreatetransactionruleaction_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='updateorcreatetransactionruleaction',
|
||||
name='search_entities',
|
||||
field=models.TextField(blank=True, help_text='Expression to match transaction entities (list of IDs or names)', verbose_name='Search Entities'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='updateorcreatetransactionruleaction',
|
||||
name='search_entities_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='contains', max_length=10, verbose_name='Entities Operator'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='updateorcreatetransactionruleaction',
|
||||
name='search_tags',
|
||||
field=models.TextField(blank=True, help_text='Expression to match transaction tags (list of IDs or names)', verbose_name='Search Tags'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='updateorcreatetransactionruleaction',
|
||||
name='search_tags_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='contains', max_length=10, verbose_name='Tags Operator'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-08 06:40
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rules', '0008_updateorcreatetransactionruleaction_search_entities_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='transactionrule',
|
||||
options={'verbose_name': 'Transaction rule', 'verbose_name_plural': 'Transaction rules'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='transactionruleaction',
|
||||
options={'verbose_name': 'Edit transaction action', 'verbose_name_plural': 'Edit transaction actions'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='updateorcreatetransactionruleaction',
|
||||
options={'verbose_name': 'Update or create transaction action', 'verbose_name_plural': 'Update or create transaction actions'},
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@@ -10,6 +11,10 @@ class TransactionRule(models.Model):
|
||||
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
|
||||
trigger = models.TextField(verbose_name=_("Trigger"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Transaction rule")
|
||||
verbose_name_plural = _("Transaction rules")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -45,4 +50,375 @@ class TransactionRuleAction(models.Model):
|
||||
return f"{self.rule} - {self.field} - {self.value}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Edit transaction action")
|
||||
verbose_name_plural = _("Edit transaction actions")
|
||||
unique_together = (("rule", "field"),)
|
||||
|
||||
|
||||
class UpdateOrCreateTransactionRuleAction(models.Model):
|
||||
"""
|
||||
Will attempt to find and update latest matching transaction, or create new if none found.
|
||||
"""
|
||||
|
||||
class SearchOperator(models.TextChoices):
|
||||
EXACT = "exact", _("is exactly")
|
||||
CONTAINS = "contains", _("contains")
|
||||
STARTSWITH = "startswith", _("starts with")
|
||||
ENDSWITH = "endswith", _("ends with")
|
||||
EQ = "eq", _("equals")
|
||||
GT = "gt", _("greater than")
|
||||
LT = "lt", _("less than")
|
||||
GTE = "gte", _("greater than or equal")
|
||||
LTE = "lte", _("less than or equal")
|
||||
|
||||
rule = models.ForeignKey(
|
||||
TransactionRule,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="update_or_create_transaction_actions",
|
||||
verbose_name=_("Rule"),
|
||||
)
|
||||
|
||||
filter = models.TextField(
|
||||
verbose_name=_("Filter"),
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"Generic expression to enable or disable execution. Should evaluate to True or False"
|
||||
),
|
||||
)
|
||||
|
||||
# Search fields with operators
|
||||
search_account = models.TextField(
|
||||
verbose_name=_("Search Account"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction account (ID or name)"),
|
||||
)
|
||||
search_account_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name=_("Account Operator"),
|
||||
)
|
||||
|
||||
search_type = models.TextField(
|
||||
verbose_name=_("Search Type"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction type ('IN' or 'EX')"),
|
||||
)
|
||||
search_type_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name=_("Type Operator"),
|
||||
)
|
||||
|
||||
search_is_paid = models.TextField(
|
||||
verbose_name=_("Search Is Paid"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction paid status"),
|
||||
)
|
||||
search_is_paid_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name=_("Is Paid Operator"),
|
||||
)
|
||||
|
||||
search_date = models.TextField(
|
||||
verbose_name=_("Search Date"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction date"),
|
||||
)
|
||||
search_date_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name=_("Date Operator"),
|
||||
)
|
||||
|
||||
search_reference_date = models.TextField(
|
||||
verbose_name=_("Search Reference Date"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction reference date"),
|
||||
)
|
||||
search_reference_date_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name=_("Reference Date Operator"),
|
||||
)
|
||||
|
||||
search_amount = models.TextField(
|
||||
verbose_name=_("Search Amount"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction amount"),
|
||||
)
|
||||
search_amount_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name=_("Amount Operator"),
|
||||
)
|
||||
|
||||
search_description = models.TextField(
|
||||
verbose_name=_("Search Description"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction description"),
|
||||
)
|
||||
search_description_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.CONTAINS,
|
||||
verbose_name=_("Description Operator"),
|
||||
)
|
||||
|
||||
search_notes = models.TextField(
|
||||
verbose_name=_("Search Notes"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction notes"),
|
||||
)
|
||||
search_notes_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.CONTAINS,
|
||||
verbose_name=_("Notes Operator"),
|
||||
)
|
||||
|
||||
search_category = models.TextField(
|
||||
verbose_name=_("Search Category"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction category (ID or name)"),
|
||||
)
|
||||
search_category_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name=_("Category Operator"),
|
||||
)
|
||||
|
||||
search_tags = models.TextField(
|
||||
verbose_name=_("Search Tags"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction tags (list of IDs or names)"),
|
||||
)
|
||||
search_tags_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.CONTAINS,
|
||||
verbose_name=_("Tags Operator"),
|
||||
)
|
||||
|
||||
search_entities = models.TextField(
|
||||
verbose_name=_("Search Entities"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction entities (list of IDs or names)"),
|
||||
)
|
||||
search_entities_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.CONTAINS,
|
||||
verbose_name=_("Entities Operator"),
|
||||
)
|
||||
|
||||
search_internal_note = models.TextField(
|
||||
verbose_name=_("Search Internal Note"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction internal note"),
|
||||
)
|
||||
search_internal_note_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name=_("Internal Note Operator"),
|
||||
)
|
||||
|
||||
search_internal_id = models.TextField(
|
||||
verbose_name=_("Search Internal ID"),
|
||||
blank=True,
|
||||
help_text=_("Expression to match transaction internal ID"),
|
||||
)
|
||||
search_internal_id_operator = models.CharField(
|
||||
max_length=10,
|
||||
choices=SearchOperator.choices,
|
||||
default=SearchOperator.EXACT,
|
||||
verbose_name=_("Internal ID Operator"),
|
||||
)
|
||||
|
||||
# Set fields
|
||||
set_account = models.TextField(
|
||||
verbose_name=_("Set Account"),
|
||||
blank=True,
|
||||
help_text=_("Expression for account to set (ID or name)"),
|
||||
)
|
||||
set_type = models.TextField(
|
||||
verbose_name=_("Set Type"),
|
||||
blank=True,
|
||||
help_text=_("Expression for type to set ('IN' or 'EX')"),
|
||||
)
|
||||
set_is_paid = models.TextField(
|
||||
verbose_name=_("Set Is Paid"),
|
||||
blank=True,
|
||||
help_text=_("Expression for paid status to set"),
|
||||
)
|
||||
set_date = models.TextField(
|
||||
verbose_name=_("Set Date"),
|
||||
blank=True,
|
||||
help_text=_("Expression for date to set"),
|
||||
)
|
||||
set_reference_date = models.TextField(
|
||||
verbose_name=_("Set Reference Date"),
|
||||
blank=True,
|
||||
help_text=_("Expression for reference date to set"),
|
||||
)
|
||||
set_amount = models.TextField(
|
||||
verbose_name=_("Set Amount"),
|
||||
blank=True,
|
||||
help_text=_("Expression for amount to set"),
|
||||
)
|
||||
set_description = models.TextField(
|
||||
verbose_name=_("Set Description"),
|
||||
blank=True,
|
||||
help_text=_("Expression for description to set"),
|
||||
)
|
||||
set_notes = models.TextField(
|
||||
verbose_name=_("Set Notes"),
|
||||
blank=True,
|
||||
help_text=_("Expression for notes to set"),
|
||||
)
|
||||
set_internal_note = models.TextField(
|
||||
verbose_name=_("Set Internal Note"),
|
||||
blank=True,
|
||||
help_text=_("Expression for internal note to set"),
|
||||
)
|
||||
set_internal_id = models.TextField(
|
||||
verbose_name=_("Set Internal ID"),
|
||||
blank=True,
|
||||
help_text=_("Expression for internal ID to set"),
|
||||
)
|
||||
set_entities = models.TextField(
|
||||
verbose_name=_("Set Entities"),
|
||||
blank=True,
|
||||
help_text=_("Expression for entities to set (list of IDs or names)"),
|
||||
)
|
||||
set_category = models.TextField(
|
||||
verbose_name=_("Set Category"),
|
||||
blank=True,
|
||||
help_text=_("Expression for category to set (ID or name)"),
|
||||
)
|
||||
set_tags = models.TextField(
|
||||
verbose_name=_("Set Tags"),
|
||||
blank=True,
|
||||
help_text=_("Expression for tags to set (list of IDs or names)"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Update or create transaction action")
|
||||
verbose_name_plural = _("Update or create transaction actions")
|
||||
|
||||
def __str__(self):
|
||||
return f"Update or create transaction action for {self.rule}"
|
||||
|
||||
def build_search_query(self, simple):
|
||||
"""Builds Q objects based on search fields and their operators"""
|
||||
search_query = Q()
|
||||
|
||||
def add_to_query(field_name, value, operator):
|
||||
if isinstance(value, (int, str)):
|
||||
lookup = f"{field_name}__{operator}"
|
||||
return Q(**{lookup: value})
|
||||
return Q()
|
||||
|
||||
if self.search_account:
|
||||
value = simple.eval(self.search_account)
|
||||
if isinstance(value, int):
|
||||
search_query &= add_to_query(
|
||||
"account_id", value, self.search_account_operator
|
||||
)
|
||||
else:
|
||||
search_query &= add_to_query(
|
||||
"account__name", value, self.search_account_operator
|
||||
)
|
||||
|
||||
if self.search_type:
|
||||
value = simple.eval(self.search_type)
|
||||
search_query &= add_to_query("type", value, self.search_type_operator)
|
||||
|
||||
if self.search_is_paid:
|
||||
value = simple.eval(self.search_is_paid)
|
||||
search_query &= add_to_query("is_paid", value, self.search_is_paid_operator)
|
||||
|
||||
if self.search_date:
|
||||
value = simple.eval(self.search_date)
|
||||
search_query &= add_to_query("date", value, self.search_date_operator)
|
||||
|
||||
if self.search_reference_date:
|
||||
value = simple.eval(self.search_reference_date)
|
||||
search_query &= add_to_query(
|
||||
"reference_date", value, self.search_reference_date_operator
|
||||
)
|
||||
|
||||
if self.search_amount:
|
||||
value = simple.eval(self.search_amount)
|
||||
search_query &= add_to_query("amount", value, self.search_amount_operator)
|
||||
|
||||
if self.search_description:
|
||||
value = simple.eval(self.search_description)
|
||||
search_query &= add_to_query(
|
||||
"description", value, self.search_description_operator
|
||||
)
|
||||
|
||||
if self.search_notes:
|
||||
value = simple.eval(self.search_notes)
|
||||
search_query &= add_to_query("notes", value, self.search_notes_operator)
|
||||
|
||||
if self.search_internal_note:
|
||||
value = simple.eval(self.search_internal_note)
|
||||
search_query &= add_to_query(
|
||||
"internal_note", value, self.search_internal_note_operator
|
||||
)
|
||||
|
||||
if self.search_internal_id:
|
||||
value = simple.eval(self.search_internal_id)
|
||||
search_query &= add_to_query(
|
||||
"internal_id", value, self.search_internal_id_operator
|
||||
)
|
||||
|
||||
if self.search_category:
|
||||
value = simple.eval(self.search_category)
|
||||
if isinstance(value, int):
|
||||
search_query &= add_to_query(
|
||||
"category_id", value, self.search_category_operator
|
||||
)
|
||||
else:
|
||||
search_query &= add_to_query(
|
||||
"category__name", value, self.search_category_operator
|
||||
)
|
||||
|
||||
if self.search_tags:
|
||||
tags_value = simple.eval(self.search_tags)
|
||||
if isinstance(tags_value, (list, tuple)):
|
||||
for tag in tags_value:
|
||||
if isinstance(tag, int):
|
||||
search_query &= Q(tags__id=tag)
|
||||
else:
|
||||
search_query &= Q(tags__name__iexact=tag)
|
||||
elif isinstance(tags_value, (int, str)):
|
||||
if isinstance(tags_value, int):
|
||||
search_query &= Q(tags__id=tags_value)
|
||||
else:
|
||||
search_query &= Q(tags__name__iexact=tags_value)
|
||||
|
||||
if self.search_entities:
|
||||
entities_value = simple.eval(self.search_entities)
|
||||
if isinstance(entities_value, (list, tuple)):
|
||||
for entity in entities_value:
|
||||
if isinstance(entity, int):
|
||||
search_query &= Q(entities__id=entity)
|
||||
else:
|
||||
search_query &= Q(entities__name__iexact=entity)
|
||||
elif isinstance(entities_value, (int, str)):
|
||||
if isinstance(entities_value, int):
|
||||
search_query &= Q(entities__id=entities_value)
|
||||
else:
|
||||
search_query &= Q(entities__name__iexact=entities_value)
|
||||
|
||||
return search_query
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import decimal
|
||||
import logging
|
||||
from datetime import datetime, date
|
||||
|
||||
from cachalot.api import cachalot_disabled
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@@ -6,7 +8,10 @@ from procrastinate.contrib.django import app
|
||||
from simpleeval import EvalWithCompoundTypes
|
||||
|
||||
from apps.accounts.models import Account
|
||||
from apps.rules.models import TransactionRule, TransactionRuleAction
|
||||
from apps.rules.models import (
|
||||
TransactionRule,
|
||||
TransactionRuleAction,
|
||||
)
|
||||
from apps.transactions.models import (
|
||||
Transaction,
|
||||
TransactionCategory,
|
||||
@@ -14,7 +19,6 @@ from apps.transactions.models import (
|
||||
TransactionEntity,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -25,137 +29,332 @@ def check_for_transaction_rules(
|
||||
):
|
||||
try:
|
||||
with cachalot_disabled():
|
||||
|
||||
instance = Transaction.objects.get(id=instance_id)
|
||||
|
||||
context = {
|
||||
"account_name": instance.account.name,
|
||||
"account_id": instance.account.id,
|
||||
"account_group_name": (
|
||||
instance.account.group.name if instance.account.group else None
|
||||
),
|
||||
"account_group_id": (
|
||||
instance.account.group.id if instance.account.group else None
|
||||
),
|
||||
"is_asset_account": instance.account.is_asset,
|
||||
"is_archived_account": instance.account.is_archived,
|
||||
"category_name": instance.category.name if instance.category else None,
|
||||
"category_id": instance.category.id if instance.category else None,
|
||||
"tag_names": [x.name for x in instance.tags.all()],
|
||||
"tag_ids": [x.id for x in instance.tags.all()],
|
||||
"entities_names": [x.name for x in instance.entities.all()],
|
||||
"entities_ids": [x.id for x in instance.entities.all()],
|
||||
"is_expense": instance.type == Transaction.Type.EXPENSE,
|
||||
"is_income": instance.type == Transaction.Type.INCOME,
|
||||
"is_paid": instance.is_paid,
|
||||
"description": instance.description,
|
||||
"amount": instance.amount,
|
||||
"notes": instance.notes,
|
||||
"date": instance.date,
|
||||
"reference_date": instance.reference_date,
|
||||
functions = {
|
||||
"relativedelta": relativedelta,
|
||||
"str": str,
|
||||
"int": int,
|
||||
"float": float,
|
||||
"decimal": decimal.Decimal,
|
||||
"datetime": datetime,
|
||||
"date": date,
|
||||
}
|
||||
|
||||
functions = {"relativedelta": relativedelta}
|
||||
|
||||
simple = EvalWithCompoundTypes(names=context, functions=functions)
|
||||
simple = EvalWithCompoundTypes(
|
||||
names=_get_names(instance), functions=functions
|
||||
)
|
||||
|
||||
if signal == "transaction_created":
|
||||
rules = TransactionRule.objects.filter(active=True, on_create=True)
|
||||
rules = TransactionRule.objects.filter(
|
||||
active=True, on_create=True
|
||||
).order_by("id")
|
||||
elif signal == "transaction_updated":
|
||||
rules = TransactionRule.objects.filter(active=True, on_update=True)
|
||||
rules = TransactionRule.objects.filter(
|
||||
active=True, on_update=True
|
||||
).order_by("id")
|
||||
else:
|
||||
rules = TransactionRule.objects.filter(active=True)
|
||||
rules = TransactionRule.objects.filter(active=True).order_by("id")
|
||||
|
||||
for rule in rules:
|
||||
if simple.eval(rule.trigger):
|
||||
for action in rule.transaction_actions.all():
|
||||
if action.field in [
|
||||
TransactionRuleAction.Field.type,
|
||||
TransactionRuleAction.Field.is_paid,
|
||||
TransactionRuleAction.Field.date,
|
||||
TransactionRuleAction.Field.reference_date,
|
||||
TransactionRuleAction.Field.amount,
|
||||
TransactionRuleAction.Field.description,
|
||||
TransactionRuleAction.Field.notes,
|
||||
]:
|
||||
setattr(
|
||||
instance,
|
||||
action.field,
|
||||
simple.eval(action.value),
|
||||
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,
|
||||
)
|
||||
# else:
|
||||
# simple.names.update(_get_names(instance))
|
||||
# instance.save()
|
||||
|
||||
simple.names.update(_get_names(instance))
|
||||
instance.save()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
elif action.field == TransactionRuleAction.Field.account:
|
||||
value = simple.eval(action.value)
|
||||
if isinstance(value, int):
|
||||
account = Account.objects.get(id=value)
|
||||
instance.account = account
|
||||
elif isinstance(value, str):
|
||||
account = Account.objects.filter(name=value).first()
|
||||
instance.account = account
|
||||
|
||||
elif action.field == TransactionRuleAction.Field.category:
|
||||
value = simple.eval(action.value)
|
||||
if isinstance(value, int):
|
||||
category = TransactionCategory.objects.get(id=value)
|
||||
instance.category = category
|
||||
elif isinstance(value, str):
|
||||
category = TransactionCategory.objects.get(name=value)
|
||||
instance.category = category
|
||||
|
||||
elif action.field == TransactionRuleAction.Field.tags:
|
||||
value = simple.eval(action.value)
|
||||
if isinstance(value, list):
|
||||
# Clear existing tags
|
||||
instance.tags.clear()
|
||||
for tag_value in value:
|
||||
if isinstance(tag_value, int):
|
||||
tag = TransactionTag.objects.get(id=tag_value)
|
||||
instance.tags.add(tag)
|
||||
elif isinstance(tag_value, str):
|
||||
tag = TransactionTag.objects.get(name=tag_value)
|
||||
instance.tags.add(tag)
|
||||
|
||||
elif isinstance(value, (int, str)):
|
||||
# If a single value is provided, treat it as a single tag
|
||||
instance.tags.clear()
|
||||
if isinstance(value, int):
|
||||
tag = TransactionTag.objects.get(id=value)
|
||||
else:
|
||||
tag = TransactionTag.objects.get(name=value)
|
||||
|
||||
instance.tags.add(tag)
|
||||
|
||||
elif action.field == TransactionRuleAction.Field.entities:
|
||||
value = simple.eval(action.value)
|
||||
if isinstance(value, list):
|
||||
# Clear existing entities
|
||||
instance.entities.clear()
|
||||
for entity_value in value:
|
||||
if isinstance(entity_value, int):
|
||||
entity = TransactionEntity.objects.get(
|
||||
id=entity_value
|
||||
)
|
||||
instance.entities.add(entity)
|
||||
elif isinstance(entity_value, str):
|
||||
entity = TransactionEntity.objects.get(
|
||||
name=entity_value
|
||||
)
|
||||
instance.entities.add(entity)
|
||||
|
||||
elif isinstance(value, (int, str)):
|
||||
# If a single value is provided, treat it as a single entity
|
||||
instance.entities.clear()
|
||||
if isinstance(value, int):
|
||||
entity = TransactionEntity.objects.get(id=value)
|
||||
else:
|
||||
entity = TransactionEntity.objects.get(name=value)
|
||||
|
||||
instance.entities.add(entity)
|
||||
|
||||
instance.save()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error while executing 'check_for_transaction_rules' task",
|
||||
exc_info=True,
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
def _get_names(instance):
|
||||
return {
|
||||
"id": instance.id,
|
||||
"account_name": instance.account.name,
|
||||
"account_id": instance.account.id,
|
||||
"account_group_name": (
|
||||
instance.account.group.name if instance.account.group else None
|
||||
),
|
||||
"account_group_id": (
|
||||
instance.account.group.id if instance.account.group else None
|
||||
),
|
||||
"is_asset_account": instance.account.is_asset,
|
||||
"is_archived_account": instance.account.is_archived,
|
||||
"category_name": instance.category.name if instance.category else None,
|
||||
"category_id": instance.category.id if instance.category else None,
|
||||
"tag_names": [x.name for x in instance.tags.all()],
|
||||
"tag_ids": [x.id for x in instance.tags.all()],
|
||||
"entities_names": [x.name for x in instance.entities.all()],
|
||||
"entities_ids": [x.id for x in instance.entities.all()],
|
||||
"is_expense": instance.type == Transaction.Type.EXPENSE,
|
||||
"is_income": instance.type == Transaction.Type.INCOME,
|
||||
"is_paid": instance.is_paid,
|
||||
"description": instance.description,
|
||||
"amount": instance.amount,
|
||||
"notes": instance.notes,
|
||||
"date": instance.date,
|
||||
"reference_date": instance.reference_date,
|
||||
"internal_note": instance.internal_note,
|
||||
"internal_id": instance.internal_id,
|
||||
}
|
||||
|
||||
|
||||
def _process_update_or_create_transaction_action(action, simple_eval):
|
||||
"""Helper to process a single linked transaction action"""
|
||||
|
||||
# Build search query using the helper method
|
||||
search_query = action.build_search_query(simple_eval)
|
||||
|
||||
# Find latest matching transaction or create new
|
||||
if search_query:
|
||||
transaction = (
|
||||
Transaction.objects.filter(search_query).order_by("-date", "-id").first()
|
||||
)
|
||||
else:
|
||||
transaction = None
|
||||
|
||||
if not transaction:
|
||||
transaction = Transaction()
|
||||
|
||||
simple_eval.names.update(
|
||||
{
|
||||
"my_account_name": (transaction.account.name if transaction.id else None),
|
||||
"my_account_id": transaction.account.id if transaction.id else None,
|
||||
"my_account_group_name": (
|
||||
transaction.account.group.name
|
||||
if transaction.id and transaction.account.group
|
||||
else None
|
||||
),
|
||||
"my_account_group_id": (
|
||||
transaction.account.group.id
|
||||
if transaction.id and transaction.account.group
|
||||
else None
|
||||
),
|
||||
"my_is_asset_account": (
|
||||
transaction.account.is_asset if transaction.id else None
|
||||
),
|
||||
"my_is_archived_account": (
|
||||
transaction.account.is_archived if transaction.id else None
|
||||
),
|
||||
"my_category_name": (
|
||||
transaction.category.name if transaction.category else None
|
||||
),
|
||||
"my_category_id": transaction.category.id if transaction.category else None,
|
||||
"my_tag_names": (
|
||||
[x.name for x in transaction.tags.all()] if transaction.id else []
|
||||
),
|
||||
"my_tag_ids": (
|
||||
[x.id for x in transaction.tags.all()] if transaction.id else []
|
||||
),
|
||||
"my_entities_names": (
|
||||
[x.name for x in transaction.entities.all()] if transaction.id else []
|
||||
),
|
||||
"my_entities_ids": (
|
||||
[x.id for x in transaction.entities.all()] if transaction.id else []
|
||||
),
|
||||
"my_is_expense": transaction.type == Transaction.Type.EXPENSE,
|
||||
"my_is_income": transaction.type == Transaction.Type.INCOME,
|
||||
"my_is_paid": transaction.is_paid,
|
||||
"my_description": transaction.description,
|
||||
"my_amount": transaction.amount or 0,
|
||||
"my_notes": transaction.notes,
|
||||
"my_date": transaction.date,
|
||||
"my_reference_date": transaction.reference_date,
|
||||
"my_internal_note": transaction.internal_note,
|
||||
"my_internal_id": transaction.reference_date,
|
||||
}
|
||||
)
|
||||
|
||||
if action.filter:
|
||||
value = simple_eval.eval(action.filter)
|
||||
if not value:
|
||||
return # Short-circuit execution if filter evaluates to false
|
||||
|
||||
# Set fields if provided
|
||||
if action.set_account:
|
||||
value = simple_eval.eval(action.set_account)
|
||||
if isinstance(value, int):
|
||||
transaction.account = Account.objects.get(id=value)
|
||||
else:
|
||||
transaction.account = Account.objects.get(name=value)
|
||||
|
||||
if action.set_type:
|
||||
transaction.type = simple_eval.eval(action.set_type)
|
||||
|
||||
if action.set_is_paid:
|
||||
transaction.is_paid = simple_eval.eval(action.set_is_paid)
|
||||
|
||||
if action.set_date:
|
||||
transaction.date = simple_eval.eval(action.set_date)
|
||||
|
||||
if action.set_reference_date:
|
||||
transaction.reference_date = simple_eval.eval(action.set_reference_date)
|
||||
|
||||
if action.set_amount:
|
||||
transaction.amount = simple_eval.eval(action.set_amount)
|
||||
|
||||
if action.set_description:
|
||||
transaction.description = simple_eval.eval(action.set_description)
|
||||
|
||||
if action.set_internal_note:
|
||||
transaction.internal_note = simple_eval.eval(action.set_internal_note)
|
||||
|
||||
if action.set_internal_id:
|
||||
transaction.internal_id = simple_eval.eval(action.set_internal_id)
|
||||
|
||||
if action.set_notes:
|
||||
transaction.notes = simple_eval.eval(action.set_notes)
|
||||
|
||||
if action.set_category:
|
||||
value = simple_eval.eval(action.set_category)
|
||||
if isinstance(value, int):
|
||||
transaction.category = TransactionCategory.objects.get(id=value)
|
||||
else:
|
||||
transaction.category = TransactionCategory.objects.get(name=value)
|
||||
|
||||
transaction.save()
|
||||
|
||||
# Handle M2M fields after save
|
||||
if action.set_tags:
|
||||
tags_value = simple_eval.eval(action.set_tags)
|
||||
transaction.tags.clear()
|
||||
if isinstance(tags_value, (list, tuple)):
|
||||
for tag in tags_value:
|
||||
if isinstance(tag, int):
|
||||
transaction.tags.add(TransactionTag.objects.get(id=tag))
|
||||
else:
|
||||
transaction.tags.add(TransactionTag.objects.get(name=tag))
|
||||
elif isinstance(tags_value, (int, str)):
|
||||
if isinstance(tags_value, int):
|
||||
transaction.tags.add(TransactionTag.objects.get(id=tags_value))
|
||||
else:
|
||||
transaction.tags.add(TransactionTag.objects.get(name=tags_value))
|
||||
|
||||
if action.set_entities:
|
||||
entities_value = simple_eval.eval(action.set_entities)
|
||||
transaction.entities.clear()
|
||||
if isinstance(entities_value, (list, tuple)):
|
||||
for entity in entities_value:
|
||||
if isinstance(entity, int):
|
||||
transaction.entities.add(TransactionEntity.objects.get(id=entity))
|
||||
else:
|
||||
transaction.entities.add(TransactionEntity.objects.get(name=entity))
|
||||
elif isinstance(entities_value, (int, str)):
|
||||
if isinstance(entities_value, int):
|
||||
transaction.entities.add(
|
||||
TransactionEntity.objects.get(id=entities_value)
|
||||
)
|
||||
else:
|
||||
transaction.entities.add(
|
||||
TransactionEntity.objects.get(name=entities_value)
|
||||
)
|
||||
|
||||
|
||||
def _process_edit_transaction_action(instance, action, simple_eval) -> Transaction:
|
||||
if action.field in [
|
||||
TransactionRuleAction.Field.type,
|
||||
TransactionRuleAction.Field.is_paid,
|
||||
TransactionRuleAction.Field.date,
|
||||
TransactionRuleAction.Field.reference_date,
|
||||
TransactionRuleAction.Field.amount,
|
||||
TransactionRuleAction.Field.description,
|
||||
TransactionRuleAction.Field.notes,
|
||||
]:
|
||||
setattr(
|
||||
instance,
|
||||
action.field,
|
||||
simple_eval.eval(action.value),
|
||||
)
|
||||
|
||||
elif action.field == TransactionRuleAction.Field.account:
|
||||
value = simple_eval.eval(action.value)
|
||||
if isinstance(value, int):
|
||||
account = Account.objects.get(id=value)
|
||||
instance.account = account
|
||||
elif isinstance(value, str):
|
||||
account = Account.objects.filter(name=value).first()
|
||||
instance.account = account
|
||||
|
||||
elif action.field == TransactionRuleAction.Field.category:
|
||||
value = simple_eval.eval(action.value)
|
||||
if isinstance(value, int):
|
||||
category = TransactionCategory.objects.get(id=value)
|
||||
instance.category = category
|
||||
elif isinstance(value, str):
|
||||
category = TransactionCategory.objects.get(name=value)
|
||||
instance.category = category
|
||||
|
||||
elif action.field == TransactionRuleAction.Field.tags:
|
||||
value = simple_eval.eval(action.value)
|
||||
if isinstance(value, list):
|
||||
# Clear existing tags
|
||||
instance.tags.clear()
|
||||
for tag_value in value:
|
||||
if isinstance(tag_value, int):
|
||||
tag = TransactionTag.objects.get(id=tag_value)
|
||||
instance.tags.add(tag)
|
||||
elif isinstance(tag_value, str):
|
||||
tag = TransactionTag.objects.get(name=tag_value)
|
||||
instance.tags.add(tag)
|
||||
|
||||
elif isinstance(value, (int, str)):
|
||||
# If a single value is provided, treat it as a single tag
|
||||
instance.tags.clear()
|
||||
if isinstance(value, int):
|
||||
tag = TransactionTag.objects.get(id=value)
|
||||
else:
|
||||
tag = TransactionTag.objects.get(name=value)
|
||||
|
||||
instance.tags.add(tag)
|
||||
|
||||
elif action.field == TransactionRuleAction.Field.entities:
|
||||
value = simple_eval.eval(action.value)
|
||||
if isinstance(value, list):
|
||||
# Clear existing entities
|
||||
instance.entities.clear()
|
||||
for entity_value in value:
|
||||
if isinstance(entity_value, int):
|
||||
entity = TransactionEntity.objects.get(id=entity_value)
|
||||
instance.entities.add(entity)
|
||||
elif isinstance(entity_value, str):
|
||||
entity = TransactionEntity.objects.get(name=entity_value)
|
||||
instance.entities.add(entity)
|
||||
|
||||
elif isinstance(value, (int, str)):
|
||||
# If a single value is provided, treat it as a single entity
|
||||
instance.entities.clear()
|
||||
if isinstance(value, int):
|
||||
entity = TransactionEntity.objects.get(id=value)
|
||||
else:
|
||||
entity = TransactionEntity.objects.get(name=value)
|
||||
|
||||
instance.entities.add(entity)
|
||||
|
||||
return instance
|
||||
|
||||
@@ -38,18 +38,33 @@ urlpatterns = [
|
||||
name="transaction_rule_delete",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/<int:transaction_rule_id>/action/add/",
|
||||
"rules/transaction/<int:transaction_rule_id>/transaction-action/add/",
|
||||
views.transaction_rule_action_add,
|
||||
name="transaction_rule_action_add",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/action/<int:transaction_rule_action_id>/edit/",
|
||||
"rules/transaction/transaction-action/<int:transaction_rule_action_id>/edit/",
|
||||
views.transaction_rule_action_edit,
|
||||
name="transaction_rule_action_edit",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/action/<int:transaction_rule_action_id>/delete/",
|
||||
"rules/transaction/transaction-action/<int:transaction_rule_action_id>/delete/",
|
||||
views.transaction_rule_action_delete,
|
||||
name="transaction_rule_action_delete",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/<int:transaction_rule_id>/update-or-create-transaction-action/add/",
|
||||
views.update_or_create_transaction_rule_action_add,
|
||||
name="update_or_create_transaction_rule_action_add",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/update-or-create-transaction-action/<int:pk>/edit/",
|
||||
views.update_or_create_transaction_rule_action_edit,
|
||||
name="update_or_create_transaction_rule_action_edit",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/update-or-create-transaction-action/<int:pk>/delete/",
|
||||
views.update_or_create_transaction_rule_action_delete,
|
||||
name="update_or_create_transaction_rule_action_delete",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,8 +6,16 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
from apps.rules.forms import TransactionRuleForm, TransactionRuleActionForm
|
||||
from apps.rules.models import TransactionRule, TransactionRuleAction
|
||||
from apps.rules.forms import (
|
||||
TransactionRuleForm,
|
||||
TransactionRuleActionForm,
|
||||
UpdateOrCreateTransactionRuleActionForm,
|
||||
)
|
||||
from apps.rules.models import (
|
||||
TransactionRule,
|
||||
TransactionRuleAction,
|
||||
UpdateOrCreateTransactionRuleAction,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -60,10 +68,15 @@ def transaction_rule_add(request, **kwargs):
|
||||
if request.method == "POST":
|
||||
form = TransactionRuleForm(request.POST)
|
||||
if form.is_valid():
|
||||
instance = form.save()
|
||||
form.save()
|
||||
messages.success(request, _("Rule added successfully"))
|
||||
|
||||
return redirect("transaction_rule_action_add", instance.id)
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = TransactionRuleForm()
|
||||
|
||||
@@ -215,3 +228,88 @@ def transaction_rule_action_delete(request, transaction_rule_action_id):
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def update_or_create_transaction_rule_action_add(request, transaction_rule_id):
|
||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
||||
|
||||
if request.method == "POST":
|
||||
form = UpdateOrCreateTransactionRuleActionForm(
|
||||
request.POST, rule=transaction_rule
|
||||
)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(
|
||||
request, _("Update or Create Transaction action added successfully")
|
||||
)
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = UpdateOrCreateTransactionRuleActionForm(rule=transaction_rule)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"rules/fragments/transaction_rule/update_or_create_transaction_rule_action/add.html",
|
||||
{"form": form, "transaction_rule_id": transaction_rule_id},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def update_or_create_transaction_rule_action_edit(request, pk):
|
||||
linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk)
|
||||
transaction_rule = linked_action.rule
|
||||
|
||||
if request.method == "POST":
|
||||
form = UpdateOrCreateTransactionRuleActionForm(
|
||||
request.POST, instance=linked_action, rule=transaction_rule
|
||||
)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(
|
||||
request, _("Update or Create Transaction action updated successfully")
|
||||
)
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = UpdateOrCreateTransactionRuleActionForm(
|
||||
instance=linked_action, rule=transaction_rule
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"rules/fragments/transaction_rule/update_or_create_transaction_rule_action/edit.html",
|
||||
{"form": form, "action": linked_action},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["DELETE"])
|
||||
def update_or_create_transaction_rule_action_delete(request, pk):
|
||||
linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk)
|
||||
|
||||
linked_action.delete()
|
||||
|
||||
messages.success(
|
||||
request, _("Update or Create Transaction action deleted successfully")
|
||||
)
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends 'extends/offcanvas.html' %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% translate 'Add action to transaction rule' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form hx-post="{% url 'update_or_create_transaction_rule_action_add' transaction_rule_id=transaction_rule_id %}" hx-target="#generic-offcanvas" novalidate>
|
||||
{% crispy form %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends 'extends/offcanvas.html' %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% translate 'Edit transaction rule action' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form hx-post="{% url 'update_or_create_transaction_rule_action_edit' pk=action.id %}" hx-target="#generic-offcanvas" novalidate>
|
||||
{% crispy form %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -5,80 +5,121 @@
|
||||
{% block title %}{% translate 'Transaction Rule' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div hx-get="{% url 'transaction_rule_view' transaction_rule_id=transaction_rule.id %}" hx-trigger="updated from:window" hx-target="closest .offcanvas" class="show-loading">
|
||||
<div class="tw-text-2xl">{{ transaction_rule.name }}</div>
|
||||
<div class="tw-text-base tw-text-gray-400">{{ transaction_rule.description }}</div>
|
||||
<hr>
|
||||
<div class="my-3">
|
||||
<div class="tw-text-xl">{% translate 'If transaction...' %}</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ transaction_rule.trigger }}
|
||||
</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_edit' transaction_rule_id=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">
|
||||
<div hx-get="{% url 'transaction_rule_view' transaction_rule_id=transaction_rule.id %}"
|
||||
hx-trigger="updated from:window" hx-target="closest .offcanvas" class="show-loading">
|
||||
<div class="tw-text-2xl">{{ transaction_rule.name }}</div>
|
||||
<div class="tw-text-base tw-text-gray-400">{{ transaction_rule.description }}</div>
|
||||
<hr>
|
||||
<div class="my-3">
|
||||
<div class="tw-text-xl mb-2">{% translate 'If transaction...' %}</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ transaction_rule.trigger }}
|
||||
</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_edit' transaction_rule_id=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<div class="tw-text-xl">{% translate 'Then...' %}</div>
|
||||
{% for action in transaction_rule.transaction_actions.all %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">{% translate 'Set' %}</div>
|
||||
<div class="card-body">{{ action.get_field_display }}</div>
|
||||
<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">
|
||||
<i class="fa-solid fa-trash fa-fw"></i>
|
||||
</a>
|
||||
</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">
|
||||
<i class="fa-solid fa-trash fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not transaction_rule.update_or_create_transaction_actions.all and not transaction_rule.transaction_actions.all %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% translate 'This rule has no actions' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<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>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">{% translate 'to' %}</div>
|
||||
<div class="card-body">{{ action.value }}</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% translate 'This rule has no actions' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<hr>
|
||||
<a class="btn btn-outline-primary text-decoration-none w-100"
|
||||
hx-get="{% url 'transaction_rule_action_add' transaction_rule_id=transaction_rule.id %}"
|
||||
role="button"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-circle-plus me-2"></i>{% translate 'Add new' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user