mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-09 06:15:14 +02:00
changes
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
from crispy_bootstrap5.bootstrap5 import Switch
|
||||
from crispy_forms.bootstrap import FormActions
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Field, Column, Row
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.accounts.models import Account
|
||||
from apps.accounts.models import AccountGroup
|
||||
from apps.common.fields.forms.dynamic_select import (
|
||||
DynamicModelMultipleChoiceField,
|
||||
DynamicModelChoiceField,
|
||||
)
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
from apps.common.widgets.tom_select import TomSelect
|
||||
from apps.transactions.models import TransactionCategory, TransactionTag
|
||||
from apps.transactions.widgets import ArbitraryDecimalDisplayNumberInput
|
||||
|
||||
|
||||
class AccountGroupForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = AccountGroup
|
||||
fields = ["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(
|
||||
"name",
|
||||
)
|
||||
|
||||
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 AccountForm(forms.ModelForm):
|
||||
group = DynamicModelChoiceField(
|
||||
model=AccountGroup,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ["name", "group", "currency", "exchange_currency", "is_asset"]
|
||||
widgets = {
|
||||
"currency": TomSelect(),
|
||||
"exchange_currency": TomSelect(),
|
||||
}
|
||||
|
||||
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(
|
||||
"name",
|
||||
"group",
|
||||
Switch("is_asset"),
|
||||
"currency",
|
||||
"exchange_currency",
|
||||
)
|
||||
|
||||
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 AccountBalanceForm(forms.Form):
|
||||
account_id = forms.IntegerField(widget=forms.HiddenInput())
|
||||
new_balance = forms.DecimalField(
|
||||
max_digits=42, decimal_places=30, required=False, label=_("New balance")
|
||||
)
|
||||
category = DynamicModelChoiceField(
|
||||
model=TransactionCategory,
|
||||
required=False,
|
||||
label=_("Category"),
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
model=TransactionTag,
|
||||
to_field_name="name",
|
||||
create_field="name",
|
||||
required=False,
|
||||
label=_("Tags"),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.currency_suffix = self.initial.get("suffix", "")
|
||||
self.currency_prefix = self.initial.get("prefix", "")
|
||||
self.currency_decimal_places = self.initial.get("decimal_places", 2)
|
||||
self.account_name = self.initial.get("account_name", "")
|
||||
self.current_balance = self.initial.get("current_balance", 0)
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
self.helper.layout = Layout(
|
||||
"new_balance",
|
||||
Row(
|
||||
Column("category", css_class="form-group col-md-6 mb-0"),
|
||||
Column("tags", css_class="form-group col-md-6 mb-0"),
|
||||
css_class="form-row",
|
||||
),
|
||||
Field("account_id"),
|
||||
)
|
||||
|
||||
self.fields["new_balance"].widget = ArbitraryDecimalDisplayNumberInput(
|
||||
decimal_places=self.currency_decimal_places
|
||||
)
|
||||
|
||||
|
||||
AccountBalanceFormSet = forms.formset_factory(AccountBalanceForm, extra=0)
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-04 03:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0001_initial"),
|
||||
("currencies", "0004_exchangerate"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="account",
|
||||
name="exchange_currency",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Default currency for exchange calculations",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="exchange_accounts",
|
||||
to="currencies.currency",
|
||||
verbose_name="Exchange Currency",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-08 23:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0002_account_exchange_currency"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AccountGroup",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(max_length=255, unique=True, verbose_name="Name"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Account Group",
|
||||
"verbose_name_plural": "Account Groups",
|
||||
"db_table": "account_groups",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="account",
|
||||
name="name",
|
||||
field=models.CharField(max_length=255, verbose_name="Name"),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-08 23:47
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0003_accountgroup_alter_account_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.accountgroup', verbose_name='Account Group'),
|
||||
),
|
||||
]
|
||||
@@ -1,15 +1,46 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.transactions.models import Transaction
|
||||
|
||||
|
||||
class AccountGroup(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Account Group")
|
||||
verbose_name_plural = _("Account Groups")
|
||||
db_table = "account_groups"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Account(models.Model):
|
||||
name = models.CharField(max_length=40, verbose_name=_("Name"))
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
||||
group = models.ForeignKey(
|
||||
AccountGroup,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("Account Group"),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
currency = models.ForeignKey(
|
||||
"currencies.Currency",
|
||||
verbose_name=_("Currency"),
|
||||
on_delete=models.PROTECT,
|
||||
related_name="accounts",
|
||||
)
|
||||
exchange_currency = models.ForeignKey(
|
||||
"currencies.Currency",
|
||||
verbose_name=_("Exchange Currency"),
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="exchange_accounts",
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Default currency for exchange calculations"),
|
||||
)
|
||||
|
||||
is_asset = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Is an asset account?"),
|
||||
@@ -17,7 +48,6 @@ class Account(models.Model):
|
||||
"Asset accounts count towards your Net Worth, but not towards your month."
|
||||
),
|
||||
)
|
||||
a = models.BigIntegerField
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Account")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# urls.py
|
||||
from django.urls import path
|
||||
from apps.accounts import views
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"account-reconciliation/",
|
||||
views.account_reconciliation,
|
||||
name="account_reconciliation",
|
||||
),
|
||||
path("accounts/", views.accounts_list, name="accounts_list"),
|
||||
path("account/add/", views.account_add, name="account_add"),
|
||||
path(
|
||||
"account/<int:pk>/edit/",
|
||||
views.account_edit,
|
||||
name="account_edit",
|
||||
),
|
||||
path(
|
||||
"account/<int:pk>/delete/",
|
||||
views.account_delete,
|
||||
name="account_delete",
|
||||
),
|
||||
path("account-groups/", views.account_groups_list, name="account_groups_list"),
|
||||
path("account-groups/add/", views.account_group_add, name="account_group_add"),
|
||||
path(
|
||||
"account-groups/<int:pk>/edit/",
|
||||
views.account_group_edit,
|
||||
name="account_group_edit",
|
||||
),
|
||||
path(
|
||||
"account-groups/<int:pk>/delete/",
|
||||
views.account_group_delete,
|
||||
name="account_group_delete",
|
||||
),
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from .accounts import *
|
||||
from .account_groups import *
|
||||
from .balance import *
|
||||
@@ -0,0 +1,94 @@
|
||||
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.urls import reverse
|
||||
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.accounts.forms import AccountGroupForm
|
||||
from apps.accounts.models import AccountGroup
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def account_groups_list(request):
|
||||
account_groups = AccountGroup.objects.all().order_by("id")
|
||||
return render(
|
||||
request, "account_groups/pages/list.html", {"account_groups": account_groups}
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def account_group_add(request, **kwargs):
|
||||
if request.method == "POST":
|
||||
form = AccountGroupForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Account Group added successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Location": reverse("account_groups_list"),
|
||||
"HX-Trigger": "hide_offcanvas, toasts",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = AccountGroupForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"account_groups/fragments/add.html",
|
||||
{"form": form},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def account_group_edit(request, pk):
|
||||
account_group = get_object_or_404(AccountGroup, id=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = AccountGroupForm(request.POST, instance=account_group)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Account Group updated successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Location": reverse("account_groups_list"),
|
||||
"HX-Trigger": "hide_offcanvas, toasts",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = AccountGroupForm(instance=account_group)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"account_groups/fragments/edit.html",
|
||||
{"form": form, "account_group": account_group},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def account_group_delete(request, pk):
|
||||
account_group = get_object_or_404(AccountGroup, id=pk)
|
||||
|
||||
account_group.delete()
|
||||
|
||||
messages.success(request, _("Account Group deleted successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={"HX-Location": reverse("account_groups_list")},
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
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.urls import reverse
|
||||
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.accounts.forms import AccountForm
|
||||
from apps.accounts.models import Account
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def accounts_list(request):
|
||||
accounts = Account.objects.all().order_by("id")
|
||||
return render(request, "accounts/pages/list.html", {"accounts": accounts})
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def account_add(request, **kwargs):
|
||||
if request.method == "POST":
|
||||
form = AccountForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Account added successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Location": reverse("accounts_list"),
|
||||
"HX-Trigger": "hide_offcanvas, toasts",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = AccountForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"accounts/fragments/add.html",
|
||||
{"form": form},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def account_edit(request, pk):
|
||||
account = get_object_or_404(Account, id=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = AccountForm(request.POST, instance=account)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Account updated successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Location": reverse("accounts_list"),
|
||||
"HX-Trigger": "hide_offcanvas, toasts",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = AccountForm(instance=account)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"accounts/fragments/edit.html",
|
||||
{"form": form, "account": account},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def account_delete(request, pk):
|
||||
account = get_object_or_404(Account, id=pk)
|
||||
|
||||
account.delete()
|
||||
|
||||
messages.success(request, _("Account deleted successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={"HX-Location": reverse("accounts_list")},
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import models
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.accounts.forms import AccountBalanceFormSet
|
||||
from apps.accounts.models import Account, Transaction
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
def account_reconciliation(request):
|
||||
def get_account_balance(account):
|
||||
income = Transaction.objects.filter(
|
||||
account=account, type=Transaction.Type.INCOME, is_paid=True
|
||||
).aggregate(total=models.Sum("amount"))["total"] or Decimal("0")
|
||||
|
||||
expense = Transaction.objects.filter(
|
||||
account=account, type=Transaction.Type.EXPENSE, is_paid=True
|
||||
).aggregate(total=models.Sum("amount"))["total"] or Decimal("0")
|
||||
|
||||
return income - expense
|
||||
|
||||
initial_data = [
|
||||
{
|
||||
"account_id": account.id,
|
||||
"account_name": account.name,
|
||||
"decimal_places": account.currency.decimal_places,
|
||||
"suffix": account.currency.suffix,
|
||||
"prefix": account.currency.prefix,
|
||||
"current_balance": get_account_balance(account),
|
||||
}
|
||||
for account in Account.objects.all().select_related("currency")
|
||||
]
|
||||
|
||||
if request.method == "POST":
|
||||
formset = AccountBalanceFormSet(request.POST, initial=initial_data)
|
||||
if formset.is_valid():
|
||||
with transaction.atomic():
|
||||
for form in formset:
|
||||
if form.is_valid():
|
||||
account_id = form.cleaned_data["account_id"]
|
||||
new_balance = form.cleaned_data["new_balance"]
|
||||
account = Account.objects.get(id=account_id)
|
||||
category = form.cleaned_data["category"]
|
||||
tags = form.cleaned_data.get("tags", [])
|
||||
|
||||
if new_balance is None:
|
||||
continue
|
||||
|
||||
current_balance = get_account_balance(account)
|
||||
difference = new_balance - current_balance
|
||||
|
||||
if difference != 0:
|
||||
new_transaction = Transaction.objects.create(
|
||||
account=account,
|
||||
type=(
|
||||
Transaction.Type.INCOME
|
||||
if difference > 0
|
||||
else Transaction.Type.EXPENSE
|
||||
),
|
||||
amount=abs(difference),
|
||||
date=timezone.localdate(timezone.now()),
|
||||
reference_date=timezone.localdate(
|
||||
timezone.now()
|
||||
).replace(day=1),
|
||||
description=_("Balance reconciliation"),
|
||||
is_paid=True,
|
||||
category=category,
|
||||
)
|
||||
|
||||
new_transaction.tags.set(tags)
|
||||
|
||||
messages.success(
|
||||
request, _("Account balances have been reconciled successfully.")
|
||||
)
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={"HX-Trigger": "transaction_updated, hide_offcanvas, toast"},
|
||||
)
|
||||
else:
|
||||
formset = AccountBalanceFormSet(initial=initial_data)
|
||||
|
||||
return render(
|
||||
request, "accounts/fragments/account_reconciliation.html", {"form": formset}
|
||||
)
|
||||
Reference in New Issue
Block a user