Merge pull request #18

feat: allow for deactivating Tags, Categories and Entities, hiding them from menus
This commit is contained in:
Herculino Trotta
2025-01-04 18:17:42 -03:00
committed by GitHub
8 changed files with 155 additions and 18 deletions
+2
View File
@@ -53,6 +53,7 @@ class AccountGroupForm(forms.ModelForm):
class AccountForm(forms.ModelForm): class AccountForm(forms.ModelForm):
group = DynamicModelChoiceField( group = DynamicModelChoiceField(
create_field="name",
label=_("Group"), label=_("Group"),
model=AccountGroup, model=AccountGroup,
required=False, required=False,
@@ -112,6 +113,7 @@ class AccountBalanceForm(forms.Form):
max_digits=42, decimal_places=30, required=False, label=_("New balance") max_digits=42, decimal_places=30, required=False, label=_("New balance")
) )
category = DynamicModelChoiceField( category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory, model=TransactionCategory,
required=False, required=False,
label=_("Category"), label=_("Category"),
+7 -6
View File
@@ -1,6 +1,7 @@
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from apps.transactions.models import ( from apps.transactions.models import (
TransactionCategory, TransactionCategory,
@@ -25,13 +26,13 @@ class TransactionCategoryField(serializers.Field):
return TransactionCategory.objects.get(pk=data) return TransactionCategory.objects.get(pk=data)
except TransactionCategory.DoesNotExist: except TransactionCategory.DoesNotExist:
raise serializers.ValidationError( raise serializers.ValidationError(
"Category with this ID does not exist." _("Category with this ID does not exist.")
) )
elif isinstance(data, str): elif isinstance(data, str):
category, created = TransactionCategory.objects.get_or_create(name=data) category, created = TransactionCategory.objects.get_or_create(name=data)
return category return category
raise serializers.ValidationError( raise serializers.ValidationError(
"Invalid category data. Provide an ID or name." _("Invalid category data. Provide an ID or name.")
) )
@staticmethod @staticmethod
@@ -61,13 +62,13 @@ class TransactionTagField(serializers.Field):
tag = TransactionTag.objects.get(pk=item) tag = TransactionTag.objects.get(pk=item)
except TransactionTag.DoesNotExist: except TransactionTag.DoesNotExist:
raise serializers.ValidationError( raise serializers.ValidationError(
f"Tag with ID {item} does not exist." _("Tag with this ID does not exist.")
) )
elif isinstance(item, str): elif isinstance(item, str):
tag, created = TransactionTag.objects.get_or_create(name=item) tag, created = TransactionTag.objects.get_or_create(name=item)
else: else:
raise serializers.ValidationError( raise serializers.ValidationError(
"Invalid tag data. Provide an ID or name." _("Invalid tag data. Provide an ID or name.")
) )
tags.append(tag) tags.append(tag)
return tags return tags
@@ -85,13 +86,13 @@ class TransactionEntityField(serializers.Field):
entity = TransactionEntity.objects.get(pk=item) entity = TransactionEntity.objects.get(pk=item)
except TransactionTag.DoesNotExist: except TransactionTag.DoesNotExist:
raise serializers.ValidationError( raise serializers.ValidationError(
f"Entity with ID {item} does not exist." _("Entity with this ID does not exist.")
) )
elif isinstance(item, str): elif isinstance(item, str):
entity, created = TransactionEntity.objects.get_or_create(name=item) entity, created = TransactionEntity.objects.get_or_create(name=item)
else: else:
raise serializers.ValidationError( raise serializers.ValidationError(
"Invalid entity data. Provide an ID or name." _("Invalid entity data. Provide an ID or name.")
) )
entities.append(entity) entities.append(entity)
return entities return entities
@@ -1,6 +1,7 @@
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.utils.translation import gettext_lazy as _
from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple
@@ -124,7 +125,7 @@ class DynamicModelMultipleChoiceField(forms.ModelMultipleChoiceField):
) )
return instance return instance
except Exception as e: except Exception as e:
raise ValidationError(f"Error creating new instance: {str(e)}") raise ValidationError(_("Error creating new instance"))
def clean(self, value): def clean(self, value):
""" """
@@ -160,6 +161,6 @@ class DynamicModelMultipleChoiceField(forms.ModelMultipleChoiceField):
try: try:
new_objects.append(self._create_new_instance(new_value)) new_objects.append(self._create_new_instance(new_value))
except ValidationError as e: except ValidationError as e:
raise ValidationError(f"Error creating '{new_value}': {str(e)}") raise ValidationError(_("Error creating new instance"))
return existing_objects + new_objects return existing_objects + new_objects
+1 -1
View File
@@ -18,7 +18,7 @@ class MonthYearModelField(models.DateField):
# Set the day to 1 # Set the day to 1
return date.replace(day=1).date() return date.replace(day=1).date()
except ValueError: except ValueError:
raise ValidationError("Invalid date format. Use YYYY-MM.") raise ValidationError(_("Invalid date format. Use YYYY-MM."))
def formfield(self, **kwargs): def formfield(self, **kwargs):
kwargs["widget"] = MonthYearWidget kwargs["widget"] = MonthYearWidget
+80 -7
View File
@@ -8,6 +8,7 @@ from crispy_forms.layout import (
Field, Field,
) )
from django import forms from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from apps.accounts.models import Account from apps.accounts.models import Account
@@ -32,9 +33,11 @@ from apps.rules.signals import transaction_created, transaction_updated
class TransactionForm(forms.ModelForm): class TransactionForm(forms.ModelForm):
category = DynamicModelChoiceField( category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory, model=TransactionCategory,
required=False, required=False,
label=_("Category"), label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
) )
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
model=TransactionTag, model=TransactionTag,
@@ -42,6 +45,7 @@ class TransactionForm(forms.ModelForm):
create_field="name", create_field="name",
required=False, required=False,
label=_("Tags"), label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
) )
entities = DynamicModelMultipleChoiceField( entities = DynamicModelMultipleChoiceField(
model=TransactionEntity, model=TransactionEntity,
@@ -81,6 +85,24 @@ class TransactionForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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 = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.form_method = "post" self.helper.form_method = "post"
@@ -181,14 +203,18 @@ class TransferForm(forms.Form):
) )
from_category = DynamicModelChoiceField( from_category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory, model=TransactionCategory,
required=False, required=False,
label=_("Category"), label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
) )
to_category = DynamicModelChoiceField( to_category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory, model=TransactionCategory,
required=False, required=False,
label=_("Category"), label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
) )
from_tags = DynamicModelMultipleChoiceField( from_tags = DynamicModelMultipleChoiceField(
@@ -197,6 +223,7 @@ class TransferForm(forms.Form):
create_field="name", create_field="name",
required=False, required=False,
label=_("Tags"), label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
) )
to_tags = DynamicModelMultipleChoiceField( to_tags = DynamicModelMultipleChoiceField(
model=TransactionTag, model=TransactionTag,
@@ -204,6 +231,7 @@ class TransferForm(forms.Form):
create_field="name", create_field="name",
required=False, required=False,
label=_("Tags"), label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
) )
date = forms.DateField( date = forms.DateField(
@@ -299,7 +327,7 @@ class TransferForm(forms.Form):
to_account = cleaned_data.get("to_account") to_account = cleaned_data.get("to_account")
if from_account == 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 return cleaned_data
@@ -358,11 +386,14 @@ class InstallmentPlanForm(forms.ModelForm):
create_field="name", create_field="name",
required=False, required=False,
label=_("Tags"), label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
) )
category = DynamicModelChoiceField( category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory, model=TransactionCategory,
required=False, required=False,
label=_("Category"), label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
) )
entities = DynamicModelMultipleChoiceField( entities = DynamicModelMultipleChoiceField(
model=TransactionEntity, model=TransactionEntity,
@@ -370,6 +401,7 @@ class InstallmentPlanForm(forms.ModelForm):
create_field="name", create_field="name",
required=False, required=False,
label=_("Entities"), label=_("Entities"),
queryset=TransactionEntity.objects.filter(active=True),
) )
type = forms.ChoiceField(choices=Transaction.Type.choices) type = forms.ChoiceField(choices=Transaction.Type.choices)
reference_date = MonthYearFormField(label=_("Reference Date"), required=False) reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
@@ -401,6 +433,24 @@ class InstallmentPlanForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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 = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.form_method = "post" self.helper.form_method = "post"
@@ -470,7 +520,7 @@ class InstallmentPlanForm(forms.ModelForm):
class TransactionTagForm(forms.ModelForm): class TransactionTagForm(forms.ModelForm):
class Meta: class Meta:
model = TransactionTag model = TransactionTag
fields = ["name"] fields = ["name", "active"]
labels = {"name": _("Tag name")} labels = {"name": _("Tag name")}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -479,7 +529,7 @@ class TransactionTagForm(forms.ModelForm):
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.form_method = "post" 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: if self.instance and self.instance.pk:
self.helper.layout.append( self.helper.layout.append(
@@ -502,7 +552,7 @@ class TransactionTagForm(forms.ModelForm):
class TransactionEntityForm(forms.ModelForm): class TransactionEntityForm(forms.ModelForm):
class Meta: class Meta:
model = TransactionEntity model = TransactionEntity
fields = ["name"] fields = ["name", "active"]
labels = {"name": _("Entity name")} labels = {"name": _("Entity name")}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -511,7 +561,7 @@ class TransactionEntityForm(forms.ModelForm):
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.form_method = "post" 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: if self.instance and self.instance.pk:
self.helper.layout.append( self.helper.layout.append(
@@ -534,7 +584,7 @@ class TransactionEntityForm(forms.ModelForm):
class TransactionCategoryForm(forms.ModelForm): class TransactionCategoryForm(forms.ModelForm):
class Meta: class Meta:
model = TransactionCategory model = TransactionCategory
fields = ["name", "mute"] fields = ["name", "mute", "active"]
labels = {"name": _("Category name")} labels = {"name": _("Category name")}
help_texts = { help_texts = {
"mute": _("Muted categories won't count towards your monthly total") "mute": _("Muted categories won't count towards your monthly total")
@@ -546,7 +596,7 @@ class TransactionCategoryForm(forms.ModelForm):
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.form_method = "post" 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: if self.instance and self.instance.pk:
self.helper.layout.append( self.helper.layout.append(
@@ -578,11 +628,14 @@ class RecurringTransactionForm(forms.ModelForm):
create_field="name", create_field="name",
required=False, required=False,
label=_("Tags"), label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
) )
category = DynamicModelChoiceField( category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory, model=TransactionCategory,
required=False, required=False,
label=_("Category"), label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
) )
entities = DynamicModelMultipleChoiceField( entities = DynamicModelMultipleChoiceField(
model=TransactionEntity, model=TransactionEntity,
@@ -590,6 +643,7 @@ class RecurringTransactionForm(forms.ModelForm):
create_field="name", create_field="name",
required=False, required=False,
label=_("Entities"), label=_("Entities"),
queryset=TransactionEntity.objects.filter(active=True),
) )
type = forms.ChoiceField(choices=Transaction.Type.choices) type = forms.ChoiceField(choices=Transaction.Type.choices)
reference_date = MonthYearFormField(label=_("Reference Date"), required=False) reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
@@ -624,6 +678,25 @@ class RecurringTransactionForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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 = FormHelper()
self.helper.form_method = "post" self.helper.form_method = "post"
self.helper.form_tag = False self.helper.form_tag = False
@@ -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'),
),
]
@@ -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'),
),
]
+21 -2
View File
@@ -18,6 +18,13 @@ logger = logging.getLogger()
class TransactionCategory(models.Model): class TransactionCategory(models.Model):
name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True) name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True)
mute = models.BooleanField(default=False, verbose_name=_("Mute")) 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: class Meta:
verbose_name = _("Transaction Category") verbose_name = _("Transaction Category")
@@ -30,6 +37,13 @@ class TransactionCategory(models.Model):
class TransactionTag(models.Model): class TransactionTag(models.Model):
name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True) 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: class Meta:
verbose_name = _("Transaction Tags") verbose_name = _("Transaction Tags")
@@ -42,8 +56,13 @@ class TransactionTag(models.Model):
class TransactionEntity(models.Model): class TransactionEntity(models.Model):
name = models.CharField(max_length=255, verbose_name=_("Name")) name = models.CharField(max_length=255, verbose_name=_("Name"))
active = models.BooleanField(
# Add any other fields you might want for entities default=True,
verbose_name=_("Active"),
help_text=_(
"Deactivated entities won't be able to be selected when creating new transactions"
),
)
class Meta: class Meta:
verbose_name = _("Entity") verbose_name = _("Entity")