mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-12 07:42:53 +02:00
feat: add Transaction Entity
This commit is contained in:
@@ -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 *
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user