mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-03-31 22:43:20 +02:00
feat: add Transaction Entity
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -26,6 +26,7 @@ class TransactionRuleAction(models.Model):
|
||||
notes = "notes", _("Notes")
|
||||
category = "category", _("Category")
|
||||
tags = "tags", _("Tags")
|
||||
entities = "entities", _("Entities")
|
||||
|
||||
rule = models.ForeignKey(
|
||||
TransactionRule,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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/<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/list/", views.categories_list, name="categories_list"),
|
||||
path("categories/add/", views.category_add, name="category_add"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .transactions import *
|
||||
from .tags import *
|
||||
from .entities import *
|
||||
from .categories import *
|
||||
from .actions import *
|
||||
from .installment_plans import *
|
||||
|
||||
105
app/apps/transactions/views/entities.py
Normal file
105
app/apps/transactions/views/entities.py
Normal 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 %}
|
||||
</div>
|
||||
<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#}
|
||||
{% if transaction.notes %}
|
||||
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
|
||||
|
||||
11
app/templates/entities/fragments/add.html
Normal file
11
app/templates/entities/fragments/add.html
Normal 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 %}
|
||||
11
app/templates/entities/fragments/edit.html
Normal file
11
app/templates/entities/fragments/edit.html
Normal file
@@ -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 %}
|
||||
60
app/templates/entities/fragments/list.html
Normal file
60
app/templates/entities/fragments/list.html
Normal file
@@ -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
app/templates/entities/pages/index.html
Normal file
8
app/templates/entities/pages/index.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user