Files
WYGIWYH/app/apps/rules/models.py
2025-02-08 04:16:28 -03:00

425 lines
14 KiB
Python

from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
class TransactionRule(models.Model):
active = models.BooleanField(default=True)
on_update = models.BooleanField(default=False)
on_create = models.BooleanField(default=True)
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"))
class Meta:
verbose_name = _("Transaction rule")
verbose_name_plural = _("Transaction rules")
def __str__(self):
return self.name
class TransactionRuleAction(models.Model):
class Field(models.TextChoices):
account = "account", _("Account")
type = "type", _("Type")
is_paid = "is_paid", _("Paid")
date = "date", _("Date")
reference_date = "reference_date", _("Reference Date")
amount = "amount", _("Amount")
description = "description", _("Description")
notes = "notes", _("Notes")
category = "category", _("Category")
tags = "tags", _("Tags")
entities = "entities", _("Entities")
rule = models.ForeignKey(
TransactionRule,
on_delete=models.CASCADE,
related_name="transaction_actions",
verbose_name=_("Rule"),
)
field = models.CharField(
max_length=50,
choices=Field,
verbose_name=_("Field"),
)
value = models.TextField(verbose_name=_("Value"))
def __str__(self):
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