Files
WYGIWYH/app/apps/rules/models.py
Herculino Trotta d724300513 changes
2025-09-02 15:54:45 -03:00

444 lines
14 KiB
Python

from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from apps.common.models import SharedObject, SharedObjectManager
class TransactionRule(SharedObject):
active = models.BooleanField(default=True)
on_update = models.BooleanField(default=False)
on_create = models.BooleanField(default=True)
on_delete = models.BooleanField(default=False)
name = models.CharField(max_length=100, verbose_name=_("Name"))
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
trigger = models.TextField(verbose_name=_("Trigger"))
sequenced = models.BooleanField(
verbose_name=_("Sequenced"),
default=False,
)
order = models.PositiveIntegerField(default=0, verbose_name=_("Order"))
objects = SharedObjectManager()
all_objects = models.Manager() # Unfiltered manager
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")
mute = "mute", _("Mute")
amount = "amount", _("Amount")
description = "description", _("Description")
notes = "notes", _("Notes")
category = "category", _("Category")
tags = "tags", _("Tags")
entities = "entities", _("Entities")
internal_note = "internal_nome", _("Internal Note")
internal_id = "internal_id", _("Internal ID")
rule = models.ForeignKey(
TransactionRule,
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"))
order = models.PositiveIntegerField(default=0, verbose_name=_("Order"))
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"),)
ordering = ["order"]
@property
def action_type(self):
return "edit_transaction"
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,
)
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,
)
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,
)
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,
)
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,
)
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,
)
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,
)
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,
)
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,
)
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,
)
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,
)
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,
)
search_internal_id_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Internal ID Operator",
)
search_mute = models.TextField(
verbose_name="Search Mute",
blank=True,
)
search_mute_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Mute Operator",
)
# Set fields
set_account = models.TextField(
verbose_name=_("Account"),
blank=True,
)
set_type = models.TextField(
verbose_name=_("Type"),
blank=True,
)
set_is_paid = models.TextField(
verbose_name=_("Paid"),
blank=True,
)
set_date = models.TextField(
verbose_name=_("Date"),
blank=True,
)
set_reference_date = models.TextField(
verbose_name=_("Reference Date"),
blank=True,
)
set_amount = models.TextField(
verbose_name=_("Amount"),
blank=True,
)
set_description = models.TextField(
verbose_name=_("Description"),
blank=True,
)
set_notes = models.TextField(
verbose_name=_("Notes"),
blank=True,
)
set_internal_note = models.TextField(
verbose_name=_("Internal Note"),
blank=True,
)
set_internal_id = models.TextField(
verbose_name=_("Internal ID"),
blank=True,
)
set_entities = models.TextField(
verbose_name=_("Entities"),
blank=True,
)
set_category = models.TextField(
verbose_name=_("Category"),
blank=True,
)
set_tags = models.TextField(
verbose_name=_("Tags"),
blank=True,
)
set_mute = models.TextField(
verbose_name=_("Mute"),
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}"
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):
lookup = f"{field_name}__{operator}"
return Q(**{lookup: value})
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_mute:
value = simple.eval(self.search_mute)
search_query &= add_to_query("mute", value, self.search_mute_operator)
if self.search_date:
value = simple.eval(self.search_date)
search_query &= add_to_query("date", value, self.search_date_operator)
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