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