feat: add Transaction Entity

This commit is contained in:
Herculino Trotta
2024-11-30 17:12:35 -03:00
parent 28c8a961bc
commit 2382abf3c0
21 changed files with 517 additions and 11 deletions
+29 -1
View File
@@ -2,7 +2,11 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
from apps.transactions.models import TransactionCategory, TransactionTag from apps.transactions.models import (
TransactionCategory,
TransactionTag,
TransactionEntity,
)
@extend_schema_field( @extend_schema_field(
@@ -67,3 +71,27 @@ class TransactionTagField(serializers.Field):
) )
tags.append(tag) tags.append(tag)
return tags 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
+22 -2
View File
@@ -7,17 +7,21 @@ from rest_framework import serializers
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from apps.accounts.models import Account 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.api.serializers.accounts import AccountSerializer
from apps.transactions.models import ( from apps.transactions.models import (
Transaction, Transaction,
TransactionCategory, TransactionCategory,
TransactionTag, TransactionTag,
InstallmentPlan, InstallmentPlan,
TransactionEntity,
) )
# Create serializers for other related models as needed
class TransactionCategorySerializer(serializers.ModelSerializer): class TransactionCategorySerializer(serializers.ModelSerializer):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -34,6 +38,14 @@ class TransactionTagSerializer(serializers.ModelSerializer):
fields = "__all__" fields = "__all__"
class TransactionEntitySerializer(serializers.ModelSerializer):
permission_classes = [IsAuthenticated]
class Meta:
model = TransactionEntity
fields = "__all__"
class InstallmentPlanSerializer(serializers.ModelSerializer): class InstallmentPlanSerializer(serializers.ModelSerializer):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -45,6 +57,7 @@ class InstallmentPlanSerializer(serializers.ModelSerializer):
class TransactionSerializer(serializers.ModelSerializer): class TransactionSerializer(serializers.ModelSerializer):
category = TransactionCategoryField(required=False) category = TransactionCategoryField(required=False)
tags = TransactionTagField(required=False) tags = TransactionTagField(required=False)
entities = TransactionEntityField(required=False)
exchanged_amount = serializers.SerializerMethodField() exchanged_amount = serializers.SerializerMethodField()
@@ -86,17 +99,24 @@ class TransactionSerializer(serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
tags = validated_data.pop("tags", []) tags = validated_data.pop("tags", [])
entities = validated_data.pop("entities", [])
transaction = Transaction.objects.create(**validated_data) transaction = Transaction.objects.create(**validated_data)
transaction.tags.set(tags) transaction.tags.set(tags)
transaction.entities.set(entities)
return transaction return transaction
def update(self, instance, validated_data): def update(self, instance, validated_data):
tags = validated_data.pop("tags", None) tags = validated_data.pop("tags", None)
entities = validated_data.pop("entities", None)
for attr, value in validated_data.items(): for attr, value in validated_data.items():
setattr(instance, attr, value) setattr(instance, attr, value)
instance.save() instance.save()
if tags is not None: if tags is not None:
instance.tags.set(tags) instance.tags.set(tags)
if entities is not None:
instance.entities.set(entities)
return instance return instance
@staticmethod @staticmethod
+1
View File
@@ -7,6 +7,7 @@ router = routers.DefaultRouter()
router.register(r"transactions", views.TransactionViewSet) router.register(r"transactions", views.TransactionViewSet)
router.register(r"categories", views.TransactionCategoryViewSet) router.register(r"categories", views.TransactionCategoryViewSet)
router.register(r"tags", views.TransactionTagViewSet) router.register(r"tags", views.TransactionTagViewSet)
router.register(r"entities", views.TransactionEntityViewSet)
router.register(r"installment-plans", views.InstallmentPlanViewSet) router.register(r"installment-plans", views.InstallmentPlanViewSet)
router.register(r"account-groups", views.AccountGroupViewSet) router.register(r"account-groups", views.AccountGroupViewSet)
router.register(r"accounts", views.AccountViewSet) router.register(r"accounts", views.AccountViewSet)
+7
View File
@@ -5,12 +5,14 @@ from apps.api.serializers import (
TransactionCategorySerializer, TransactionCategorySerializer,
TransactionTagSerializer, TransactionTagSerializer,
InstallmentPlanSerializer, InstallmentPlanSerializer,
TransactionEntitySerializer,
) )
from apps.transactions.models import ( from apps.transactions.models import (
Transaction, Transaction,
TransactionCategory, TransactionCategory,
TransactionTag, TransactionTag,
InstallmentPlan, InstallmentPlan,
TransactionEntity,
) )
from apps.rules.signals import transaction_updated, transaction_created from apps.rules.signals import transaction_updated, transaction_created
@@ -42,6 +44,11 @@ class TransactionTagViewSet(viewsets.ModelViewSet):
serializer_class = TransactionTagSerializer serializer_class = TransactionTagSerializer
class TransactionEntityViewSet(viewsets.ModelViewSet):
queryset = TransactionEntity.objects.all()
serializer_class = TransactionEntitySerializer
class InstallmentPlanViewSet(viewsets.ModelViewSet): class InstallmentPlanViewSet(viewsets.ModelViewSet):
queryset = InstallmentPlan.objects.all() queryset = InstallmentPlan.objects.all()
serializer_class = InstallmentPlanSerializer serializer_class = InstallmentPlanSerializer
@@ -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'),
),
]
+1
View File
@@ -26,6 +26,7 @@ class TransactionRuleAction(models.Model):
notes = "notes", _("Notes") notes = "notes", _("Notes")
category = "category", _("Category") category = "category", _("Category")
tags = "tags", _("Tags") tags = "tags", _("Tags")
entities = "entities", _("Entities")
rule = models.ForeignKey( rule = models.ForeignKey(
TransactionRule, TransactionRule,
+35 -1
View File
@@ -5,7 +5,12 @@ from simpleeval import EvalWithCompoundTypes
from apps.accounts.models import Account from apps.accounts.models import Account
from apps.rules.models import TransactionRule, TransactionRuleAction 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 @app.task
@@ -32,6 +37,8 @@ def check_for_transaction_rules(
"category_id": instance.category.id if instance.category else None, "category_id": instance.category.id if instance.category else None,
"tag_names": [x.name for x in instance.tags.all()], "tag_names": [x.name for x in instance.tags.all()],
"tag_ids": [x.id 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_expense": instance.type == Transaction.Type.EXPENSE,
"is_income": instance.type == Transaction.Type.INCOME, "is_income": instance.type == Transaction.Type.INCOME,
"is_paid": instance.is_paid, "is_paid": instance.is_paid,
@@ -112,4 +119,31 @@ def check_for_transaction_rules(
instance.tags.add(tag) 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() instance.save()
+2
View File
@@ -6,6 +6,7 @@ from apps.transactions.models import (
TransactionTag, TransactionTag,
InstallmentPlan, InstallmentPlan,
RecurringTransaction, RecurringTransaction,
TransactionEntity,
) )
@@ -43,3 +44,4 @@ class RecurringTransactionAdmin(admin.ModelAdmin):
admin.site.register(TransactionCategory) admin.site.register(TransactionCategory)
admin.site.register(TransactionTag) admin.site.register(TransactionTag)
admin.site.register(TransactionEntity)
+14 -3
View File
@@ -7,9 +7,12 @@ from django.utils.translation import gettext_lazy as _
from django_filters import Filter from django_filters import Filter
from apps.accounts.models import Account from apps.accounts.models import Account
from apps.transactions.models import Transaction from apps.transactions.models import (
from apps.transactions.models import TransactionCategory Transaction,
from apps.transactions.models import TransactionTag TransactionCategory,
TransactionTag,
TransactionEntity,
)
from apps.common.widgets.tom_select import TomSelectMultiple from apps.common.widgets.tom_select import TomSelectMultiple
from apps.common.fields.month_year import MonthYearFormField from apps.common.fields.month_year import MonthYearFormField
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
@@ -62,6 +65,13 @@ class TransactionsFilter(django_filters.FilterSet):
label=_("Tags"), label=_("Tags"),
widget=TomSelectMultiple(checkboxes=True, remove_button=True), 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( is_paid = django_filters.MultipleChoiceFilter(
choices=SITUACAO_CHOICES, choices=SITUACAO_CHOICES,
field_name="is_paid", field_name="is_paid",
@@ -159,6 +169,7 @@ class TransactionsFilter(django_filters.FilterSet):
Field("account", size=1), Field("account", size=1),
Field("category", size=1), Field("category", size=1),
Field("tags", size=1), Field("tags", size=1),
Field("entities", size=1),
) )
self.form.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput() self.form.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()
+71 -3
View File
@@ -25,6 +25,7 @@ from apps.transactions.models import (
TransactionTag, TransactionTag,
InstallmentPlan, InstallmentPlan,
RecurringTransaction, RecurringTransaction,
TransactionEntity,
) )
from apps.rules.signals import transaction_created, transaction_updated from apps.rules.signals import transaction_created, transaction_updated
@@ -42,6 +43,13 @@ class TransactionForm(forms.ModelForm):
required=False, required=False,
label=_("Tags"), label=_("Tags"),
) )
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
)
account = forms.ModelChoiceField( account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False), queryset=Account.objects.filter(is_archived=False),
label=_("Account"), label=_("Account"),
@@ -62,6 +70,7 @@ class TransactionForm(forms.ModelForm):
"notes", "notes",
"category", "category",
"tags", "tags",
"entities",
] ]
widgets = { widgets = {
"date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"), "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", template="transactions/widgets/income_expense_toggle_buttons.html",
), ),
Switch("is_paid"), 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( Row(
Column("date", css_class="form-group col-md-6 mb-0"), Column("date", css_class="form-group col-md-6 mb-0"),
Column("reference_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, required=False,
label=_("Category"), label=_("Category"),
) )
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
)
type = forms.ChoiceField(choices=Transaction.Type.choices) type = forms.ChoiceField(choices=Transaction.Type.choices)
reference_date = MonthYearFormField(label=_("Reference Date"), required=False) reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
@@ -369,6 +389,7 @@ class InstallmentPlanForm(forms.ModelForm):
"tags", "tags",
"notes", "notes",
"installment_start", "installment_start",
"entities",
] ]
widgets = { widgets = {
"start_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"), "start_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
@@ -389,7 +410,11 @@ class InstallmentPlanForm(forms.ModelForm):
"type", "type",
template="transactions/widgets/income_expense_toggle_buttons.html", 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", "description",
"notes", "notes",
Row( 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 TransactionCategoryForm(forms.ModelForm):
class Meta: class Meta:
model = TransactionCategory model = TransactionCategory
@@ -527,6 +584,13 @@ class RecurringTransactionForm(forms.ModelForm):
required=False, required=False,
label=_("Category"), label=_("Category"),
) )
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
)
type = forms.ChoiceField(choices=Transaction.Type.choices) type = forms.ChoiceField(choices=Transaction.Type.choices)
reference_date = MonthYearFormField(label=_("Reference Date"), required=False) reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
@@ -568,7 +632,11 @@ class RecurringTransactionForm(forms.ModelForm):
"type", "type",
template="transactions/widgets/income_expense_toggle_buttons.html", 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", "description",
"amount", "amount",
Row( Row(
@@ -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'),
),
]
@@ -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'),
),
]
+41 -1
View File
@@ -40,6 +40,20 @@ class TransactionTag(models.Model):
return self.name 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 Transaction(models.Model):
class Type(models.TextChoices): class Type(models.TextChoices):
INCOME = "IN", _("Income") INCOME = "IN", _("Income")
@@ -77,7 +91,17 @@ class Transaction(models.Model):
blank=True, blank=True,
null=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( installment_plan = models.ForeignKey(
"InstallmentPlan", "InstallmentPlan",
@@ -185,6 +209,12 @@ class InstallmentPlan(models.Model):
verbose_name=_("Category"), verbose_name=_("Category"),
) )
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,
)
notes = models.TextField(blank=True, verbose_name=_("Notes")) notes = models.TextField(blank=True, verbose_name=_("Notes"))
class Meta: class Meta:
@@ -255,6 +285,7 @@ class InstallmentPlan(models.Model):
notes=self.notes, notes=self.notes,
) )
new_transaction.tags.set(self.tags.all()) new_transaction.tags.set(self.tags.all())
new_transaction.entities.set(self.entities.all())
@transaction.atomic @transaction.atomic
def update_transactions(self): def update_transactions(self):
@@ -292,6 +323,7 @@ class InstallmentPlan(models.Model):
# Update tags # Update tags
existing_transaction.tags.set(self.tags.all()) existing_transaction.tags.set(self.tags.all())
existing_transaction.entities.set(self.entities.all())
else: else:
# If the transaction doesn't exist, create a new one # If the transaction doesn't exist, create a new one
new_transaction = Transaction.objects.create( new_transaction = Transaction.objects.create(
@@ -308,6 +340,7 @@ class InstallmentPlan(models.Model):
notes=self.notes, notes=self.notes,
) )
new_transaction.tags.set(self.tags.all()) 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 # Remove any extra transactions that are no longer part of the plan
self.transactions.filter( self.transactions.filter(
@@ -353,6 +386,11 @@ class RecurringTransaction(models.Model):
null=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,
)
notes = models.TextField(blank=True, verbose_name=_("Notes")) notes = models.TextField(blank=True, verbose_name=_("Notes"))
reference_date = models.DateField( reference_date = models.DateField(
verbose_name=_("Reference Date"), null=True, blank=True verbose_name=_("Reference Date"), null=True, blank=True
@@ -426,6 +464,8 @@ class RecurringTransaction(models.Model):
) )
if self.tags.exists(): if self.tags.exists():
created_transaction.tags.set(self.tags.all()) created_transaction.tags.set(self.tags.all())
if self.entities.exists():
created_transaction.entities.set(self.entities.all())
def get_recurrence_delta(self): def get_recurrence_delta(self):
if self.recurrence_type == self.RecurrenceType.DAY: if self.recurrence_type == self.RecurrenceType.DAY:
+13
View File
@@ -59,6 +59,19 @@ urlpatterns = [
views.tag_delete, views.tag_delete,
name="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/<int:entity_id>/edit/",
views.entity_edit,
name="entity_edit",
),
path(
"entities/<int:entity_id>/delete/",
views.entity_delete,
name="entity_delete",
),
path("categories/", views.categories_index, name="categories_index"), path("categories/", views.categories_index, name="categories_index"),
path("categories/list/", views.categories_list, name="categories_list"), path("categories/list/", views.categories_list, name="categories_list"),
path("categories/add/", views.category_add, name="category_add"), path("categories/add/", views.category_add, name="category_add"),
+1
View File
@@ -1,5 +1,6 @@
from .transactions import * from .transactions import *
from .tags import * from .tags import *
from .entities import *
from .categories import * from .categories import *
from .actions import * from .actions import *
from .installment_plans import * from .installment_plans import *
+105
View File
@@ -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",
},
)
@@ -41,6 +41,15 @@
{% endspaceless %} {% endspaceless %}
</div> </div>
<div class="tw-text-gray-400 tw-text-sm"> <div class="tw-text-gray-400 tw-text-sm">
{# Entities #}
{% with transaction.entities.all as entities %}
{% if entities %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ entities|join:", " }}</div>
</div>
{% endif %}
{% endwith %}
{# Notes#} {# Notes#}
{% if transaction.notes %} {% if transaction.notes %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400"> <div class="row mb-2 mb-lg-1 tw-text-gray-400">
+11
View File
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Add entity' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'entity_add' %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit entity' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'entity_edit' entity_id=entity.id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
@@ -0,0 +1,60 @@
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
{% spaceless %}
<div>{% translate 'Entities' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"
hx-get="{% url 'entity_add' %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-circle-plus fa-fw"></i></a>
</span></div>
{% endspaceless %}
</div>
<div class="border p-3 rounded-3 table-responsive">
{% if entities %}
<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>
</div>
+8
View File
@@ -0,0 +1,8 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% translate 'Entities' %}{% endblock %}
{% block content %}
<div hx-get="{% url 'entities_list' %}" hx-trigger="load, updated from:window" class="show-loading"></div>
{% endblock %}