Compare commits

...

25 Commits
0.1.7 ... 0.2.0

Author SHA1 Message Date
Herculino Trotta
10a0ac42a2 Merge pull request #22
feat(api): add RecurringTransaction and InstallmentPlan endpoints
2025-01-05 11:14:03 -03:00
Herculino Trotta
1b47c12a22 feat(api): add RecurringTransaction and InstallmentPlan endpoints 2025-01-05 11:13:23 -03:00
Herculino Trotta
091f73bf8d feat(api): support string name and ids for installmentplan endpoint 2025-01-05 11:07:38 -03:00
Herculino Trotta
73fe17de64 feat(api): add auth permission to all api endpoint 2025-01-05 11:04:50 -03:00
Herculino Trotta
52af1b2260 Merge pull request #21
feat(api): add API endpoints to add DCA entries and strategies
2025-01-05 10:54:55 -03:00
Herculino Trotta
8efa087aee feat(api): add API endpoints to add DCA entries and strategies 2025-01-05 10:54:31 -03:00
Herculino Trotta
6f69f15474 Merge pull request #20
feat: archived tabs for categories, tags and entities
2025-01-05 01:46:01 -03:00
Herculino Trotta
905e80cffe fix: overflowing empty message 2025-01-05 01:45:11 -03:00
Herculino Trotta
baae6bb96a feat(entities): add tab to show archived entities 2025-01-05 01:43:24 -03:00
Herculino Trotta
f5132e24bd feat(tags): add tab to show archived tags 2025-01-05 01:36:30 -03:00
Herculino Trotta
41303f39a0 fix: typo 2025-01-05 01:35:34 -03:00
Herculino Trotta
0fc8b0ee49 feat(tags): add tab to show archived tags 2025-01-05 01:35:25 -03:00
Herculino Trotta
037014d024 feat(categories): add tab to show archived categories 2025-01-05 01:22:14 -03:00
Herculino Trotta
8c5a9efe05 Merge pull request #19 from eitchtee/dev
locale(pt-BR): update translation
2025-01-04 18:24:47 -03:00
Herculino Trotta
f940414b5c locale(pt-BR): update translation 2025-01-04 18:23:01 -03:00
Herculino Trotta
2d8e97a27e Merge pull request #18
feat: allow for deactivating Tags, Categories and Entities, hiding them from menus
2025-01-04 18:17:42 -03:00
Herculino Trotta
5ccb9ff152 locale: add lazy translations to missing ValidationErrors 2025-01-04 18:17:06 -03:00
Herculino Trotta
3c0a2d82ac feat: allow for deactivating Tags, Categories and Entities, hiding them from menus 2025-01-04 18:13:11 -03:00
Herculino Trotta
62f049cbb2 Merge pull request #17
feat(fields:forms:dynamic-select): support existing objects not currently on the queryset
2025-01-04 18:00:33 -03:00
Herculino Trotta
7a759be357 feat(fields:forms:dynamic-select): support existing objects not currently on the queryset
and add create_field to DynamicModelChoiceField
2025-01-04 17:59:59 -03:00
Herculino Trotta
6297e73307 Merge pull request #16
feat(transactions): properly sum income and expense when selected
2025-01-04 01:33:05 -03:00
Herculino Trotta
eb753bb30e feat(transactions): properly sum income and expense when selected
also added a flatTotal (old behavior) for future use
2025-01-04 01:32:09 -03:00
Herculino Trotta
1047fb23dd fix(networth): charts not changing between views 2025-01-03 17:50:41 -03:00
Herculino Trotta
c861b9ae07 fix(networth): charts not changing between views 2025-01-03 17:36:10 -03:00
Herculino Trotta
4be849f5de github(release): add gha cache 2024-12-28 02:42:43 -03:00
35 changed files with 893 additions and 321 deletions

View File

@@ -29,14 +29,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build and push image
uses: docker/build-push-action@v6
with:
@@ -48,10 +40,5 @@ jobs:
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -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"),

View File

@@ -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

View File

@@ -1,3 +1,4 @@
from .transactions import *
from .accounts import *
from .currencies import *
from .dca import *

View File

@@ -1,4 +1,5 @@
from rest_framework import serializers
from rest_framework.permissions import IsAuthenticated
from apps.api.serializers.currencies import CurrencySerializer
from apps.accounts.models import AccountGroup, Account
@@ -6,6 +7,8 @@ from apps.currencies.models import Currency
class AccountGroupSerializer(serializers.ModelSerializer):
permission_classes = [IsAuthenticated]
class Meta:
model = AccountGroup
fields = "__all__"
@@ -31,6 +34,8 @@ class AccountSerializer(serializers.ModelSerializer):
allow_null=True,
)
permission_classes = [IsAuthenticated]
class Meta:
model = Account
fields = [

View File

@@ -1,8 +1,12 @@
from rest_framework import serializers
from rest_framework.permissions import IsAuthenticated
from apps.currencies.models import Currency, ExchangeRate
class CurrencySerializer(serializers.ModelSerializer):
permission_classes = [IsAuthenticated]
class Meta:
model = Currency
fields = "__all__"
@@ -24,6 +28,8 @@ class ExchangeRateSerializer(serializers.ModelSerializer):
queryset=Currency.objects.all(), source="to_currency", write_only=True
)
permission_classes = [IsAuthenticated]
class Meta:
model = ExchangeRate
fields = "__all__"

View File

@@ -0,0 +1,85 @@
from rest_framework import serializers
from rest_framework.permissions import IsAuthenticated
from apps.dca.models import DCAEntry, DCAStrategy
class DCAEntrySerializer(serializers.ModelSerializer):
profit_loss = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
profit_loss_percentage = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
current_value = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
entry_price = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
permission_classes = [IsAuthenticated]
class Meta:
model = DCAEntry
fields = [
"id",
"strategy",
"date",
"amount_paid",
"amount_received",
"notes",
"created_at",
"updated_at",
"profit_loss",
"profit_loss_percentage",
"current_value",
"entry_price",
]
read_only_fields = ["created_at", "updated_at"]
class DCAStrategySerializer(serializers.ModelSerializer):
entries = DCAEntrySerializer(many=True, read_only=True)
total_invested = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
total_received = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
average_entry_price = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
total_entries = serializers.IntegerField(read_only=True)
current_total_value = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
total_profit_loss = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
total_profit_loss_percentage = serializers.DecimalField(
max_digits=42, decimal_places=30, read_only=True
)
permission_classes = [IsAuthenticated]
class Meta:
model = DCAStrategy
fields = [
"id",
"name",
"target_currency",
"payment_currency",
"notes",
"created_at",
"updated_at",
"entries",
"total_invested",
"total_received",
"average_entry_price",
"total_entries",
"current_total_value",
"total_profit_loss",
"total_profit_loss_percentage",
]
read_only_fields = ["created_at", "updated_at"]

