From 3c0a2d82ac9ca695d4033e35e33d09a391bb10dd Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Sat, 4 Jan 2025 18:13:11 -0300 Subject: [PATCH 1/2] feat: allow for deactivating Tags, Categories and Entities, hiding them from menus --- app/apps/accounts/forms.py | 2 + app/apps/transactions/forms.py | 85 +++++++++++++++++-- ...oncategory_active_transactiontag_active.py | 23 +++++ .../0026_transactionentity_active.py | 18 ++++ app/apps/transactions/models.py | 23 ++++- 5 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 app/apps/transactions/migrations/0025_transactioncategory_active_transactiontag_active.py create mode 100644 app/apps/transactions/migrations/0026_transactionentity_active.py diff --git a/app/apps/accounts/forms.py b/app/apps/accounts/forms.py index ba83092..748c8d0 100644 --- a/app/apps/accounts/forms.py +++ b/app/apps/accounts/forms.py @@ -53,6 +53,7 @@ class AccountGroupForm(forms.ModelForm): class AccountForm(forms.ModelForm): group = DynamicModelChoiceField( + create_field="name", label=_("Group"), model=AccountGroup, required=False, @@ -112,6 +113,7 @@ class AccountBalanceForm(forms.Form): max_digits=42, decimal_places=30, required=False, label=_("New balance") ) category = DynamicModelChoiceField( + create_field="name", model=TransactionCategory, required=False, label=_("Category"), diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py index bcdc200..e2b7ecc 100644 --- a/app/apps/transactions/forms.py +++ b/app/apps/transactions/forms.py @@ -8,6 +8,7 @@ from crispy_forms.layout import ( Field, ) from django import forms +from django.db.models import Q from django.utils.translation import gettext_lazy as _ from apps.accounts.models import Account @@ -32,9 +33,11 @@ from apps.rules.signals import transaction_created, transaction_updated class TransactionForm(forms.ModelForm): category = DynamicModelChoiceField( + create_field="name", model=TransactionCategory, required=False, label=_("Category"), + queryset=TransactionCategory.objects.filter(active=True), ) tags = DynamicModelMultipleChoiceField( model=TransactionTag, @@ -42,6 +45,7 @@ class TransactionForm(forms.ModelForm): create_field="name", required=False, label=_("Tags"), + queryset=TransactionTag.objects.filter(active=True), ) entities = DynamicModelMultipleChoiceField( model=TransactionEntity, @@ -81,6 +85,24 @@ class TransactionForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # if editing a transaction display non-archived items and it's own item even if it's archived + if self.instance.id: + self.fields["account"].queryset = Account.objects.filter( + Q(is_archived=False) | Q(transactions=self.instance.id) + ).distinct() + + self.fields["category"].queryset = TransactionCategory.objects.filter( + Q(active=True) | Q(transaction=self.instance.id) + ).distinct() + + self.fields["tags"].queryset = TransactionTag.objects.filter( + Q(active=True) | Q(transaction=self.instance.id) + ).distinct() + + self.fields["entities"].queryset = TransactionEntity.objects.filter( + Q(active=True) | Q(transactions=self.instance.id) + ).distinct() + self.helper = FormHelper() self.helper.form_tag = False self.helper.form_method = "post" @@ -181,14 +203,18 @@ class TransferForm(forms.Form): ) from_category = DynamicModelChoiceField( + create_field="name", model=TransactionCategory, required=False, label=_("Category"), + queryset=TransactionCategory.objects.filter(active=True), ) to_category = DynamicModelChoiceField( + create_field="name", model=TransactionCategory, required=False, label=_("Category"), + queryset=TransactionCategory.objects.filter(active=True), ) from_tags = DynamicModelMultipleChoiceField( @@ -197,6 +223,7 @@ class TransferForm(forms.Form): create_field="name", required=False, label=_("Tags"), + queryset=TransactionTag.objects.filter(active=True), ) to_tags = DynamicModelMultipleChoiceField( model=TransactionTag, @@ -204,6 +231,7 @@ class TransferForm(forms.Form): create_field="name", required=False, label=_("Tags"), + queryset=TransactionTag.objects.filter(active=True), ) date = forms.DateField( @@ -358,11 +386,14 @@ class InstallmentPlanForm(forms.ModelForm): create_field="name", required=False, label=_("Tags"), + queryset=TransactionTag.objects.filter(active=True), ) category = DynamicModelChoiceField( + create_field="name", model=TransactionCategory, required=False, label=_("Category"), + queryset=TransactionCategory.objects.filter(active=True), ) entities = DynamicModelMultipleChoiceField( model=TransactionEntity, @@ -370,6 +401,7 @@ class InstallmentPlanForm(forms.ModelForm): create_field="name", required=False, label=_("Entities"), + queryset=TransactionEntity.objects.filter(active=True), ) type = forms.ChoiceField(choices=Transaction.Type.choices) reference_date = MonthYearFormField(label=_("Reference Date"), required=False) @@ -401,6 +433,24 @@ class InstallmentPlanForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # if editing display non-archived items and it's own item even if it's archived + if self.instance.id: + self.fields["account"].queryset = Account.objects.filter( + Q(is_archived=False) | Q(installmentplan=self.instance.id) + ).distinct() + + self.fields["category"].queryset = TransactionCategory.objects.filter( + Q(active=True) | Q(installmentplan=self.instance.id) + ).distinct() + + self.fields["tags"].queryset = TransactionTag.objects.filter( + Q(active=True) | Q(installmentplan=self.instance.id) + ).distinct() + + self.fields["entities"].queryset = TransactionEntity.objects.filter( + Q(active=True) | Q(installmentplan=self.instance.id) + ).distinct() + self.helper = FormHelper() self.helper.form_tag = False self.helper.form_method = "post" @@ -470,7 +520,7 @@ class InstallmentPlanForm(forms.ModelForm): class TransactionTagForm(forms.ModelForm): class Meta: model = TransactionTag - fields = ["name"] + fields = ["name", "active"] labels = {"name": _("Tag name")} def __init__(self, *args, **kwargs): @@ -479,7 +529,7 @@ class TransactionTagForm(forms.ModelForm): self.helper = FormHelper() self.helper.form_tag = False self.helper.form_method = "post" - self.helper.layout = Layout(Field("name", css_class="mb-3")) + self.helper.layout = Layout(Field("name", css_class="mb-3"), Switch("active")) if self.instance and self.instance.pk: self.helper.layout.append( @@ -502,7 +552,7 @@ class TransactionTagForm(forms.ModelForm): class TransactionEntityForm(forms.ModelForm): class Meta: model = TransactionEntity - fields = ["name"] + fields = ["name", "active"] labels = {"name": _("Entity name")} def __init__(self, *args, **kwargs): @@ -511,7 +561,7 @@ class TransactionEntityForm(forms.ModelForm): self.helper = FormHelper() self.helper.form_tag = False self.helper.form_method = "post" - self.helper.layout = Layout(Field("name", css_class="mb-3")) + self.helper.layout = Layout(Field("name"), Switch("active")) if self.instance and self.instance.pk: self.helper.layout.append( @@ -534,7 +584,7 @@ class TransactionEntityForm(forms.ModelForm): class TransactionCategoryForm(forms.ModelForm): class Meta: model = TransactionCategory - fields = ["name", "mute"] + fields = ["name", "mute", "active"] labels = {"name": _("Category name")} help_texts = { "mute": _("Muted categories won't count towards your monthly total") @@ -546,7 +596,7 @@ class TransactionCategoryForm(forms.ModelForm): self.helper = FormHelper() self.helper.form_tag = False self.helper.form_method = "post" - self.helper.layout = Layout(Field("name", css_class="mb-3"), Switch("mute")) + self.helper.layout = Layout(Field("name"), Switch("mute"), Switch("active")) if self.instance and self.instance.pk: self.helper.layout.append( @@ -578,11 +628,14 @@ class RecurringTransactionForm(forms.ModelForm): create_field="name", required=False, label=_("Tags"), + queryset=TransactionTag.objects.filter(active=True), ) category = DynamicModelChoiceField( + create_field="name", model=TransactionCategory, required=False, label=_("Category"), + queryset=TransactionCategory.objects.filter(active=True), ) entities = DynamicModelMultipleChoiceField( model=TransactionEntity, @@ -590,6 +643,7 @@ class RecurringTransactionForm(forms.ModelForm): create_field="name", required=False, label=_("Entities"), + queryset=TransactionEntity.objects.filter(active=True), ) type = forms.ChoiceField(choices=Transaction.Type.choices) reference_date = MonthYearFormField(label=_("Reference Date"), required=False) @@ -624,6 +678,25 @@ class RecurringTransactionForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + + # if editing display non-archived items and it's own item even if it's archived + if self.instance.id: + self.fields["account"].queryset = Account.objects.filter( + Q(is_archived=False) | Q(recurringtransaction=self.instance.id) + ).distinct() + + self.fields["category"].queryset = TransactionCategory.objects.filter( + Q(active=True) | Q(recurringtransaction=self.instance.id) + ).distinct() + + self.fields["tags"].queryset = TransactionTag.objects.filter( + Q(active=True) | Q(recurringtransaction=self.instance.id) + ).distinct() + + self.fields["entities"].queryset = TransactionEntity.objects.filter( + Q(active=True) | Q(recurringtransaction=self.instance.id) + ).distinct() + self.helper = FormHelper() self.helper.form_method = "post" self.helper.form_tag = False diff --git a/app/apps/transactions/migrations/0025_transactioncategory_active_transactiontag_active.py b/app/apps/transactions/migrations/0025_transactioncategory_active_transactiontag_active.py new file mode 100644 index 0000000..5b5794e --- /dev/null +++ b/app/apps/transactions/migrations/0025_transactioncategory_active_transactiontag_active.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.3 on 2025-01-04 19:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transactions', '0024_installmentplan_entities_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='transactioncategory', + name='active', + field=models.BooleanField(default=True, help_text="Deactivated categories won't be able to be selected when creating new transactions", verbose_name='Active'), + ), + migrations.AddField( + model_name='transactiontag', + name='active', + field=models.BooleanField(default=True, help_text="Deactivated tags won't be able to be selected when creating new transactions", verbose_name='Active'), + ), + ] diff --git a/app/apps/transactions/migrations/0026_transactionentity_active.py b/app/apps/transactions/migrations/0026_transactionentity_active.py new file mode 100644 index 0000000..4f66003 --- /dev/null +++ b/app/apps/transactions/migrations/0026_transactionentity_active.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2025-01-04 19:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transactions', '0025_transactioncategory_active_transactiontag_active'), + ] + + operations = [ + migrations.AddField( + model_name='transactionentity', + name='active', + field=models.BooleanField(default=True, help_text="Deactivated entities won't be able to be selected when creating new transactions", verbose_name='Active'), + ), + ] diff --git a/app/apps/transactions/models.py b/app/apps/transactions/models.py index 8728a53..b503887 100644 --- a/app/apps/transactions/models.py +++ b/app/apps/transactions/models.py @@ -18,6 +18,13 @@ logger = logging.getLogger() class TransactionCategory(models.Model): name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True) mute = models.BooleanField(default=False, verbose_name=_("Mute")) + active = models.BooleanField( + default=True, + verbose_name=_("Active"), + help_text=_( + "Deactivated categories won't be able to be selected when creating new transactions" + ), + ) class Meta: verbose_name = _("Transaction Category") @@ -30,6 +37,13 @@ class TransactionCategory(models.Model): class TransactionTag(models.Model): name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True) + active = models.BooleanField( + default=True, + verbose_name=_("Active"), + help_text=_( + "Deactivated tags won't be able to be selected when creating new transactions" + ), + ) class Meta: verbose_name = _("Transaction Tags") @@ -42,8 +56,13 @@ class TransactionTag(models.Model): class TransactionEntity(models.Model): name = models.CharField(max_length=255, verbose_name=_("Name")) - - # Add any other fields you might want for entities + active = models.BooleanField( + default=True, + verbose_name=_("Active"), + help_text=_( + "Deactivated entities won't be able to be selected when creating new transactions" + ), + ) class Meta: verbose_name = _("Entity") From 5ccb9ff152d9b680379a41f1357ffc5cd121cd96 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Sat, 4 Jan 2025 18:17:06 -0300 Subject: [PATCH 2/2] locale: add lazy translations to missing ValidationErrors --- app/apps/api/fields/transactions.py | 13 +++++++------ app/apps/common/fields/forms/dynamic_select.py | 5 +++-- app/apps/common/fields/month_year.py | 2 +- app/apps/transactions/forms.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/apps/api/fields/transactions.py b/app/apps/api/fields/transactions.py index 2ca9466..eceaebe 100644 --- a/app/apps/api/fields/transactions.py +++ b/app/apps/api/fields/transactions.py @@ -1,6 +1,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers +from django.utils.translation import gettext_lazy as _ from apps.transactions.models import ( TransactionCategory, @@ -25,13 +26,13 @@ class TransactionCategoryField(serializers.Field): return TransactionCategory.objects.get(pk=data) except TransactionCategory.DoesNotExist: raise serializers.ValidationError( - "Category with this ID does not exist." + _("Category with this ID does not exist.") ) elif isinstance(data, str): category, created = TransactionCategory.objects.get_or_create(name=data) return category raise serializers.ValidationError( - "Invalid category data. Provide an ID or name." + _("Invalid category data. Provide an ID or name.") ) @staticmethod @@ -61,13 +62,13 @@ class TransactionTagField(serializers.Field): tag = TransactionTag.objects.get(pk=item) except TransactionTag.DoesNotExist: raise serializers.ValidationError( - f"Tag with ID {item} does not exist." + _("Tag with this ID does not exist.") ) elif isinstance(item, str): tag, created = TransactionTag.objects.get_or_create(name=item) else: raise serializers.ValidationError( - "Invalid tag data. Provide an ID or name." + _("Invalid tag data. Provide an ID or name.") ) tags.append(tag) return tags @@ -85,13 +86,13 @@ class TransactionEntityField(serializers.Field): entity = TransactionEntity.objects.get(pk=item) except TransactionTag.DoesNotExist: raise serializers.ValidationError( - f"Entity with ID {item} does not exist." + _("Entity with this ID does not exist.") ) elif isinstance(item, str): entity, created = TransactionEntity.objects.get_or_create(name=item) else: raise serializers.ValidationError( - "Invalid entity data. Provide an ID or name." + _("Invalid entity data. Provide an ID or name.") ) entities.append(entity) return entities diff --git a/app/apps/common/fields/forms/dynamic_select.py b/app/apps/common/fields/forms/dynamic_select.py index f5a6a02..2d2ab01 100644 --- a/app/apps/common/fields/forms/dynamic_select.py +++ b/app/apps/common/fields/forms/dynamic_select.py @@ -1,6 +1,7 @@ from django import forms from django.core.exceptions import ValidationError from django.db import transaction +from django.utils.translation import gettext_lazy as _ from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple @@ -124,7 +125,7 @@ class DynamicModelMultipleChoiceField(forms.ModelMultipleChoiceField): ) return instance except Exception as e: - raise ValidationError(f"Error creating new instance: {str(e)}") + raise ValidationError(_("Error creating new instance")) def clean(self, value): """ @@ -160,6 +161,6 @@ class DynamicModelMultipleChoiceField(forms.ModelMultipleChoiceField): try: new_objects.append(self._create_new_instance(new_value)) except ValidationError as e: - raise ValidationError(f"Error creating '{new_value}': {str(e)}") + raise ValidationError(_("Error creating new instance")) return existing_objects + new_objects diff --git a/app/apps/common/fields/month_year.py b/app/apps/common/fields/month_year.py index ffb24bf..70eb33c 100644 --- a/app/apps/common/fields/month_year.py +++ b/app/apps/common/fields/month_year.py @@ -18,7 +18,7 @@ class MonthYearModelField(models.DateField): # Set the day to 1 return date.replace(day=1).date() except ValueError: - raise ValidationError("Invalid date format. Use YYYY-MM.") + raise ValidationError(_("Invalid date format. Use YYYY-MM.")) def formfield(self, **kwargs): kwargs["widget"] = MonthYearWidget diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py index e2b7ecc..0d8db9b 100644 --- a/app/apps/transactions/forms.py +++ b/app/apps/transactions/forms.py @@ -327,7 +327,7 @@ class TransferForm(forms.Form): to_account = cleaned_data.get("to_account") if from_account == to_account: - raise forms.ValidationError("From and To accounts must be different.") + raise forms.ValidationError(_("From and To accounts must be different.")) return cleaned_data