Files
WYGIWYH/app/apps/transactions/views.py
2024-09-27 17:56:09 -03:00

420 lines
13 KiB
Python

import datetime
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.db.models import Sum, Q, F, Case, When, DecimalField
from django.db.models.functions import Coalesce
from decimal import Decimal
from apps.transactions.forms import TransactionForm, TransferForm
from apps.transactions.models import Transaction
from apps.common.functions.dates import remaining_days_in_month
@login_required
def index(request):
now = timezone.localdate(timezone.now())
return redirect(to="transactions_overview", month=now.month, year=now.year)
@login_required
def transactions_overview(request, month: int, year: int):
from django.utils.formats import get_format
from django.utils.translation import get_language
current_language = get_language()
thousand_separator = get_format("THOUSAND_SEPARATOR")
print(thousand_separator, current_language)
if month < 1 or month > 12:
from django.http import Http404
raise Http404("Month is out of range")
next_month = 1 if month == 12 else month + 1
next_year = year + 1 if next_month == 1 and month == 12 else year
previous_month = 12 if month == 1 else month - 1
previous_year = year - 1 if previous_month == 12 and month == 1 else year
return render(
request,
"transactions/overview.html",
context={
"month": month,
"year": year,
"next_month": next_month,
"next_year": next_year,
"previous_month": previous_month,
"previous_year": previous_year,
},
)
@login_required
def transactions_list(request, month: int, year: int):
from django.db.models.functions import ExtractMonth, ExtractYear
queryset = (
Transaction.objects.annotate(
month=ExtractMonth("reference_date"), year=ExtractYear("reference_date")
)
.values("month", "year")
.distinct()
.order_by("year", "month")
)
# print(queryset)
transactions = (
Transaction.objects.all()
.filter(
reference_date__year=year,
reference_date__month=month,
)
.order_by("date", "id")
.select_related()
)
return render(
request,
"transactions/fragments/list.html",
context={"transactions": transactions},
)
@login_required
def transaction_add(request, **kwargs):
month = int(request.GET.get("month", timezone.localdate(timezone.now()).month))
year = int(request.GET.get("year", timezone.localdate(timezone.now()).year))
transaction_type = Transaction.Type(request.GET.get("type", "IN"))
now = timezone.localdate(timezone.now())
expected_date = datetime.datetime(
day=now.day if month == now.month and year == now.year else 1,
month=month,
year=year,
).date()
if request.method == "POST":
form = TransactionForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, _("Transaction added successfully!"))
# redirect to a new URL:
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, hide_offcanvas, toast"},
)
else:
form = TransactionForm(
initial={
"reference_date": expected_date,
"date": expected_date,
"type": transaction_type,
}
)
return render(
request,
"transactions/fragments/add.html",
{"form": form},
)
@login_required
def transaction_edit(request, transaction_id, **kwargs):
transaction = get_object_or_404(Transaction, id=transaction_id)
if request.method == "POST":
form = TransactionForm(request.POST, instance=transaction)
if form.is_valid():
form.save()
messages.success(request, _("Transaction updated successfully!"))
# redirect to a new URL:
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, hide_offcanvas, toast"},
)
else:
form = TransactionForm(instance=transaction)
return render(
request,
"transactions/fragments/edit.html",
{"form": form, "transaction": transaction},
)
@login_required
def transaction_delete(request, transaction_id, **kwargs):
transaction = get_object_or_404(Transaction, id=transaction_id)
transaction.delete()
messages.success(request, _("Transaction deleted successfully!"))
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, toast"},
)
@login_required
def transactions_transfer(request):
month = int(request.GET.get("month", timezone.localdate(timezone.now()).month))
year = int(request.GET.get("year", timezone.localdate(timezone.now()).year))
now = timezone.localdate(timezone.now())
expected_date = datetime.datetime(
day=now.day if month == now.month and year == now.year else 1,
month=month,
year=year,
).date()
if request.method == "POST":
form = TransferForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, _("Transfer added successfully."))
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, toast, hide_offcanvas"},
)
else:
form = TransferForm(
initial={
"reference_date": expected_date,
"date": expected_date,
}
)
return render(request, "transactions/fragments/transfer.html", {"form": form})
@login_required
def transaction_pay(request, transaction_id):
transaction = get_object_or_404(Transaction, pk=transaction_id)
new_is_paid = False if transaction.is_paid else True
transaction.is_paid = new_is_paid
transaction.save()
response = render(
request,
"transactions/fragments/item.html",
context={"transaction": transaction},
)
response.headers["HX-Trigger"] = (
f'{"paid" if new_is_paid else "unpaid"}, transaction_updated'
)
return response
@login_required
def month_year_picker(request):
current_month = int(
request.GET.get("month", timezone.localdate(timezone.now()).month)
)
current_year = int(request.GET.get("year", timezone.localdate(timezone.now()).year))
available_years = Transaction.objects.dates(
"reference_date", "year", order="ASC"
) or [datetime.datetime(current_year, current_month, 1)]
return render(
request,
"transactions/fragments/month_year_picker.html",
{
"available_years": available_years,
"months": range(1, 13),
"current_month": current_month,
"current_year": current_year,
},
)
@login_required
def monthly_summary(request, month: int, year: int):
# Helper function to calculate sums for different transaction types
def calculate_sum(transaction_type, is_paid):
return (
base_queryset.filter(type=transaction_type, is_paid=is_paid)
.values(
"account__currency__name",
"account__currency__suffix",
"account__currency__prefix",
"account__currency__decimal_places",
)
.annotate(total=Sum("amount"))
.order_by("account__currency__name")
)
# Helper function to format currency sums
def format_currency_sum(queryset):
return [
{
"currency": item["account__currency__name"],
"suffix": item["account__currency__suffix"],
"prefix": item["account__currency__prefix"],
"decimal_places": item["account__currency__decimal_places"],
"amount": round(
item["total"], item["account__currency__decimal_places"]
),
}
for item in queryset
]
# Calculate totals
def calculate_total(income, expenses):
totals = {}
# Process income
for item in income:
currency = item["account__currency__name"]
totals[currency] = totals.get(currency, Decimal("0")) + item["total"]
# Subtract expenses
for item in expenses:
currency = item["account__currency__name"]
totals[currency] = totals.get(currency, Decimal("0")) - item["total"]
return [
{
"currency": currency,
"suffix": next(
(
item["account__currency__suffix"]
for item in list(income) + list(expenses)
if item["account__currency__name"] == currency
),
"",
),
"prefix": next(
(
item["account__currency__prefix"]
for item in list(income) + list(expenses)
if item["account__currency__name"] == currency
),
"",
),
"decimal_places": next(
(
item["account__currency__decimal_places"]
for item in list(income) + list(expenses)
if item["account__currency__name"] == currency
),
2,
),
"amount": round(
amount,
next(
(
item["account__currency__decimal_places"]
for item in list(income) + list(expenses)
if item["account__currency__name"] == currency
),
2,
),
),
}
for currency, amount in totals.items()
]
# Calculate total final
def sum_totals(total1, total2):
totals = {}
for item in total1 + total2:
currency = item["currency"]
totals[currency] = totals.get(currency, Decimal("0")) + item["amount"]
return [
{
"currency": currency,
"suffix": next(
item["suffix"]
for item in total1 + total2
if item["currency"] == currency
),
"prefix": next(
item["prefix"]
for item in total1 + total2
if item["currency"] == currency
),
"decimal_places": next(
item["decimal_places"]
for item in total1 + total2
if item["currency"] == currency
),
"amount": round(
amount,
next(
item["decimal_places"]
for item in total1 + total2
if item["currency"] == currency
),
),
}
for currency, amount in totals.items()
]
# Base queryset with all required filters
base_queryset = Transaction.objects.filter(
reference_date__year=year, reference_date__month=month, account__is_asset=False
).exclude(Q(category__mute=True) & ~Q(category=None))
# Calculate sums for different transaction types
paid_income = calculate_sum(Transaction.Type.INCOME, True)
projected_income = calculate_sum(Transaction.Type.INCOME, False)
paid_expenses = calculate_sum(Transaction.Type.EXPENSE, True)
projected_expenses = calculate_sum(Transaction.Type.EXPENSE, False)
total_current = calculate_total(paid_income, paid_expenses)
total_projected = calculate_total(projected_income, projected_expenses)
total_final = sum_totals(total_current, total_projected)
# Calculate daily spending allowance
remaining_days = remaining_days_in_month(
month=month, year=year, current_date=timezone.localdate(timezone.now())
)
daily_spending_allowance = [
{
"currency": item["currency"],
"suffix": item["suffix"],
"prefix": item["prefix"],
"decimal_places": item["decimal_places"],
"amount": (
amount
if (amount := item["amount"] / remaining_days) > 0
else Decimal("0")
),
}
for item in total_final
]
# Construct the response dictionary
response_data = {
"paid_income": format_currency_sum(paid_income),
"projected_income": format_currency_sum(projected_income),
"paid_expenses": format_currency_sum(paid_expenses),
"projected_expenses": format_currency_sum(projected_expenses),
"total_current": total_current,
"total_projected": total_projected,
"total_final": total_final,
"daily_spending_allowance": daily_spending_allowance,
}
return render(
request,
"transactions/fragments/monthly_summary.html",
context={
"totals": response_data,
},
)