View File

@@ -19,6 +19,7 @@ from apps.transactions.models import (
TransactionTag,
InstallmentPlan,
TransactionEntity,
RecurringTransaction,
)
@@ -47,11 +48,77 @@ class TransactionEntitySerializer(serializers.ModelSerializer):
class InstallmentPlanSerializer(serializers.ModelSerializer):
category = TransactionCategoryField(required=False)
tags = TransactionTagField(required=False)
entities = TransactionEntityField(required=False)
permission_classes = [IsAuthenticated]
class Meta:
model = InstallmentPlan
fields = "__all__"
fields = [
"id",
"account",
"type",
"description",
"number_of_installments",
"installment_start",
"installment_total_number",
"start_date",
"reference_date",
"end_date",
"recurrence",
"installment_amount",
"category",
"tags",
"entities",
"notes",
]
read_only_fields = ["installment_total_number", "end_date"]
def create(self, validated_data):
instance = super().create(validated_data)
instance.create_transactions()
return instance
def update(self, instance, validated_data):
instance = super().update(instance, validated_data)
instance.update_transactions()
return instance
class RecurringTransactionSerializer(serializers.ModelSerializer):
category = TransactionCategoryField(required=False)
tags = TransactionTagField(required=False)
entities = TransactionEntityField(required=False)
class Meta:
model = RecurringTransaction
fields = [
"id",
"is_paused",
"account",
"type",
"amount",
"description",
"category",
"tags",
"entities",
"notes",
"reference_date",
"start_date",
"end_date",
"recurrence_type",
"recurrence_interval",
"last_generated_date",
"last_generated_reference_date",
]
read_only_fields = ["last_generated_date", "last_generated_reference_date"]
def create(self, validated_data):
instance = super().create(validated_data)
instance.create_upcoming_transactions()
return instance
class TransactionSerializer(serializers.ModelSerializer):

View File

