From 2382abf3c059604200b9fc0318a5ad1c9292a571 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Sat, 30 Nov 2024 17:12:35 -0300 Subject: [PATCH] feat: add Transaction Entity --- app/apps/api/fields/transactions.py | 30 ++++- app/apps/api/serializers/transactions.py | 24 +++- app/apps/api/urls.py | 1 + app/apps/api/views/transactions.py | 7 ++ .../0004_alter_transactionruleaction_field.py | 18 +++ app/apps/rules/models.py | 1 + app/apps/rules/tasks.py | 36 +++++- app/apps/transactions/admin.py | 2 + app/apps/transactions/filters.py | 17 ++- app/apps/transactions/forms.py | 74 +++++++++++- ..._transactionentity_transaction_entities.py | 30 +++++ .../0024_installmentplan_entities_and_more.py | 28 +++++ app/apps/transactions/models.py | 42 ++++++- app/apps/transactions/urls.py | 13 +++ app/apps/transactions/views/__init__.py | 1 + app/apps/transactions/views/entities.py | 105 ++++++++++++++++++ app/templates/cotton/transaction/item.html | 9 ++ app/templates/entities/fragments/add.html | 11 ++ app/templates/entities/fragments/edit.html | 11 ++ app/templates/entities/fragments/list.html | 60 ++++++++++ app/templates/entities/pages/index.html | 8 ++ 21 files changed, 517 insertions(+), 11 deletions(-) create mode 100644 app/apps/rules/migrations/0004_alter_transactionruleaction_field.py create mode 100644 app/apps/transactions/migrations/0023_transactionentity_transaction_entities.py create mode 100644 app/apps/transactions/migrations/0024_installmentplan_entities_and_more.py create mode 100644 app/apps/transactions/views/entities.py create mode 100644 app/templates/entities/fragments/add.html create mode 100644 app/templates/entities/fragments/edit.html create mode 100644 app/templates/entities/fragments/list.html create mode 100644 app/templates/entities/pages/index.html diff --git a/app/apps/api/fields/transactions.py b/app/apps/api/fields/transactions.py index 0225648..2ca9466 100644 --- a/app/apps/api/fields/transactions.py +++ b/app/apps/api/fields/transactions.py @@ -2,7 +2,11 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from apps.transactions.models import TransactionCategory, TransactionTag +from apps.transactions.models import ( + TransactionCategory, + TransactionTag, + TransactionEntity, +) @extend_schema_field( @@ -67,3 +71,27 @@ class TransactionTagField(serializers.Field): ) tags.append(tag) return tags + + +class TransactionEntityField(serializers.Field): + def to_representation(self, value): + return [{"id": entity.id, "name": entity.name} for entity in value.all()] + + def to_internal_value(self, data): + entities = [] + for item in data: + if isinstance(item, int): + try: + entity = TransactionEntity.objects.get(pk=item) + except TransactionTag.DoesNotExist: + raise serializers.ValidationError( + f"Entity with ID {item} 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." + ) + entities.append(entity) + return entities diff --git a/app/apps/api/serializers/transactions.py b/app/apps/api/serializers/transactions.py index ee0a5cf..467c0a0 100644 --- a/app/apps/api/serializers/transactions.py +++ b/app/apps/api/serializers/transactions.py @@ -7,17 +7,21 @@ from rest_framework import serializers from rest_framework.permissions import IsAuthenticated from apps.accounts.models import Account -from apps.api.fields.transactions import TransactionTagField, TransactionCategoryField +from apps.api.fields.transactions import ( + TransactionTagField, + TransactionCategoryField, + TransactionEntityField, +) from apps.api.serializers.accounts import AccountSerializer from apps.transactions.models import ( Transaction, TransactionCategory, TransactionTag, InstallmentPlan, + TransactionEntity, ) -# Create serializers for other related models as needed class TransactionCategorySerializer(serializers.ModelSerializer): permission_classes = [IsAuthenticated] @@ -34,6 +38,14 @@ class TransactionTagSerializer(serializers.ModelSerializer): fields = "__all__" +class TransactionEntitySerializer(serializers.ModelSerializer): + permission_classes = [IsAuthenticated] + + class Meta: + model = TransactionEntity + fields = "__all__" + + class InstallmentPlanSerializer(serializers.ModelSerializer): permission_classes = [IsAuthenticated] @@ -45,6 +57,7 @@ class InstallmentPlanSerializer(serializers.ModelSerializer): class TransactionSerializer(serializers.ModelSerializer): category = TransactionCategoryField(required=False) tags = TransactionTagField(required=False) + entities = TransactionEntityField(required=False) exchanged_amount = serializers.SerializerMethodField() @@ -86,17 +99,24 @@ class TransactionSerializer(serializers.ModelSerializer): def create(self, validated_data): tags = validated_data.pop("tags", []) + entities = validated_data.pop("entities", []) transaction = Transaction.objects.create(**validated_data) transaction.tags.set(tags) + transaction.entities.set(entities) return transaction def update(self, instance, validated_data): tags = validated_data.pop("tags", None) + entities = validated_data.pop("entities", None) for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() + if tags is not None: instance.tags.set(tags) + if entities is not None: + instance.entities.set(entities) + return instance @staticmethod diff --git a/app/apps/api/urls.py b/app/apps/api/urls.py index 071f487..b2f8028 100644 --- a/app/apps/api/urls.py +++ b/app/apps/api/urls.py @@ -7,6 +7,7 @@ router = routers.DefaultRouter() router.register(r"transactions", views.TransactionViewSet) 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"account-groups", views.AccountGroupViewSet) router.register(r"accounts", views.AccountViewSet) diff --git a/app/apps/api/views/transactions.py b/app/apps/api/views/transactions.py index e05a95c..92c12cf 100644 --- a/app/apps/api/views/transactions.py +++ b/app/apps/api/views/transactions.py @@ -5,12 +5,14 @@ from apps.api.serializers import ( TransactionCategorySerializer, TransactionTagSerializer, InstallmentPlanSerializer, + TransactionEntitySerializer, ) from apps.transactions.models import ( Transaction, TransactionCategory, TransactionTag, InstallmentPlan, + TransactionEntity, ) from apps.rules.signals import transaction_updated, transaction_created @@ -42,6 +44,11 @@ class TransactionTagViewSet(viewsets.ModelViewSet): serializer_class = TransactionTagSerializer +class TransactionEntityViewSet(viewsets.ModelViewSet): + queryset = TransactionEntity.objects.all() + serializer_class = TransactionEntitySerializer + + class InstallmentPlanViewSet(viewsets.ModelViewSet): queryset = InstallmentPlan.objects.all() serializer_class = InstallmentPlanSerializer diff --git a/app/apps/rules/migrations/0004_alter_transactionruleaction_field.py b/app/apps/rules/migrations/0004_alter_transactionruleaction_field.py new file mode 100644 index 0000000..bb66775 --- /dev/null +++ b/app/apps/rules/migrations/0004_alter_transactionruleaction_field.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2024-11-30 20:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rules', '0003_alter_transactionruleaction_unique_together'), + ] + + operations = [ + migrations.AlterField( + model_name='transactionruleaction', + name='field', + field=models.CharField(choices=[('account', 'Account'), ('type', 'Type'), ('is_paid', 'Paid'), ('date', 'Date'), ('reference_date', 'Reference Date'), ('amount', 'Amount'), ('description', 'Description'), ('notes', 'Notes'), ('category', 'Category'), ('tags', 'Tags'), ('entities', 'Entities')], max_length=50, verbose_name='Field'), + ), + ] diff --git a/app/apps/rules/models.py b/app/apps/rules/models.py index 39596f9..d8fd6c6 100644 --- a/app/apps/rules/models.py +++ b/app/apps/rules/models.py @@ -26,6 +26,7 @@ class TransactionRuleAction(models.Model): notes = "notes", _("Notes") category = "category", _("Category") tags = "tags", _("Tags") + entities = "entities", _("Entities") rule = models.ForeignKey( TransactionRule, diff --git a/app/apps/rules/tasks.py b/app/apps/rules/tasks.py index 4352640..906047c 100644 --- a/app/apps/rules/tasks.py +++ b/app/apps/rules/tasks.py @@ -5,7 +5,12 @@ from simpleeval import EvalWithCompoundTypes from apps.accounts.models import Account from apps.rules.models import TransactionRule, TransactionRuleAction -from apps.transactions.models import Transaction, TransactionCategory, TransactionTag +from apps.transactions.models import ( + Transaction, + TransactionCategory, + TransactionTag, + TransactionEntity, +) @app.task @@ -32,6 +37,8 @@ def check_for_transaction_rules( "category_id": instance.category.id if instance.category else None, "tag_names": [x.name for x in instance.tags.all()], "tag_ids": [x.id for x in instance.tags.all()], + "entities_names": [x.name for x in instance.entities.all()], + "entities_ids": [x.id for x in instance.entities.all()], "is_expense": instance.type == Transaction.Type.EXPENSE, "is_income": instance.type == Transaction.Type.INCOME, "is_paid": instance.is_paid, @@ -112,4 +119,31 @@ def check_for_transaction_rules( instance.tags.add(tag) + elif action.field == TransactionRuleAction.Field.entities: + value = simple.eval(action.value) + if isinstance(value, list): + # Clear existing entities + instance.entities.clear() + for entity_value in value: + if isinstance(entity_value, int): + entity = TransactionEntity.objects.get( + id=entity_value + ) + instance.entities.add(entity) + elif isinstance(entity_value, str): + entity = TransactionEntity.objects.get( + name=entity_value + ) + instance.entities.add(entity) + + elif isinstance(value, (int, str)): + # If a single value is provided, treat it as a single entity + instance.entities.clear() + if isinstance(value, int): + entity = TransactionEntity.objects.get(id=value) + else: + entity = TransactionEntity.objects.get(name=value) + + instance.entities.add(entity) + instance.save() diff --git a/app/apps/transactions/admin.py b/app/apps/transactions/admin.py index 073eae7..5a4ef15 100644 --- a/app/apps/transactions/admin.py +++ b/app/apps/transactions/admin.py @@ -6,6 +6,7 @@ from apps.transactions.models import ( TransactionTag, InstallmentPlan, RecurringTransaction, + TransactionEntity, ) @@ -43,3 +44,4 @@ class RecurringTransactionAdmin(admin.ModelAdmin): admin.site.register(TransactionCategory) admin.site.register(TransactionTag) +admin.site.register(TransactionEntity) diff --git a/app/apps/transactions/filters.py b/app/apps/transactions/filters.py index 5c77006..d4b6e2d 100644 --- a/app/apps/transactions/filters.py +++ b/app/apps/transactions/filters.py @@ -7,9 +7,12 @@ from django.utils.translation import gettext_lazy as _ from django_filters import Filter from apps.accounts.models import Account -from apps.transactions.models import Transaction -from apps.transactions.models import TransactionCategory -from apps.transactions.models import TransactionTag +from apps.transactions.models import ( + Transaction, + TransactionCategory, + TransactionTag, + TransactionEntity, +) from apps.common.widgets.tom_select import TomSelectMultiple from apps.common.fields.month_year import MonthYearFormField from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput @@ -62,6 +65,13 @@ class TransactionsFilter(django_filters.FilterSet): label=_("Tags"), widget=TomSelectMultiple(checkboxes=True, remove_button=True), ) + entities = django_filters.ModelMultipleChoiceFilter( + field_name="entities__name", + queryset=TransactionEntity.objects.all(), + to_field_name="name", + label=_("Entities"), + widget=TomSelectMultiple(checkboxes=True, remove_button=True), + ) is_paid = django_filters.MultipleChoiceFilter( choices=SITUACAO_CHOICES, field_name="is_paid", @@ -159,6 +169,7 @@ class TransactionsFilter(django_filters.FilterSet): Field("account", size=1), Field("category", size=1), Field("tags", size=1), + Field("entities", size=1), ) self.form.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput() diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py index 509ded2..9199e12 100644 --- a/app/apps/transactions/forms.py +++ b/app/apps/transactions/forms.py @@ -25,6 +25,7 @@ from apps.transactions.models import ( TransactionTag, InstallmentPlan, RecurringTransaction, + TransactionEntity, ) from apps.rules.signals import transaction_created, transaction_updated @@ -42,6 +43,13 @@ class TransactionForm(forms.ModelForm): required=False, label=_("Tags"), ) + entities = DynamicModelMultipleChoiceField( + model=TransactionEntity, + to_field_name="name", + create_field="name", + required=False, + label=_("Entities"), + ) account = forms.ModelChoiceField( queryset=Account.objects.filter(is_archived=False), label=_("Account"), @@ -62,6 +70,7 @@ class TransactionForm(forms.ModelForm): "notes", "category", "tags", + "entities", ] widgets = { "date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"), @@ -81,7 +90,11 @@ class TransactionForm(forms.ModelForm): template="transactions/widgets/income_expense_toggle_buttons.html", ), Switch("is_paid"), - "account", + Row( + Column("account", css_class="form-group col-md-6 mb-0"), + Column("entities", css_class="form-group col-md-6 mb-0"), + css_class="form-row", + ), Row( Column("date", css_class="form-group col-md-6 mb-0"), Column("reference_date", css_class="form-group col-md-6 mb-0"), @@ -351,6 +364,13 @@ class InstallmentPlanForm(forms.ModelForm): required=False, label=_("Category"), ) + entities = DynamicModelMultipleChoiceField( + model=TransactionEntity, + to_field_name="name", + create_field="name", + required=False, + label=_("Entities"), + ) type = forms.ChoiceField(choices=Transaction.Type.choices) reference_date = MonthYearFormField(label=_("Reference Date"), required=False) @@ -369,6 +389,7 @@ class InstallmentPlanForm(forms.ModelForm): "tags", "notes", "installment_start", + "entities", ] widgets = { "start_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"), @@ -389,7 +410,11 @@ class InstallmentPlanForm(forms.ModelForm): "type", template="transactions/widgets/income_expense_toggle_buttons.html", ), - "account", + Row( + Column("account", css_class="form-group col-md-6 mb-0"), + Column("entities", css_class="form-group col-md-6 mb-0"), + css_class="form-row", + ), "description", "notes", Row( @@ -474,6 +499,38 @@ class TransactionTagForm(forms.ModelForm): ) +class TransactionEntityForm(forms.ModelForm): + class Meta: + model = TransactionEntity + fields = ["name"] + labels = {"name": _("Entity name")} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.helper = FormHelper() + self.helper.form_tag = False + self.helper.form_method = "post" + self.helper.layout = Layout(Field("name", css_class="mb-3")) + + if self.instance and self.instance.pk: + self.helper.layout.append( + FormActions( + NoClassSubmit( + "submit", _("Update"), css_class="btn btn-outline-primary w-100" + ), + ), + ) + else: + self.helper.layout.append( + FormActions( + NoClassSubmit( + "submit", _("Add"), css_class="btn btn-outline-primary w-100" + ), + ), + ) + + class TransactionCategoryForm(forms.ModelForm): class Meta: model = TransactionCategory @@ -527,6 +584,13 @@ class RecurringTransactionForm(forms.ModelForm): required=False, label=_("Category"), ) + entities = DynamicModelMultipleChoiceField( + model=TransactionEntity, + to_field_name="name", + create_field="name", + required=False, + label=_("Entities"), + ) type = forms.ChoiceField(choices=Transaction.Type.choices) reference_date = MonthYearFormField(label=_("Reference Date"), required=False) @@ -568,7 +632,11 @@ class RecurringTransactionForm(forms.ModelForm): "type", template="transactions/widgets/income_expense_toggle_buttons.html", ), - "account", + Row( + Column("account", css_class="form-group col-md-6 mb-0"), + Column("entities", css_class="form-group col-md-6 mb-0"), + css_class="form-row", + ), "description", "amount", Row( diff --git a/app/apps/transactions/migrations/0023_transactionentity_transaction_entities.py b/app/apps/transactions/migrations/0023_transactionentity_transaction_entities.py new file mode 100644 index 0000000..0322a20 --- /dev/null +++ b/app/apps/transactions/migrations/0023_transactionentity_transaction_entities.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.3 on 2024-11-30 18:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transactions', '0022_rename_paused_recurringtransaction_is_paused'), + ] + + operations = [ + migrations.CreateModel( + name='TransactionEntity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ], + options={ + 'verbose_name': 'Entity', + 'verbose_name_plural': 'Entities', + 'db_table': 'entities', + }, + ), + migrations.AddField( + model_name='transaction', + name='entities', + field=models.ManyToManyField(blank=True, help_text='Payees/Payers involved in this transaction', related_name='transactions', to='transactions.transactionentity', verbose_name='Entities'), + ), + ] diff --git a/app/apps/transactions/migrations/0024_installmentplan_entities_and_more.py b/app/apps/transactions/migrations/0024_installmentplan_entities_and_more.py new file mode 100644 index 0000000..d66b9fa --- /dev/null +++ b/app/apps/transactions/migrations/0024_installmentplan_entities_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.3 on 2024-11-30 20:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transactions', '0023_transactionentity_transaction_entities'), + ] + + operations = [ + migrations.AddField( + model_name='installmentplan', + name='entities', + field=models.ManyToManyField(blank=True, to='transactions.transactionentity', verbose_name='Entities'), + ), + migrations.AddField( + model_name='recurringtransaction', + name='entities', + field=models.ManyToManyField(blank=True, to='transactions.transactionentity', verbose_name='Entities'), + ), + migrations.AlterField( + model_name='transaction', + name='entities', + field=models.ManyToManyField(blank=True, related_name='transactions', to='transactions.transactionentity', verbose_name='Entities'), + ), + ] diff --git a/app/apps/transactions/models.py b/app/apps/transactions/models.py index 14889b9..6e7fd40 100644 --- a/app/apps/transactions/models.py +++ b/app/apps/transactions/models.py @@ -40,6 +40,20 @@ class TransactionTag(models.Model): return self.name +class TransactionEntity(models.Model): + name = models.CharField(max_length=255, verbose_name=_("Name")) + + # Add any other fields you might want for entities + + class Meta: + verbose_name = _("Entity") + verbose_name_plural = _("Entities") + db_table = "entities" + + def __str__(self): + return self.name + + class Transaction(models.Model): class Type(models.TextChoices): INCOME = "IN", _("Income") @@ -77,7 +91,17 @@ class Transaction(models.Model): blank=True, null=True, ) - tags = models.ManyToManyField(TransactionTag, verbose_name=_("Tags"), blank=True) + tags = models.ManyToManyField( + TransactionTag, + verbose_name=_("Tags"), + blank=True, + ) + entities = models.ManyToManyField( + TransactionEntity, + verbose_name=_("Entities"), + blank=True, + related_name="transactions", + ) installment_plan = models.ForeignKey( "InstallmentPlan", @@ -185,6 +209,12 @@ class InstallmentPlan(models.Model): verbose_name=_("Category"), ) tags = models.ManyToManyField(TransactionTag, verbose_name=_("Tags"), blank=True) + entities = models.ManyToManyField( + TransactionEntity, + verbose_name=_("Entities"), + blank=True, + ) + notes = models.TextField(blank=True, verbose_name=_("Notes")) class Meta: @@ -255,6 +285,7 @@ class InstallmentPlan(models.Model): notes=self.notes, ) new_transaction.tags.set(self.tags.all()) + new_transaction.entities.set(self.entities.all()) @transaction.atomic def update_transactions(self): @@ -292,6 +323,7 @@ class InstallmentPlan(models.Model): # Update tags existing_transaction.tags.set(self.tags.all()) + existing_transaction.entities.set(self.entities.all()) else: # If the transaction doesn't exist, create a new one new_transaction = Transaction.objects.create( @@ -308,6 +340,7 @@ class InstallmentPlan(models.Model): notes=self.notes, ) new_transaction.tags.set(self.tags.all()) + new_transaction.entities.set(self.entities.all()) # Remove any extra transactions that are no longer part of the plan self.transactions.filter( @@ -353,6 +386,11 @@ class RecurringTransaction(models.Model): null=True, ) tags = models.ManyToManyField(TransactionTag, verbose_name=_("Tags"), blank=True) + entities = models.ManyToManyField( + TransactionEntity, + verbose_name=_("Entities"), + blank=True, + ) notes = models.TextField(blank=True, verbose_name=_("Notes")) reference_date = models.DateField( verbose_name=_("Reference Date"), null=True, blank=True @@ -426,6 +464,8 @@ class RecurringTransaction(models.Model): ) if self.tags.exists(): created_transaction.tags.set(self.tags.all()) + if self.entities.exists(): + created_transaction.entities.set(self.entities.all()) def get_recurrence_delta(self): if self.recurrence_type == self.RecurrenceType.DAY: diff --git a/app/apps/transactions/urls.py b/app/apps/transactions/urls.py index f69185d..c89b1a9 100644 --- a/app/apps/transactions/urls.py +++ b/app/apps/transactions/urls.py @@ -59,6 +59,19 @@ urlpatterns = [ views.tag_delete, name="tag_delete", ), + path("entities/", views.entities_index, name="entities_index"), + path("entities/list/", views.entities_list, name="entities_list"), + path("entities/add/", views.entity_add, name="entity_add"), + path( + "entities//edit/", + views.entity_edit, + name="entity_edit", + ), + path( + "entities//delete/", + views.entity_delete, + name="entity_delete", + ), path("categories/", views.categories_index, name="categories_index"), path("categories/list/", views.categories_list, name="categories_list"), path("categories/add/", views.category_add, name="category_add"), diff --git a/app/apps/transactions/views/__init__.py b/app/apps/transactions/views/__init__.py index dddb240..fa20bc8 100644 --- a/app/apps/transactions/views/__init__.py +++ b/app/apps/transactions/views/__init__.py @@ -1,5 +1,6 @@ from .transactions import * from .tags import * +from .entities import * from .categories import * from .actions import * from .installment_plans import * diff --git a/app/apps/transactions/views/entities.py b/app/apps/transactions/views/entities.py new file mode 100644 index 0000000..2faed7a --- /dev/null +++ b/app/apps/transactions/views/entities.py @@ -0,0 +1,105 @@ +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +from django.shortcuts import render, get_object_or_404 +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods + +from apps.common.decorators.htmx import only_htmx +from apps.transactions.forms import TransactionEntityForm +from apps.transactions.models import TransactionEntity + + +@login_required +@require_http_methods(["GET"]) +def entities_index(request): + return render( + request, + "entities/pages/index.html", + ) + + +@only_htmx +@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", "POST"]) +def entity_add(request, **kwargs): + if request.method == "POST": + form = TransactionEntityForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("Entity added successfully")) + + return HttpResponse( + status=204, + headers={ + "HX-Trigger": "updated, hide_offcanvas", + }, + ) + else: + form = TransactionEntityForm() + + return render( + request, + "entities/fragments/add.html", + {"form": form}, + ) + + +@only_htmx +@login_required +@require_http_methods(["GET", "POST"]) +def entity_edit(request, entity_id): + entity = get_object_or_404(TransactionEntity, id=entity_id) + + if request.method == "POST": + form = TransactionEntityForm(request.POST, instance=entity) + if form.is_valid(): + form.save() + messages.success(request, _("Entity updated successfully")) + + return HttpResponse( + status=204, + headers={ + "HX-Trigger": "updated, hide_offcanvas", + }, + ) + else: + form = TransactionEntityForm(instance=entity) + + return render( + request, + "entities/fragments/edit.html", + {"form": form, "entity": entity}, + ) + + +@only_htmx +@login_required +@csrf_exempt +@require_http_methods(["DELETE"]) +def entity_delete(request, entity_id): + entity = get_object_or_404(TransactionEntity, id=entity_id) + + entity.delete() + + messages.success(request, _("Entity deleted successfully")) + + return HttpResponse( + status=204, + headers={ + "HX-Trigger": "updated, hide_offcanvas", + }, + ) diff --git a/app/templates/cotton/transaction/item.html b/app/templates/cotton/transaction/item.html index d761e77..67f3aec 100644 --- a/app/templates/cotton/transaction/item.html +++ b/app/templates/cotton/transaction/item.html @@ -41,6 +41,15 @@ {% endspaceless %}
+ {# Entities #} + {% with transaction.entities.all as entities %} + {% if entities %} +
+
+
{{ entities|join:", " }}
+
+ {% endif %} + {% endwith %} {# Notes#} {% if transaction.notes %}
diff --git a/app/templates/entities/fragments/add.html b/app/templates/entities/fragments/add.html new file mode 100644 index 0000000..c182deb --- /dev/null +++ b/app/templates/entities/fragments/add.html @@ -0,0 +1,11 @@ +{% extends 'extends/offcanvas.html' %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title %}{% translate 'Add entity' %}{% endblock %} + +{% block body %} +
+ {% crispy form %} +
+{% endblock %} diff --git a/app/templates/entities/fragments/edit.html b/app/templates/entities/fragments/edit.html new file mode 100644 index 0000000..01deb0e --- /dev/null +++ b/app/templates/entities/fragments/edit.html @@ -0,0 +1,11 @@ +{% extends 'extends/offcanvas.html' %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title %}{% translate 'Edit entity' %}{% endblock %} + +{% block body %} +
+ {% crispy form %} +
+{% endblock %} diff --git a/app/templates/entities/fragments/list.html b/app/templates/entities/fragments/list.html new file mode 100644 index 0000000..d84f2ba --- /dev/null +++ b/app/templates/entities/fragments/list.html @@ -0,0 +1,60 @@ +{% load i18n %} +
+
+ {% spaceless %} +
{% translate 'Entities' %} + + +
+ {% endspaceless %} +
+ +
+ {% if entities %} + + + + + + + + + {% for entity in entities %} + + + + + {% endfor %} + +
{% translate 'Name' %}
+
+ + + +
+
{{ entity.name }}
+ {% else %} + + {% endif %} +
+
diff --git a/app/templates/entities/pages/index.html b/app/templates/entities/pages/index.html new file mode 100644 index 0000000..cff23d2 --- /dev/null +++ b/app/templates/entities/pages/index.html @@ -0,0 +1,8 @@ +{% extends "layouts/base.html" %} +{% load i18n %} + +{% block title %}{% translate 'Entities' %}{% endblock %} + +{% block content %} +
+{% endblock %}