Files
WYGIWYH/app/apps/yearly_overview/views.py

463 lines
15 KiB
Python

from datetime import date
from decimal import Decimal
from django.contrib.auth.decorators import login_required
from django.db.models import Sum, F, Value, DecimalField, Q
from django.db.models.expressions import Case, When
from django.db.models.functions import TruncMonth, Coalesce, TruncYear
from django.http import Http404
from django.shortcuts import render, redirect
from django.utils import timezone
from apps.accounts.models import Account
from apps.currencies.models import Currency
from apps.currencies.utils.convert import convert
from apps.transactions.models import Transaction
from apps.common.decorators.htmx import only_htmx
@login_required
def index_by_currency(request):
now = timezone.localdate(timezone.now())
return redirect(to="yearly_overview_currency", year=now.year)
@login_required
def index_by_account(request):
now = timezone.localdate(timezone.now())
return redirect(to="yearly_overview_account", year=now.year)
@login_required
def index_yearly_overview_by_currency(request, year: int):
next_year = year + 1
previous_year = year - 1
month_options = range(1, 13)
currency_options = Currency.objects.filter(
accounts__transactions__date__year=year
).distinct()
return render(
request,
"yearly_overview/pages/overview_by_currency.html",
context={
"year": year,
"next_year": next_year,
"previous_year": previous_year,
"months": month_options,
"currencies": currency_options,
},
)
@only_htmx
@login_required
def yearly_overview_by_currency(request, year: int):
month = request.GET.get("month")
currency = request.GET.get("currency")
# Base query filter
filter_params = {"reference_date__year": year, "account__is_archived": False}
# Add month filter if provided
if month:
month = int(month)
if not 1 <= month <= 12:
raise Http404("Invalid month")
filter_params["reference_date__month"] = month
# Add currency filter if provided
if currency:
filter_params["account__currency_id"] = int(currency)
transactions = Transaction.objects.filter(**filter_params).exclude(
Q(category__mute=True) & ~Q(category=None)
)
if month:
date_trunc = TruncMonth("reference_date")
else:
date_trunc = TruncYear("reference_date")
monthly_data = (
transactions.annotate(month=date_trunc)
.values(
"month",
"account__currency__code",
"account__currency__prefix",
"account__currency__suffix",
"account__currency__decimal_places",
)
.annotate(
income_paid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.INCOME, is_paid=True, then=F("amount")
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
output_field=DecimalField(),
),
expense_paid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE,
is_paid=True,
then=F("amount"),
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
output_field=DecimalField(),
),
income_unpaid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.INCOME,
is_paid=False,
then=F("amount"),
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
output_field=DecimalField(),
),
expense_unpaid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE,
is_paid=False,
then=F("amount"),
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
output_field=DecimalField(),
),
)
.annotate(
balance_unpaid=F("income_unpaid") - F("expense_unpaid"),
balance_paid=F("income_paid") - F("expense_paid"),
balance_total=F("income_paid")
+ F("income_unpaid")
- F("expense_paid")
- F("expense_unpaid"),
)
.order_by("month", "account__currency__code")
)
# Create a dictionary to store the final result
result = {
"income_paid": [],
"expense_paid": [],
"income_unpaid": [],
"expense_unpaid": [],
"balance_unpaid": [],
"balance_paid": [],
"balance_total": [],
}
# Fill in the data
for entry in monthly_data:
currency_code = entry["account__currency__code"]
prefix = entry["account__currency__prefix"]
suffix = entry["account__currency__suffix"]
decimal_places = entry["account__currency__decimal_places"]
for field in [
"income_paid",
"expense_paid",
"income_unpaid",
"expense_unpaid",
"balance_unpaid",
"balance_paid",
"balance_total",
]:
if entry[field] != 0:
result[field].append(
{
"code": currency_code,
"prefix": prefix,
"suffix": suffix,
"decimal_places": decimal_places,
"amount": entry[field],
}
)
return render(
request,
"yearly_overview/fragments/currency_data.html",
context={
"year": year,
"totals": result,
},
)
@login_required
def index_yearly_overview_by_account(request, year: int):
next_year = year + 1
previous_year = year - 1
month_options = range(1, 13)
account_options = (
Account.objects.filter(is_archived=False, transactions__date__year=year)
.select_related("group")
.distinct()
.order_by("group__name", "name", "id")
)
return render(
request,
"yearly_overview/pages/overview_by_account.html",
context={
"year": year,
"next_year": next_year,
"previous_year": previous_year,
"months": month_options,
"accounts": account_options,
},
)
@only_htmx
@login_required
def yearly_overview_by_account(request, year: int):
month = request.GET.get("month")
account = request.GET.get("account")
# Base query filter
filter_params = {"reference_date__year": year, "account__is_archived": False}
# Add month filter if provided
if month:
month = int(month)
if not 1 <= month <= 12:
raise Http404("Invalid month")
filter_params["reference_date__month"] = month
# Add account filter if provided
if account:
filter_params["account_id"] = int(account)
transactions = Transaction.objects.filter(**filter_params)
# Use TruncYear if no month specified, otherwise use TruncMonth
date_trunc = TruncMonth("reference_date") if month else TruncYear("reference_date")
monthly_data = (
transactions.annotate(month=date_trunc)
.select_related(
"account__currency",
"account__exchange_currency",
)
.values(
"month",
"account__id",
"account__name",
"account__currency",
"account__exchange_currency",
"account__currency__code",
"account__currency__prefix",
"account__currency__suffix",
"account__currency__decimal_places",
"account__exchange_currency__code",
"account__exchange_currency__prefix",
"account__exchange_currency__suffix",
"account__exchange_currency__decimal_places",
)
.annotate(
income_paid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.INCOME, is_paid=True, then=F("amount")
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
output_field=DecimalField(),
),
expense_paid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE,
is_paid=True,
then=F("amount"),
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
output_field=DecimalField(),
),
income_unpaid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.INCOME,
is_paid=False,
then=F("amount"),
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
output_field=DecimalField(),
),
expense_unpaid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE,
is_paid=False,
then=F("amount"),
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
output_field=DecimalField(),
),
)
.annotate(
balance_unpaid=F("income_unpaid") - F("expense_unpaid"),
balance_paid=F("income_paid") - F("expense_paid"),
balance_total=F("income_paid")
+ F("income_unpaid")
- F("expense_paid")
- F("expense_unpaid"),
)
.order_by("month", "account__name")
)
# Determine which dates to include
if month:
all_months = [date(year, month, 1)]
else:
all_months = [date(year, 1, 1)] # Just one entry for the whole year
# Get all accounts with their currencies (filtered by account if specified)
accounts_filter = {}
if account:
accounts_filter["account__id"] = int(account)
accounts = (
transactions.filter(**accounts_filter)
.values(
"account__id",
"account__name",
"account__group__name",
"account__currency__code",
"account__currency__prefix",
"account__currency__suffix",
"account__currency__decimal_places",
"account__exchange_currency__code",
"account__exchange_currency__prefix",
"account__exchange_currency__suffix",
"account__exchange_currency__decimal_places",
)
.distinct()
.order_by("account__group__name", "account__name", "account__id")
)
# Get Currency objects for conversion
currencies = {currency.id: currency for currency in Currency.objects.all()}
result = {
month: {
account["account__id"]: {
"name": account["account__name"],
"group": account["account__group__name"],
"currency": {
"code": account["account__currency__code"],
"prefix": account["account__currency__prefix"],
"suffix": account["account__currency__suffix"],
"decimal_places": account["account__currency__decimal_places"],
},
"exchange_currency": (
{
"code": account["account__exchange_currency__code"],
"prefix": account["account__exchange_currency__prefix"],
"suffix": account["account__exchange_currency__suffix"],
"decimal_places": account[
"account__exchange_currency__decimal_places"
],
}
if account["account__exchange_currency__code"]
else None
),
"income_paid": Decimal("0"),
"expense_paid": Decimal("0"),
"income_unpaid": Decimal("0"),
"expense_unpaid": Decimal("0"),
"balance_unpaid": Decimal("0"),
"balance_paid": Decimal("0"),
"balance_total": Decimal("0"),
}
for account in accounts
}
for month in all_months
}
# Fill in the data
for entry in monthly_data:
month = entry["month"]
account_id = entry["account__id"]
for field in [
"income_paid",
"expense_paid",
"income_unpaid",
"expense_unpaid",
"balance_unpaid",
"balance_paid",
"balance_total",
]:
result[month][account_id][field] = entry[field]
if result[month][account_id]["exchange_currency"]:
from_currency = currencies[entry["account__currency"]]
to_currency = currencies[entry["account__exchange_currency"]]
if entry[field] > 0 or entry[field] < 0:
converted_amount, prefix, suffix, decimal_places = convert(
amount=entry[field],
from_currency=from_currency,
to_currency=to_currency,
)
if isinstance(converted_amount, Decimal):
result[month][account_id][
f"exchange_{field}"
] = converted_amount
else:
result[month][account_id][f"exchange_{field}"] = Decimal(0)
return render(
request,
"yearly_overview/fragments/account_data.html",
context={"year": year, "totals": result, "single": True if account else False},
)