@@ -9,10 +9,13 @@ router.register(r"categories", views.TransactionCategoryViewSet)
router.register(r"tags", views.TransactionTagViewSet)
router.register(r"entities", views.TransactionEntityViewSet)
router.register(r"installment-plans", views.InstallmentPlanViewSet)
router.register(r"recurring-transactions", views.RecurringTransactionViewSet)
router.register(r"account-groups", views.AccountGroupViewSet)
router.register(r"accounts", views.AccountViewSet)
router.register(r"currencies", views.CurrencyViewSet)
router.register(r"exchange-rates", views.ExchangeRateViewSet)
router.register(r"dca/strategies", views.DCAStrategyViewSet)
router.register(r"dca/entries", views.DCAEntryViewSet)
urlpatterns = [
path("", include(router.urls)),

View File

@@ -1,3 +1,4 @@
from .transactions import *
from .accounts import *
from .currencies import *
from .dca import *

41
app/apps/api/views/dca.py Normal file
View File

@@ -0,0 +1,41 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from apps.dca.models import DCAStrategy, DCAEntry
from apps.api.serializers import DCAStrategySerializer, DCAEntrySerializer
class DCAStrategyViewSet(viewsets.ModelViewSet):
queryset = DCAStrategy.objects.all()
serializer_class = DCAStrategySerializer
@action(detail=True, methods=["get"])
def investment_frequency(self, request, pk=None):
strategy = self.get_object()
return Response(strategy.investment_frequency_data())
@action(detail=True, methods=["get"])
def price_comparison(self, request, pk=None):
strategy = self.get_object()
return Response(strategy.price_comparison_data())
@action(detail=True, methods=["get"])
def current_price(self, request, pk=None):
strategy = self.get_object()
price_data = strategy.current_price()
if price_data:
price, date = price_data
return Response({"price": price, "date": date})
return Response({"price": None, "date": None})
class DCAEntryViewSet(viewsets.ModelViewSet):
queryset = DCAEntry.objects.all()
serializer_class = DCAEntrySerializer
def get_queryset(self):
queryset = DCAEntry.objects.all()
strategy_id = self.request.query_params.get("strategy", None)
if strategy_id is not None:
queryset = queryset.filter(strategy_id=strategy_id)
return queryset

View File

@@ -1,4 +1,4 @@
from rest_framework import permissions, viewsets
from rest_framework import viewsets
from apps.api.serializers import (
TransactionSerializer,
@@ -6,6 +6,7 @@ from apps.api.serializers import (
TransactionTagSerializer,
InstallmentPlanSerializer,
TransactionEntitySerializer,
RecurringTransactionSerializer,
)
from apps.transactions.models import (
Transaction,
@@ -13,6 +14,7 @@ from apps.transactions.models import (
TransactionTag,
InstallmentPlan,
TransactionEntity,
RecurringTransaction,
)
from apps.rules.signals import transaction_updated, transaction_created
@@ -53,10 +55,7 @@ class InstallmentPlanViewSet(viewsets.ModelViewSet):
queryset = InstallmentPlan.objects.all()
serializer_class = InstallmentPlanSerializer
def perform_create(self, serializer):
instance = serializer.save()
instance.create_transactions()
def perform_update(self, serializer):
instance = serializer.save()
instance.create_transactions()
class RecurringTransactionViewSet(viewsets.ModelViewSet):
queryset = RecurringTransaction.objects.all()
serializer_class = RecurringTransactionSerializer

View File

@@ -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
@@ -8,6 +9,12 @@ from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple
class DynamicModelChoiceField(forms.ModelChoiceField):
def __init__(self, model, *args, **kwargs):
self.model = model
self.to_field_name = kwargs.pop("to_field_name", "pk")
self.create_field = kwargs.pop("create_field", None)
if not self.create_field:
raise ValueError("The 'create_field' parameter is required.")
self.queryset = kwargs.pop("queryset", model.objects.all())
super().__init__(queryset=self.queryset, *args, **kwargs)
self._created_instance = None
@@ -18,8 +25,7 @@ class DynamicModelChoiceField(forms.ModelChoiceField):
if value in self.empty_values:
return None
try:
key = self.to_field_name or "pk"
return self.model.objects.get(**{key: value})
return self.model.objects.get(**{self.to_field_name: value})
except (ValueError, TypeError, self.model.DoesNotExist):
return value # Return the raw value; we'll handle creation in clean()
@@ -49,10 +55,13 @@ class DynamicModelChoiceField(forms.ModelChoiceField):
except self.model.DoesNotExist:
try:
with transaction.atomic():
instance = self.model.objects.create(name=value)
instance, _ = self.model.objects.update_or_create(
**{self.create_field: value}
)
self._created_instance = instance
return instance
except Exception as e:
print(e)
raise ValidationError(
self.error_messages["invalid_choice"], code="invalid_choice"
)
@@ -111,12 +120,12 @@ class DynamicModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""
try:
with transaction.atomic():
new_instance = self.queryset.model(**{self.create_field: value})
new_instance.full_clean()
new_instance.save()
return new_instance
instance, _ = self.model.objects.update_or_create(
**{self.create_field: value}
)
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):
"""
@@ -152,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

View File

@@ -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

View File

@@ -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(
@@ -299,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
@@ -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

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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")

View File

@@ -53,6 +53,8 @@ urlpatterns = [
),
path("tags/", views.tags_index, name="tags_index"),
path("tags/list/", views.tags_list, name="tags_list"),
path("tags/table/active/", views.tags_table_active, name="tags_table_active"),
path("tags/table/archived/", views.tags_table_archived, name="tags_table_archived"),
path("tags/add/", views.tag_add, name="tag_add"),
path(
"tags/<int:tag_id>/edit/",
@@ -66,6 +68,16 @@ urlpatterns = [
),
path("entities/", views.entities_index, name="entities_index"),
path("entities/list/", views.entities_list, name="entities_list"),
path(
"entities/table/active/",
views.entities_table_active,
name="entities_table_active",
),
path(
"entities/table/archived/",
views.entities_table_archived,
name="entities_table_archived",
),
path("entities/add/", views.entity_add, name="entity_add"),
path(
"entities/<int:entity_id>/edit/",
@@ -79,6 +91,16 @@ urlpatterns = [
),
path("categories/", views.categories_index, name="categories_index"),
path("categories/list/", views.categories_list, name="categories_list"),
path(
"categories/table/active/",
views.categories_table_active,
name="categories_table_active",
),
path(
"categories/table/archived/",
views.categories_table_archived,
name="categories_table_archived",
),
path("categories/add/", views.category_add, name="category_add"),
path(
"categories/<int:category_id>/edit/",

View File

@@ -25,11 +25,33 @@ def categories_index(request):
@login_required
@require_http_methods(["GET"])
def categories_list(request):
categories = TransactionCategory.objects.all().order_by("id")
return render(
request,
"categories/fragments/list.html",
{"categories": categories},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def categories_table_active(request):
categories = TransactionCategory.objects.filter(active=True).order_by("id")
return render(
request,
"categories/fragments/table.html",
{"categories": categories, "active": True},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def categories_table_archived(request):
categories = TransactionCategory.objects.filter(active=False).order_by("id")
return render(
request,
"categories/fragments/table.html",
{"categories": categories, "active": False},
)

View File

@@ -24,11 +24,33 @@ def entities_index(request):
@login_required
@require_http_methods(["GET"])
def entities_list(request):
entities = TransactionEntity.objects.all().order_by("id")
return render(
request,
"entities/fragments/list.html",
{"entities": entities},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def entities_table_active(request):
entities = TransactionEntity.objects.filter(active=True).order_by("id")
return render(
request,
"entities/fragments/table.html",
{"entities": entities, "active": True},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def entities_table_archived(request):
entities = TransactionEntity.objects.filter(active=False).order_by("id")
return render(
request,
"entities/fragments/table.html",
{"entities": entities, "active": False},
)

View File

@@ -24,11 +24,33 @@ def tags_index(request):
@login_required
@require_http_methods(["GET"])
def tags_list(request):
tags = TransactionTag.objects.all().order_by("id")
return render(
request,
"tags/fragments/list.html",
{"tags": tags},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def tags_table_active(request):
tags = TransactionTag.objects.filter(active=True).order_by("id")
return render(
request,
"tags/fragments/table.html",
{"tags": tags, "active": True},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def tags_table_archived(request):
tags = TransactionTag.objects.filter(active=False).order_by("id")
return render(
request,
"tags/fragments/table.html",
{"tags": tags, "active": False},
)

View File

@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-28 05:32+0000\n"
"PO-Revision-Date: 2024-12-28 02:32-0300\n"
"POT-Creation-Date: 2025-01-04 21:19+0000\n"
"PO-Revision-Date: 2025-01-04 18:22-0300\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: pt_BR\n"
@@ -23,22 +23,22 @@ msgstr ""
msgid "Group name"
msgstr "Nome do grupo"
#: apps/accounts/forms.py:40 apps/accounts/forms.py:95
#: apps/accounts/forms.py:40 apps/accounts/forms.py:96
#: apps/currencies/forms.py:51 apps/currencies/forms.py:90 apps/dca/forms.py:40
#: apps/dca/forms.py:93 apps/rules/forms.py:45 apps/rules/forms.py:87
#: apps/transactions/forms.py:123 apps/transactions/forms.py:445
#: apps/transactions/forms.py:488 apps/transactions/forms.py:520
#: apps/transactions/forms.py:555 apps/transactions/forms.py:668
#: apps/transactions/forms.py:145 apps/transactions/forms.py:495
#: apps/transactions/forms.py:538 apps/transactions/forms.py:570
#: apps/transactions/forms.py:605 apps/transactions/forms.py:741
msgid "Update"
msgstr "Atualizar"
#: apps/accounts/forms.py:48 apps/accounts/forms.py:103
#: apps/accounts/forms.py:48 apps/accounts/forms.py:104
#: apps/common/widgets/tom_select.py:12 apps/currencies/forms.py:59
#: apps/currencies/forms.py:98 apps/dca/forms.py:48 apps/dca/forms.py:102
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/transactions/forms.py:132
#: apps/transactions/forms.py:453 apps/transactions/forms.py:496
#: apps/transactions/forms.py:528 apps/transactions/forms.py:563
#: apps/transactions/forms.py:676
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/transactions/forms.py:154
#: apps/transactions/forms.py:503 apps/transactions/forms.py:546
#: apps/transactions/forms.py:578 apps/transactions/forms.py:613
#: apps/transactions/forms.py:749
#: templates/account_groups/fragments/list.html:9
#: templates/accounts/fragments/list.html:9
#: templates/categories/fragments/list.html:9
@@ -54,35 +54,35 @@ msgstr "Atualizar"
msgid "Add"
msgstr "Adicionar"
#: apps/accounts/forms.py:56 templates/accounts/fragments/list.html:26
#: apps/accounts/forms.py:57 templates/accounts/fragments/list.html:26
msgid "Group"
msgstr "Grupo da Conta"
#: apps/accounts/forms.py:112
#: apps/accounts/forms.py:113
msgid "New balance"
msgstr "Novo saldo"
#: apps/accounts/forms.py:117 apps/rules/models.py:27
#: apps/transactions/forms.py:37 apps/transactions/forms.py:186
#: apps/transactions/forms.py:191 apps/transactions/forms.py:365
#: apps/transactions/forms.py:585 apps/transactions/models.py:90
#: apps/transactions/models.py:209 apps/transactions/models.py:384
#: apps/accounts/forms.py:119 apps/rules/models.py:27
#: apps/transactions/forms.py:39 apps/transactions/forms.py:209
#: apps/transactions/forms.py:216 apps/transactions/forms.py:395
#: apps/transactions/forms.py:637 apps/transactions/models.py:109
#: apps/transactions/models.py:228 apps/transactions/models.py:403
msgid "Category"
msgstr "Categoria"
#: apps/accounts/forms.py:124 apps/rules/models.py:28
#: apps/transactions/filters.py:73 apps/transactions/forms.py:44
#: apps/transactions/forms.py:199 apps/transactions/forms.py:206
#: apps/transactions/forms.py:360 apps/transactions/forms.py:580
#: apps/transactions/models.py:96 apps/transactions/models.py:211
#: apps/transactions/models.py:388 templates/includes/navbar.html:98
#: apps/accounts/forms.py:126 apps/rules/models.py:28
#: apps/transactions/filters.py:73 apps/transactions/forms.py:47
#: apps/transactions/forms.py:225 apps/transactions/forms.py:233
#: apps/transactions/forms.py:388 apps/transactions/forms.py:630
#: apps/transactions/models.py:115 apps/transactions/models.py:230
#: apps/transactions/models.py:407 templates/includes/navbar.html:98
#: templates/tags/fragments/list.html:5 templates/tags/pages/index.html:4
msgid "Tags"
msgstr "Tags"
#: apps/accounts/models.py:9 apps/accounts/models.py:21 apps/dca/models.py:14
#: apps/rules/models.py:9 apps/transactions/models.py:19
#: apps/transactions/models.py:32 apps/transactions/models.py:44
#: apps/transactions/models.py:39 apps/transactions/models.py:58
#: templates/account_groups/fragments/list.html:25
#: templates/accounts/fragments/list.html:25
#: templates/categories/fragments/list.html:25
@@ -139,9 +139,9 @@ msgstr ""
"Contas arquivadas não aparecem nem contam para o seu patrimônio líquido"
#: apps/accounts/models.py:59 apps/rules/models.py:19
#: apps/transactions/forms.py:55 apps/transactions/forms.py:352
#: apps/transactions/forms.py:572 apps/transactions/models.py:65
#: apps/transactions/models.py:169 apps/transactions/models.py:366
#: apps/transactions/forms.py:59 apps/transactions/forms.py:380
#: apps/transactions/forms.py:622 apps/transactions/models.py:84
#: apps/transactions/models.py:188 apps/transactions/models.py:385
msgid "Account"
msgstr "Conta"
@@ -189,16 +189,45 @@ msgstr "Reconciliação do saldo"
msgid "Account balances have been reconciled successfully"
msgstr "Os saldos das contas foram conciliados com sucesso"
#: apps/api/fields/transactions.py:29
msgid "Category with this ID does not exist."
msgstr "Categoria com esse ID não existe."
#: apps/api/fields/transactions.py:35
msgid "Invalid category data. Provide an ID or name."
msgstr "Dados da categoria inválidos. Forneça um ID ou nome."
#: apps/api/fields/transactions.py:65
msgid "Tag with this ID does not exist."
msgstr "Tag com esse ID não existe."
#: apps/api/fields/transactions.py:71
msgid "Invalid tag data. Provide an ID or name."
msgstr "Dados da tag inválidos. Forneça um ID ou nome."
#: apps/api/fields/transactions.py:89
msgid "Entity with this ID does not exist."
msgstr "Entidade com esse ID não existe."
#: apps/api/fields/transactions.py:95
msgid "Invalid entity data. Provide an ID or name."
msgstr "Dados da entidade inválidos. Forneça um ID ou nome."
#: apps/api/serializers/transactions.py:96
msgid "Either 'date' or 'reference_date' must be provided."
msgstr "É necessário fornecer “date” ou “reference_date”."
#: apps/common/fields/forms/dynamic_select.py:128
#: apps/common/fields/forms/dynamic_select.py:164
msgid "Error creating new instance"
msgstr "Erro criando nova instância"
#: apps/common/fields/forms/grouped_select.py:24
#: apps/common/widgets/tom_select.py:91 apps/common/widgets/tom_select.py:94
msgid "Ungrouped"
msgstr "Não agrupado"
#: apps/common/fields/month_year.py:45
#: apps/common/fields/month_year.py:21 apps/common/fields/month_year.py:45
msgid "Invalid date format. Use YYYY-MM."
msgstr "Formato de data inválido. Use AAAA-MM."
@@ -386,8 +415,8 @@ msgid "Payment Currency"
msgstr "Moeda de pagamento"
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/models.py:26
#: apps/transactions/forms.py:222 apps/transactions/models.py:86
#: apps/transactions/models.py:218 apps/transactions/models.py:394
#: apps/transactions/forms.py:250 apps/transactions/models.py:105
#: apps/transactions/models.py:237 apps/transactions/models.py:413
msgid "Notes"
msgstr "Notas"
@@ -404,7 +433,7 @@ msgid "Strategy"
msgstr "Estratégia"
#: apps/dca/models.py:156 apps/rules/models.py:22
#: apps/transactions/forms.py:210 apps/transactions/models.py:75
#: apps/transactions/forms.py:238 apps/transactions/models.py:94
#: templates/dca/fragments/strategy/details.html:52
#: templates/exchange_rates/fragments/table.html:10
msgid "Date"
@@ -483,8 +512,8 @@ msgid "A value for this field already exists in the rule."
msgstr "Já existe um valor para esse campo na regra."
#: apps/rules/models.py:10 apps/rules/models.py:25
#: apps/transactions/forms.py:214 apps/transactions/models.py:85
#: apps/transactions/models.py:176 apps/transactions/models.py:380
#: apps/transactions/forms.py:242 apps/transactions/models.py:104
#: apps/transactions/models.py:195 apps/transactions/models.py:399
msgid "Description"
msgstr "Descrição"
@@ -492,33 +521,33 @@ msgstr "Descrição"
msgid "Trigger"
msgstr "Gatilho"
#: apps/rules/models.py:20 apps/transactions/models.py:72
#: apps/transactions/models.py:174 apps/transactions/models.py:372
#: apps/rules/models.py:20 apps/transactions/models.py:91
#: apps/transactions/models.py:193 apps/transactions/models.py:391
msgid "Type"
msgstr "Tipo"
#: apps/rules/models.py:21 apps/transactions/filters.py:22
#: apps/transactions/models.py:74
#: apps/transactions/models.py:93
msgid "Paid"
msgstr "Pago"
#: apps/rules/models.py:23 apps/transactions/forms.py:58
#: apps/transactions/forms.py:213 apps/transactions/forms.py:375
#: apps/transactions/forms.py:595 apps/transactions/models.py:76
#: apps/transactions/models.py:192 apps/transactions/models.py:396
#: apps/rules/models.py:23 apps/transactions/forms.py:62
#: apps/transactions/forms.py:241 apps/transactions/forms.py:407
#: apps/transactions/forms.py:649 apps/transactions/models.py:95
#: apps/transactions/models.py:211 apps/transactions/models.py:415
msgid "Reference Date"
msgstr "Data de Referência"
#: apps/rules/models.py:24 apps/transactions/models.py:81
#: apps/transactions/models.py:377
#: apps/rules/models.py:24 apps/transactions/models.py:100
#: apps/transactions/models.py:396
msgid "Amount"
msgstr "Quantia"
#: apps/rules/models.py:29 apps/transactions/filters.py:80
#: apps/transactions/forms.py:51 apps/transactions/forms.py:372
#: apps/transactions/forms.py:592 apps/transactions/models.py:50
#: apps/transactions/models.py:101 apps/transactions/models.py:214
#: apps/transactions/models.py:391 templates/entities/fragments/list.html:5
#: apps/transactions/forms.py:55 apps/transactions/forms.py:403
#: apps/transactions/forms.py:645 apps/transactions/models.py:69
#: apps/transactions/models.py:120 apps/transactions/models.py:233
#: apps/transactions/models.py:410 templates/entities/fragments/list.html:5
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:100
msgid "Entities"
msgstr "Entidades"
@@ -600,23 +629,23 @@ msgstr "Quantia miníma"
msgid "Amount max"
msgstr "Quantia máxima"
#: apps/transactions/forms.py:162
#: apps/transactions/forms.py:184
msgid "From Account"
msgstr "Conta de origem"
#: apps/transactions/forms.py:167
#: apps/transactions/forms.py:189
msgid "To Account"
msgstr "Conta de destino"
#: apps/transactions/forms.py:174
#: apps/transactions/forms.py:196
msgid "From Amount"
msgstr "Quantia de origem"
#: apps/transactions/forms.py:179
#: apps/transactions/forms.py:201
msgid "To Amount"
msgstr "Quantia de destino"
#: apps/transactions/forms.py:287
#: apps/transactions/forms.py:315
#: templates/calendar_view/pages/calendar.html:84
#: templates/monthly_overview/pages/overview.html:84
#: templates/yearly_overview/pages/overview_by_account.html:79
@@ -624,23 +653,27 @@ msgstr "Quantia de destino"
msgid "Transfer"
msgstr "Transferir"
#: apps/transactions/forms.py:474
#: apps/transactions/forms.py:330
msgid "From and To accounts must be different."
msgstr "As contas De e Para devem ser diferentes."
#: apps/transactions/forms.py:524
msgid "Tag name"
msgstr "Nome da Tag"
#: apps/transactions/forms.py:506
#: apps/transactions/forms.py:556
msgid "Entity name"
msgstr "Nome da entidade"
#: apps/transactions/forms.py:538
#: apps/transactions/forms.py:588
msgid "Category name"
msgstr "Nome da Categoria"
#: apps/transactions/forms.py:540
#: apps/transactions/forms.py:590
msgid "Muted categories won't count towards your monthly total"
msgstr "As categorias silenciadas não serão contabilizadas em seu total mensal"
#: apps/transactions/forms.py:687
#: apps/transactions/forms.py:760
msgid "End date should be after the start date"
msgstr "Data final deve ser após data inicial"
@@ -648,23 +681,51 @@ msgstr "Data final deve ser após data inicial"
msgid "Mute"
msgstr "Silenciada"
#: apps/transactions/models.py:23
#: apps/transactions/models.py:23 apps/transactions/models.py:42
#: apps/transactions/models.py:61
#: templates/recurring_transactions/fragments/list.html:21
msgid "Active"
msgstr "Ativo"
#: apps/transactions/models.py:25
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
msgstr ""
"As categorias desativadas não poderão ser selecionadas ao criar novas "
"transações"
#: apps/transactions/models.py:30
msgid "Transaction Category"
msgstr "Categoria da Transação"
#: apps/transactions/models.py:24
#: apps/transactions/models.py:31
msgid "Transaction Categories"
msgstr "Categorias da Trasanção"
#: apps/transactions/models.py:35 apps/transactions/models.py:36
#: apps/transactions/models.py:44
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
"As tags desativadas não poderão ser selecionadas ao criar novas transações"
#: apps/transactions/models.py:49 apps/transactions/models.py:50
msgid "Transaction Tags"
msgstr "Tags da Transação"
#: apps/transactions/models.py:49
#: apps/transactions/models.py:63
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
msgstr ""
"As entidades desativadas não poderão ser selecionadas ao criar novas "
"transações"
#: apps/transactions/models.py:68
msgid "Entity"
msgstr "Entidade"
#: apps/transactions/models.py:59
#: apps/transactions/models.py:78
#: templates/calendar_view/pages/calendar.html:54
#: templates/monthly_overview/fragments/monthly_summary.html:39
#: templates/monthly_overview/pages/overview.html:54
@@ -674,7 +735,7 @@ msgstr "Entidade"
msgid "Income"
msgstr "Renda"
#: apps/transactions/models.py:60
#: apps/transactions/models.py:79
#: templates/calendar_view/pages/calendar.html:62
#: templates/monthly_overview/pages/overview.html:62
#: templates/yearly_overview/pages/overview_by_account.html:57
@@ -682,19 +743,19 @@ msgstr "Renda"
msgid "Expense"
msgstr "Despesa"
#: apps/transactions/models.py:112 apps/transactions/models.py:221
#: apps/transactions/models.py:131 apps/transactions/models.py:240
msgid "Installment Plan"
msgstr "Parcelamento"
#: apps/transactions/models.py:121 apps/transactions/models.py:417
#: apps/transactions/models.py:140 apps/transactions/models.py:436
msgid "Recurring Transaction"
msgstr "Transação Recorrente"
#: apps/transactions/models.py:125
#: apps/transactions/models.py:144
msgid "Transaction"
msgstr "Transação"
#: apps/transactions/models.py:126 templates/includes/navbar.html:53
#: apps/transactions/models.py:145 templates/includes/navbar.html:53
#: templates/includes/navbar.html:94
#: templates/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
@@ -702,95 +763,95 @@ msgstr "Transação"
msgid "Transactions"
msgstr "Transações"
#: apps/transactions/models.py:163
#: apps/transactions/models.py:182
msgid "Yearly"
msgstr "Anual"
#: apps/transactions/models.py:164 apps/users/models.py:26
#: apps/transactions/models.py:183 apps/users/models.py:26
#: templates/includes/navbar.html:25
msgid "Monthly"
msgstr "Mensal"
#: apps/transactions/models.py:165
#: apps/transactions/models.py:184
msgid "Weekly"
msgstr "Semanal"
#: apps/transactions/models.py:166
#: apps/transactions/models.py:185
msgid "Daily"
msgstr "Diária"
#: apps/transactions/models.py:179
#: apps/transactions/models.py:198
msgid "Number of Installments"
msgstr "Número de Parcelas"
#: apps/transactions/models.py:184
#: apps/transactions/models.py:203
msgid "Installment Start"
msgstr "Parcela inicial"
#: apps/transactions/models.py:185
#: apps/transactions/models.py:204
msgid "The installment number to start counting from"
msgstr "O número da parcela a partir do qual se inicia a contagem"
#: apps/transactions/models.py:190 apps/transactions/models.py:400
#: apps/transactions/models.py:209 apps/transactions/models.py:419
msgid "Start Date"
msgstr "Data de Início"
#: apps/transactions/models.py:194 apps/transactions/models.py:401
#: apps/transactions/models.py:213 apps/transactions/models.py:420
msgid "End Date"
msgstr "Data Final"
#: apps/transactions/models.py:199
#: apps/transactions/models.py:218
msgid "Recurrence"
msgstr "Recorrência"
#: apps/transactions/models.py:202
#: apps/transactions/models.py:221
msgid "Installment Amount"
msgstr "Valor da Parcela"
#: apps/transactions/models.py:222 templates/includes/navbar.html:62
#: apps/transactions/models.py:241 templates/includes/navbar.html:62
#: templates/installment_plans/fragments/list.html:5
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
msgstr "Parcelamentos"
#: apps/transactions/models.py:359
#: apps/transactions/models.py:378
msgid "day(s)"
msgstr "dia(s)"
#: apps/transactions/models.py:360
#: apps/transactions/models.py:379
msgid "week(s)"
msgstr "semana(s)"
#: apps/transactions/models.py:361
#: apps/transactions/models.py:380
msgid "month(s)"
msgstr "mês(es)"
#: apps/transactions/models.py:362
#: apps/transactions/models.py:381
msgid "year(s)"
msgstr "ano(s)"
#: apps/transactions/models.py:364
#: apps/transactions/models.py:383
#: templates/recurring_transactions/fragments/list.html:24
msgid "Paused"
msgstr "Pausado"
#: apps/transactions/models.py:403
#: apps/transactions/models.py:422
msgid "Recurrence Type"
msgstr "Tipo de recorrência"
#: apps/transactions/models.py:406
#: apps/transactions/models.py:425
msgid "Recurrence Interval"
msgstr "Intervalo de recorrência"
#: apps/transactions/models.py:410
#: apps/transactions/models.py:429
msgid "Last Generated Date"
msgstr "Última data gerada"
#: apps/transactions/models.py:413
#: apps/transactions/models.py:432
msgid "Last Generated Reference Date"
msgstr "Última data de referência gerada"
#: apps/transactions/models.py:418 templates/includes/navbar.html:64
#: apps/transactions/models.py:437 templates/includes/navbar.html:64
#: templates/recurring_transactions/fragments/list.html:5
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
@@ -1276,7 +1337,7 @@ msgstr "Marcar como não pago"
msgid "Yes, delete them!"
msgstr "Sim, apague!"
#: templates/cotton/ui/transactions_action_bar.html:73
#: templates/cotton/ui/transactions_action_bar.html:81
msgid "copied!"
msgstr "copiado!"
@@ -1694,11 +1755,11 @@ msgstr "Por moeda"
msgid "By account"
msgstr "Por conta"
#: templates/net_worth/net_worth.html:166
#: templates/net_worth/net_worth.html:172
msgid "Evolution by currency"
msgstr "Evolução por moeda"
#: templates/net_worth/net_worth.html:224
#: templates/net_worth/net_worth.html:236
msgid "Evolution by account"
msgstr "Evolução por conta"
@@ -1710,10 +1771,6 @@ msgstr "Adicionar transação recorrente"
msgid "Edit recurring transaction"
msgstr "Editar transação recorrente"
#: templates/recurring_transactions/fragments/list.html:21
msgid "Active"
msgstr "Ativo"
#: templates/recurring_transactions/fragments/table.html:47
msgid "Unpause"
msgstr "Despausar"

View File

@@ -15,53 +15,18 @@
</div>
<div class="card">
<div class="card-body table-responsive">
{% if categories %}
<c-config.search></c-config.search>
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
<th scope="col" class="col">{% translate 'Muted' %}</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr class="category">
<td class="col-auto text-center">
<div class="btn-group" role="group" aria-label="{% translate 'Actions' %}">
<a class="btn btn-secondary btn-sm"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'category_edit' category_id=category.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm text-danger"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'category_delete' category_id=category.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
</td>
<td class="col">{{ category.name }}</td>
<td class="col">
{% if category.mute %}<i class="fa-solid fa-check text-success"></i>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<c-msg.empty title="{% translate "No categories" %}" remove-padding></c-msg.empty>
{% endif %}
<div class="card-header">
<ul class="nav nav-pills card-header-pills" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" type="button" role="tab" aria-selected="true" hx-get="{% url 'categories_table_active' %}" hx-trigger="load, click" hx-target="#categories-table">{% translate 'Active' %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" hx-get="{% url 'categories_table_archived' %}" hx-target="#categories-table" data-bs-toggle="tab" type="button" role="tab" aria-selected="false">{% translate 'Archived' %}</button>
</li>
</ul>
</div>
<div class="card-body">
<div id="categories-table"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,59 @@
{% load i18n %}
{% if active %}
<div class="show-loading" hx-get="{% url 'categories_table_active' %}" hx-trigger="updated from:window"
hx-swap="outerHTML">
{% else %}
<div class="show-loading" hx-get="{% url 'categories_table_archived' %}" hx-trigger="updated from:window"
hx-swap="outerHTML">
{% endif %}
{% if categories %}
<div class="table-responsive">
<c-config.search></c-config.search>
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
<th scope="col" class="col">{% translate 'Muted' %}</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr class="category">
<td class="col-auto text-center">
<div class="btn-group" role="group" aria-label="{% translate 'Actions' %}">
<a class="btn btn-secondary btn-sm"
role="button"
data-bs-toggle="tooltip"
hx-swap="innerHTML"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'category_edit' category_id=category.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm text-danger"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'category_delete' category_id=category.id %}"
hx-trigger='confirmed'
hx-swap="innerHTML"
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
</td>
<td class="col">{{ category.name }}</td>
<td class="col">
{% if category.mute %}<i class="fa-solid fa-check text-success"></i>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<c-msg.empty title="{% translate "No categories" %}" remove-padding></c-msg.empty>
{% endif %}
</div>

View File

@@ -4,5 +4,5 @@
{% block title %}{% translate 'Categories' %}{% endblock %}
{% block content %}
<div hx-get="{% url 'categories_list' %}" hx-trigger="load, updated from:window" class="show-loading"></div>
<div hx-get="{% url 'categories_list' %}" hx-trigger="load" class="show-loading"></div>
{% endblock %}

View File

@@ -1,5 +1,5 @@
{% load i18n %}
<div class="transaction d-flex my-1">
<div class="transaction d-flex my-1 {% if transaction.type == "EX" %}expense{% else %}income{% endif %}">
{% if not disable_selection %}
<label class="px-3 d-flex align-items-center justify-content-center">
<input class="form-check-input" type="checkbox" name="transactions" value="{{ transaction.id }}" id="check-{{ transaction.id }}" aria-label="{% translate 'Select' %}" hx-preserve>

View File

@@ -57,15 +57,23 @@
</button>
<div class="vr mx-3 tw-align-middle"></div>
<span _="on selected_transactions_updated from #actions-bar
set total to 0.0
for amt in <.transaction:has(input[name='transactions']:checked) .main-amount .amount/>
set realTotal to 0.0
set flatTotal to 0.0
for transaction in <.transaction:has(input[name='transactions']:checked)/>
set amt to first <.main-amount .amount/> in transaction
set amountValue to parseFloat(amt.getAttribute('data-amount'))
if not isNaN(amountValue)
set total to total + (amountValue * 100)
set flatTotal to flatTotal + (amountValue * 100)
if transaction match .income
set realTotal to realTotal + (amountValue * 100)
else
set realTotal to realTotal - (amountValue * 100)
end
end
end
set total to total / 100
put total.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into me
set realTotal to realTotal / 100
put realTotal.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into me
end
on click
set original_value to my innerText

View File

@@ -15,49 +15,18 @@
</div>
<div class="card">
<div class="card-body table-responsive">
{% if entities %}
<c-config.search></c-config.search>
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
</tr>
</thead>
<tbody>
{% for entity in entities %}
<tr class="entity">
<td class="col-auto">
<div class="btn-group" role="group" aria-label="{% translate 'Actions' %}">
<a class="btn btn-secondary btn-sm"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'entity_edit' entity_id=entity.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm text-danger"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'entity_delete' entity_id=entity.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
</td>
<td class="col">{{ entity.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<c-msg.empty title="{% translate "No entities" %}" remove-padding></c-msg.empty>
{% endif %}
<div class="card-header">
<ul class="nav nav-pills card-header-pills" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" type="button" role="tab" aria-selected="true" hx-get="{% url 'entities_table_active' %}" hx-trigger="load, click" hx-target="#entities-table">{% translate 'Active' %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" hx-get="{% url 'entities_table_archived' %}" hx-target="#entities-table" data-bs-toggle="tab" type="button" role="tab" aria-selected="false">{% translate 'Archived' %}</button>
</li>
</ul>
</div>
<div class="card-body">
<div id="entities-table"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,55 @@
{% load i18n %}
{% if active %}
<div class="show-loading" hx-get="{% url 'entities_table_active' %}" hx-trigger="updated from:window"
hx-swap="outerHTML">
{% else %}
<div class="show-loading" hx-get="{% url 'entities_table_archived' %}" hx-trigger="updated from:window"
hx-swap="outerHTML">
{% endif %}
{% if entities %}
<div class="table-responsive">
<c-config.search></c-config.search>
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
</tr>
</thead>
<tbody>
{% for entity in entities %}
<tr class="entity">
<td class="col-auto">
<div class="btn-group" role="group" aria-label="{% translate 'Actions' %}">
<a class="btn btn-secondary btn-sm"
role="button"
hx-swap="innerHTML"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'entity_edit' entity_id=entity.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm text-danger"
role="button"
hx-swap="innerHTML"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'entity_delete' entity_id=entity.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
</td>
<td class="col">{{ entity.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<c-msg.empty title="{% translate "No entities" %}" remove-padding></c-msg.empty>
{% endif %}
</div>

View File

@@ -4,5 +4,5 @@
{% block title %}{% translate 'Entities' %}{% endblock %}
{% block content %}
<div hx-get="{% url 'entities_list' %}" hx-trigger="load, updated from:window" class="show-loading"></div>
<div hx-get="{% url 'entities_list' %}" hx-trigger="load" class="show-loading"></div>
{% endblock %}

View File

@@ -9,7 +9,7 @@
{% block title %}{% if type == "current" %}{% translate 'Current Net Worth' %}{% else %}{% translate 'Projected Net Worth' %}{% endif %}{% endblock %}
{% block content %}
<div class="container px-md-3 py-3">
<div class="container px-md-3 py-3" _="on load call initializeAccountChart() then initializeCurrencyChart()">
<div class="row gx-xl-4 gy-3 mb-4">
<div class="col-12 col-xl-5">
<div class="row row-cols-1 g-4">
@@ -52,7 +52,7 @@
</div>
</div>
<div class="col-12 col-xl-7">
<div class="chart-container position-relativo tw-min-h-[40vh] tw-h-full">
<div class="chart-container position-relative tw-min-h-[40vh] tw-h-full">
<canvas id="currencyBalanceChart"></canvas>
</div>
</div>
@@ -136,7 +136,7 @@
</div>
</div>
<div class="col-12 col-xl-7">
<div class="chart-container position-relativo tw-min-h-[40vh] tw-h-full">
<div class="chart-container position-relative tw-min-h-[40vh] tw-h-full">
<canvas id="accountBalanceChart"></canvas>
</div>
</div>
@@ -144,13 +144,19 @@
</div>
<script>
document.body.addEventListener('htmx:load', function (evt) {
var currencyChart;
function initializeCurrencyChart() {
// Destroy existing chart if it exists
if (currencyChart) {
currencyChart.destroy();
}
var chartData = JSON.parse('{{ chart_data_currency_json|safe }}');
var currencies = {{ currencies|safe }};
var ctx = document.getElementById('currencyBalanceChart').getContext('2d');
new Chart(ctx, {
currencyChart = new Chart(ctx, {
type: 'line',
data: chartData,
options: {
@@ -197,17 +203,23 @@
}
}
});
});
}
</script>
<script>
document.body.addEventListener('htmx:load', function (evt) {
<script id="accountBalanceChartScript">
var accountChart;
function initializeAccountChart() {
// Destroy existing chart if it exists
if (accountChart) {
accountChart.destroy();
}
var chartData = JSON.parse('{{ chart_data_accounts_json|safe }}');
var accounts = {{ accounts|safe }};
var ctx = document.getElementById('accountBalanceChart').getContext('2d');
new Chart(ctx, {
accountChart = new Chart(ctx, {
type: 'line',
data: chartData,
options: {
@@ -256,43 +268,38 @@
}
}
});
});
}
</script>
<script type="text/hyperscript">
def showOnlyAccountDataset(datasetName)
set chart to Chart.getChart('accountBalanceChart')
for dataset in chart.data.datasets
for dataset in accountChart.data.datasets
set isMatch to dataset.label is datasetName
call chart.setDatasetVisibility(chart.data.datasets.indexOf(dataset), isMatch)
call accountChart.setDatasetVisibility(accountChart.data.datasets.indexOf(dataset), isMatch)
end
call chart.update()
call accountChart.update()
end
def showOnlyCurrencyDataset(datasetName)
set chart to Chart.getChart('currencyBalanceChart')
for dataset in chart.data.datasets
for dataset in currencyChart.data.datasets
set isMatch to dataset.label is datasetName
call chart.setDatasetVisibility(chart.data.datasets.indexOf(dataset), isMatch)
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch)
end
call chart.update()
call currencyChart.update()
end
def showAllDatasetsAccount()
set chart to Chart.getChart('accountBalanceChart')
for dataset in chart.data.datasets
call chart.setDatasetVisibility(chart.data.datasets.indexOf(dataset), true)
for dataset in accountChart.data.datasets
call accountChart.setDatasetVisibility(accountChart.data.datasets.indexOf(dataset), true)
end
call chart.update()
call accountChart.update()
end
def showAllDatasetsCurrency()
set chart to Chart.getChart('currencyBalanceChart')
for dataset in chart.data.datasets
call chart.setDatasetVisibility(chart.data.datasets.indexOf(dataset), true)
for dataset in currencyChart.data.datasets
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), true)
end
call chart.update()
call currencyChart.update()
end
</script>

View File

@@ -15,49 +15,18 @@
</div>
<div class="card">
<div class="card-body table-responsive">
{% if tags %}
<c-config.search></c-config.search>
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
</tr>
</thead>
<tbody>
{% for tag in tags %}
<tr class="tag">
<td class="col-auto">
<div class="btn-group" role="group" aria-label="{% translate 'Actions' %}">
<a class="btn btn-secondary btn-sm"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'tag_edit' tag_id=tag.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm text-danger"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'tag_delete' tag_id=tag.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
</td>
<td class="col">{{ tag.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<c-msg.empty title="{% translate "No tags" %}" remove-padding></c-msg.empty>
{% endif %}
<div class="card-header">
<ul class="nav nav-pills card-header-pills" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" type="button" role="tab" aria-selected="true" hx-get="{% url 'tags_table_active' %}" hx-trigger="load, click" hx-target="#tags-table">{% translate 'Active' %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" hx-get="{% url 'tags_table_archived' %}" hx-target="#tags-table" data-bs-toggle="tab" type="button" role="tab" aria-selected="false">{% translate 'Archived' %}</button>
</li>
</ul>
</div>
<div class="card-body">
<div id="tags-table"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,55 @@
{% load i18n %}
{% if active %}
<div class="show-loading" hx-get="{% url 'tags_table_active' %}" hx-trigger="updated from:window"
hx-swap="outerHTML">
{% else %}
<div class="show-loading" hx-get="{% url 'tags_table_archived' %}" hx-trigger="updated from:window"
hx-swap="outerHTML">
{% endif %}
{% if tags %}
<div class="table-responsive">
<c-config.search></c-config.search>
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
</tr>
</thead>
<tbody>
{% for tag in tags %}
<tr class="tag">
<td class="col-auto">
<div class="btn-group" role="group" aria-label="{% translate 'Actions' %}">
<a class="btn btn-secondary btn-sm"
role="button"
hx-swap="innerHTML"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'tag_edit' tag_id=tag.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm text-danger"
role="button"
data-bs-toggle="tooltip"
hx-swap="innerHTML"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'tag_delete' tag_id=tag.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
</td>
<td class="col">{{ tag.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<c-msg.empty title="{% translate "No tags" %}" remove-padding></c-msg.empty>
{% endif %}
</div>

View File

@@ -4,5 +4,5 @@
{% block title %}{% translate 'Tags' %}{% endblock %}
{% block content %}
<div hx-get="{% url 'tags_list' %}" hx-trigger="load, updated from:window" class="show-loading"></div>
<div hx-get="{% url 'tags_list' %}" hx-trigger="load" class="show-loading"></div>
{% endblock %}