feat: revamp Yearly Overview by account

This commit is contained in:
Herculino Trotta
2024-11-02 01:31:26 -03:00
parent 1c77774867
commit 11da277ba5
4 changed files with 291 additions and 211 deletions
+6 -1
View File
@@ -17,7 +17,12 @@ urlpatterns = [
), ),
path( path(
"yearly/account/<int:year>/", "yearly/account/<int:year>/",
views.yearly_overview_by_account, views.index_yearly_overview_by_account,
name="yearly_overview_account", name="yearly_overview_account",
), ),
path(
"yearly-overview/<int:year>/account/data/",
views.yearly_overview_by_account,
name="yearly_overview_account_data",
),
] ]
+36 -16
View File
@@ -9,6 +9,7 @@ from django.http import Http404
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.utils import timezone from django.utils import timezone
from apps.accounts.models import Account
from apps.currencies.models import Currency from apps.currencies.models import Currency
from apps.currencies.utils.convert import convert from apps.currencies.utils.convert import convert
from apps.transactions.models import Transaction from apps.transactions.models import Transaction
@@ -209,15 +210,30 @@ def yearly_overview_by_currency(request, year: int):
@login_required @login_required
def yearly_overview_by_account(request, year: int): def yearly_overview_by_account(request, year: int):
next_year = year + 1 month = request.GET.get("month")
previous_year = year - 1 account = request.GET.get("account")
transactions = Transaction.objects.filter( # Base query filter
reference_date__year=year, account__is_archived=False 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 = ( monthly_data = (
transactions.annotate(month=TruncMonth("reference_date")) transactions.annotate(month=date_trunc)
.select_related( .select_related(
"account__currency", "account__currency",
"account__exchange_currency", "account__exchange_currency",
@@ -308,11 +324,20 @@ def yearly_overview_by_account(request, year: int):
.order_by("month", "account__name") .order_by("month", "account__name")
) )
all_months = [date(year, month, 1) for month in range(1, 13)] # 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)
# Get all accounts with their currencies
accounts = ( accounts = (
transactions.values( transactions.filter(**accounts_filter)
.values(
"account__id", "account__id",
"account__name", "account__name",
"account__group__name", "account__group__name",
@@ -404,11 +429,6 @@ def yearly_overview_by_account(request, year: int):
return render( return render(
request, request,
"yearly_overview/pages/overview_by_account.html", "yearly_overview/fragments/account_data.html",
context={ context={"year": year, "totals": result, "single": True if account else False},
"year": year,
"next_year": next_year,
"previous_year": previous_year,
"totals": result,
},
) )
@@ -0,0 +1,181 @@
{% load i18n %}
<div class="row row-cols-1 g-4 mb-3">
{% for date, x in totals.items %}
<div class="col">
{% for id, account in x.items %}
{% if not single %}
<div class="tw-text-xl {% if not forloop.first %}mt-4 mb-3{% endif %}">
{% if account.group %}
<span class="badge text-bg-primary me-2">{{ account.group }}</span>{% endif %}{{ account.name }}
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace tw-text-green-400">
<c-amount.display
:amount="account.income_unpaid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_income_unpaid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_income_unpaid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div>
<div class="text-end font-monospace tw-text-red-400">
<c-amount.display
:amount="account.expense_unpaid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
</div>
{% if account.exchange_currency and account.exchange_expense_unpaid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_expense_unpaid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div
class="text-end font-monospace {% if account.balance_unpaid > 0 %}tw-text-green-400{% elif account.balance_unpaid < 0 %}tw-text-red-400{% endif %}">
<c-amount.display
:amount="account.balance_unpaid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_balance_unpaid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_balance_unpaid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<hr class="my-3">
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace tw-text-green-400">
<c-amount.display
:amount="account.income_paid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_income_paid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_income_paid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace tw-text-red-400">
<c-amount.display
:amount="account.expense_paid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_expense_paid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_expense_paid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div
class="text-end font-monospace {% if account.balance_paid > 0 %}tw-text-green-400{% elif account.balance_paid < 0 %}tw-text-red-400{% endif %}">
<c-amount.display
:amount="account.balance_paid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_balance_paid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_balance_paid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<div>
<hr class="my-3">
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'final total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div
class="text-end font-monospace {% if account.balance_total > 0 %}tw-text-green-400{% elif account.balance_total < 0 %}tw-text-red-400{% endif %}">
<c-amount.display
:amount="account.balance_total"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_balance_total %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_balance_total"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
</div>
{% empty %}
<c-msg.empty
title="{% translate "No information to display" %}"></c-msg.empty>
{% endfor %}
</div>
{% endfor %}
</div>
@@ -1,6 +1,4 @@
{% extends "layouts/base.html" %} {% extends "layouts/base.html" %}
{% load currency_display %}
{% load crispy_forms_tags %}
{% load i18n %} {% load i18n %}
{% load month_name %} {% load month_name %}
{% load static %} {% load static %}
@@ -90,201 +88,77 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row row-cols-1 g-4 mb-3"> <div class="row">
{% for date, x in totals.items %} <div class="col-lg-2">
<div class="col"> <div class="nav flex-column nav-pills" id="month-pills" role="tablist" aria-orientation="vertical">
<div class="card tw-relative h-100 shadow"> <input type="hidden" name="month" value="">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-yellow-300 tw-text-yellow-800 text-center <button class="nav-link active"
align-items-center d-flex justify-content-center rounded-2 tw-font-bold"> role="tab"
{{ forloop.counter }} data-bs-toggle="pill"
</div> hx-get="{% url 'yearly_overview_account_data' year=year %}"
<div class="card-body"> hx-target="#data-content"
<h5 class="tw-text-yellow-400 fw-bold mb-3">{{ date.month|month_name }}</h5> hx-trigger="click"
<div class="accordion accordion-flush" id="a-{{ forloop.counter }}-control"> hx-include="[name='account'], [name='month']"
{% for id, account in x.items %} hx-swap="innerHTML"
<div class="accordion-item"> onclick="document.querySelector('[name=month]').value = ''">
<h2 class="accordion-header"> {% translate 'Year' %}
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#{{ account.name|slugify }}-{{ forloop.parentloop.counter }}" aria-expanded="false" aria-controls="{{ account.name|slugify }}-{{ forloop.counter }}"> </button>
{% if account.group %}<span class="badge text-bg-primary me-2">{{ account.group }}</span>{% endif %}{{ account.name }} {% for month in months %}
</button> <button class="nav-link"
</h2> role="tab"
<div id="{{ account.name|slugify }}-{{ forloop.parentloop.counter }}" class="accordion-collapse collapse"> data-bs-toggle="pill"
<div class="accordion-body"> hx-get="{% url 'yearly_overview_account_data' year=year %}"
<div class="row"> hx-target="#data-content"
<div class="col-12 col-lg-6"> hx-trigger="click"
<div class="d-flex justify-content-between align-items-baseline mt-2"> hx-include="[name='account'], [name='month']"
<div class="text-end font-monospace"> hx-swap="innerHTML"
<div class="tw-text-gray-400">{% translate 'projected income' %}</div> onclick="document.querySelector('[name=month]').value = '{{ month }}'">
</div> {{ month|month_name }}
<div class="dotted-line flex-grow-1"></div> </button>
<div class="text-end font-monospace tw-text-green-400">
<c-amount.display
:amount="account.income_unpaid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_income_unpaid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_income_unpaid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div>
<div class="text-end font-monospace tw-text-red-400">
<c-amount.display
:amount="account.expense_unpaid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
</div>
{% if account.exchange_currency and account.exchange_expense_unpaid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_expense_unpaid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace {% if account.balance_unpaid > 0 %}tw-text-green-400{% elif account.balance_unpaid < 0 %}tw-text-red-400{% endif %}">
<c-amount.display
:amount="account.balance_unpaid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_balance_unpaid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_balance_unpaid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
</div>
<hr class="my-3 d-block d-lg-none">
<div class="col-12 col-lg-6">
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace tw-text-green-400">
<c-amount.display
:amount="account.income_paid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_income_paid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_income_paid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace tw-text-red-400">
<c-amount.display
:amount="account.expense_paid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_expense_paid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_expense_paid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace {% if account.balance_paid > 0 %}tw-text-green-400{% elif account.balance_paid < 0 %}tw-text-red-400{% endif %}">
<c-amount.display
:amount="account.balance_paid"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_balance_paid %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_balance_paid"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
</div>
<hr class="my-3">
<div>
<div class="d-flex justify-content-between align-items-baseline">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'final total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace {% if account.balance_total > 0 %}tw-text-green-400{% elif account.balance_total < 0 %}tw-text-red-400{% endif %}">
<c-amount.display
:amount="account.balance_total"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
</div>
{% if account.exchange_currency and account.exchange_balance_total %}
<div class="text-end font-monospace tw-text-gray-500">
<c-amount.display
:amount="account.exchange_balance_total"
:prefix="account.exchange_currency.prefix"
:suffix="account.exchange_currency.suffix"
:decimal_places="account.exchange_currency.decimal_places"></c-amount.display>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %} {% endfor %}
</div>
</div> </div>
</div>
</div> </div>
{% endfor %}
<hr class="my-4 d-block d-lg-none">
<div class="col-lg-3">
<div class="nav flex-column nav-pills" id="currency-pills" role="tablist" aria-orientation="vertical">
<input type="hidden" name="account" value="">
<button class="nav-link active"
role="tab"
data-bs-toggle="pill"
hx-get="{% url 'yearly_overview_account_data' year=year %}"
hx-target="#data-content"
hx-trigger="click"
hx-include="[name='account'], [name='month']"
hx-swap="innerHTML"
onclick="document.querySelector('[name=account]').value = ''">
{% translate 'All' %}
</button>
{% for account in accounts %}
<button class="nav-link"
role="tab"
data-bs-toggle="pill"
hx-get="{% url 'yearly_overview_account_data' year=year %}"
hx-target="#data-content"
hx-trigger="click"
hx-include="[name='account'], [name='month']"
hx-swap="innerHTML"
onclick="document.querySelector('[name=account]').value = '{{ account.id }}'">
<span class="badge text-bg-primary me-2">{{ account.group.name }}</span>{{ account.name }}
</button>
{% endfor %}
</div>
</div>
<div class="col-lg-7">
<div id="data-content"
class="show-loading"
hx-get="{% url 'yearly_overview_account_data' year=year %}"
hx-trigger="load"
hx-include="[name='account'], [name='month']"
hx-swap="innerHTML">
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}