mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-25 10:08:36 +02:00
feat(insights): new month by month insight
This commit is contained in:
@@ -54,4 +54,9 @@ urlpatterns = [
|
|||||||
views.year_by_year,
|
views.year_by_year,
|
||||||
name="insights_year_by_year",
|
name="insights_year_by_year",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"insights/month-by-month/",
|
||||||
|
views.month_by_month,
|
||||||
|
name="insights_month_by_month",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
316
app/apps/insights/utils/month_by_month.py
Normal file
316
app/apps/insights/utils/month_by_month.py
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import Sum, Case, When, Value
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from apps.currencies.models import Currency
|
||||||
|
from apps.currencies.utils.convert import convert
|
||||||
|
from apps.transactions.models import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
def get_month_by_month_data(year=None, group_by="categories"):
|
||||||
|
"""
|
||||||
|
Aggregate transaction totals by month for a specific year, grouped by categories, tags, or entities.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
year: The year to filter transactions (defaults to current year)
|
||||||
|
group_by: One of "categories", "tags", or "entities"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"year": 2025,
|
||||||
|
"available_years": [2025, 2024, ...],
|
||||||
|
"months": [1, 2, 3, ..., 12],
|
||||||
|
"items": {
|
||||||
|
item_id: {
|
||||||
|
"name": "Item Name",
|
||||||
|
"month_totals": {
|
||||||
|
1: {"currencies": {...}},
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"total": {"currencies": {...}}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"month_totals": {...},
|
||||||
|
"grand_total": {"currencies": {...}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if year is None:
|
||||||
|
year = timezone.localdate(timezone.now()).year
|
||||||
|
|
||||||
|
# Base queryset - all paid transactions, non-muted
|
||||||
|
transactions = Transaction.objects.filter(
|
||||||
|
is_paid=True,
|
||||||
|
account__is_archived=False,
|
||||||
|
).exclude(account__currency__is_archived=True)
|
||||||
|
|
||||||
|
# Get available years for the selector
|
||||||
|
available_years = list(
|
||||||
|
transactions.values_list("reference_date__year", flat=True)
|
||||||
|
.distinct()
|
||||||
|
.order_by("-reference_date__year")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter by the selected year
|
||||||
|
transactions = transactions.filter(reference_date__year=year)
|
||||||
|
|
||||||
|
# Define grouping fields based on group_by parameter
|
||||||
|
if group_by == "tags":
|
||||||
|
group_field = "tags"
|
||||||
|
name_field = "tags__name"
|
||||||
|
elif group_by == "entities":
|
||||||
|
group_field = "entities"
|
||||||
|
name_field = "entities__name"
|
||||||
|
else: # Default to categories
|
||||||
|
group_field = "category"
|
||||||
|
name_field = "category__name"
|
||||||
|
|
||||||
|
# Months 1-12
|
||||||
|
months = list(range(1, 13))
|
||||||
|
|
||||||
|
if not available_years:
|
||||||
|
return {
|
||||||
|
"year": year,
|
||||||
|
"available_years": [],
|
||||||
|
"months": months,
|
||||||
|
"items": {},
|
||||||
|
"month_totals": {},
|
||||||
|
"grand_total": {"currencies": {}},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Aggregate by group, month, and currency
|
||||||
|
metrics = (
|
||||||
|
transactions.values(
|
||||||
|
group_field,
|
||||||
|
name_field,
|
||||||
|
"reference_date__month",
|
||||||
|
"account__currency",
|
||||||
|
"account__currency__code",
|
||||||
|
"account__currency__name",
|
||||||
|
"account__currency__decimal_places",
|
||||||
|
"account__currency__prefix",
|
||||||
|
"account__currency__suffix",
|
||||||
|
"account__currency__exchange_currency",
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
expense_total=Coalesce(
|
||||||
|
Sum(
|
||||||
|
Case(
|
||||||
|
When(type=Transaction.Type.EXPENSE, then="amount"),
|
||||||
|
default=Value(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Decimal("0"),
|
||||||
|
),
|
||||||
|
income_total=Coalesce(
|
||||||
|
Sum(
|
||||||
|
Case(
|
||||||
|
When(type=Transaction.Type.INCOME, then="amount"),
|
||||||
|
default=Value(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Decimal("0"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.order_by(name_field, "reference_date__month")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build result structure
|
||||||
|
result = {
|
||||||
|
"year": year,
|
||||||
|
"available_years": available_years,
|
||||||
|
"months": months,
|
||||||
|
"items": OrderedDict(),
|
||||||
|
"month_totals": {},
|
||||||
|
"grand_total": {"currencies": {}},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Store currency info for later use in totals
|
||||||
|
currency_info = {}
|
||||||
|
|
||||||
|
for metric in metrics:
|
||||||
|
item_id = metric[group_field]
|
||||||
|
item_name = metric[name_field]
|
||||||
|
month = metric["reference_date__month"]
|
||||||
|
currency_id = metric["account__currency"]
|
||||||
|
|
||||||
|
# Use a consistent key for None (uncategorized/untagged/no entity)
|
||||||
|
item_key = item_id if item_id is not None else "__none__"
|
||||||
|
|
||||||
|
if item_key not in result["items"]:
|
||||||
|
result["items"][item_key] = {
|
||||||
|
"name": item_name,
|
||||||
|
"month_totals": {},
|
||||||
|
"total": {"currencies": {}},
|
||||||
|
}
|
||||||
|
|
||||||
|
if month not in result["items"][item_key]["month_totals"]:
|
||||||
|
result["items"][item_key]["month_totals"][month] = {"currencies": {}}
|
||||||
|
|
||||||
|
# Calculate final total (income - expense)
|
||||||
|
final_total = metric["income_total"] - metric["expense_total"]
|
||||||
|
|
||||||
|
# Store currency info for totals calculation
|
||||||
|
if currency_id not in currency_info:
|
||||||
|
currency_info[currency_id] = {
|
||||||
|
"code": metric["account__currency__code"],
|
||||||
|
"name": metric["account__currency__name"],
|
||||||
|
"decimal_places": metric["account__currency__decimal_places"],
|
||||||
|
"prefix": metric["account__currency__prefix"],
|
||||||
|
"suffix": metric["account__currency__suffix"],
|
||||||
|
"exchange_currency_id": metric["account__currency__exchange_currency"],
|
||||||
|
}
|
||||||
|
|
||||||
|
currency_data = {
|
||||||
|
"currency": {
|
||||||
|
"code": metric["account__currency__code"],
|
||||||
|
"name": metric["account__currency__name"],
|
||||||
|
"decimal_places": metric["account__currency__decimal_places"],
|
||||||
|
"prefix": metric["account__currency__prefix"],
|
||||||
|
"suffix": metric["account__currency__suffix"],
|
||||||
|
},
|
||||||
|
"final_total": final_total,
|
||||||
|
"income_total": metric["income_total"],
|
||||||
|
"expense_total": metric["expense_total"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle currency conversion if exchange currency is set
|
||||||
|
if metric["account__currency__exchange_currency"]:
|
||||||
|
from_currency = Currency.objects.get(id=currency_id)
|
||||||
|
exchange_currency = Currency.objects.get(
|
||||||
|
id=metric["account__currency__exchange_currency"]
|
||||||
|
)
|
||||||
|
|
||||||
|
converted_amount, prefix, suffix, decimal_places = convert(
|
||||||
|
amount=final_total,
|
||||||
|
from_currency=from_currency,
|
||||||
|
to_currency=exchange_currency,
|
||||||
|
)
|
||||||
|
|
||||||
|
if converted_amount is not None:
|
||||||
|
currency_data["exchanged"] = {
|
||||||
|
"final_total": converted_amount,
|
||||||
|
"currency": {
|
||||||
|
"prefix": prefix,
|
||||||
|
"suffix": suffix,
|
||||||
|
"decimal_places": decimal_places,
|
||||||
|
"code": exchange_currency.code,
|
||||||
|
"name": exchange_currency.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result["items"][item_key]["month_totals"][month]["currencies"][currency_id] = (
|
||||||
|
currency_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Accumulate item total (across all months for this item)
|
||||||
|
if currency_id not in result["items"][item_key]["total"]["currencies"]:
|
||||||
|
result["items"][item_key]["total"]["currencies"][currency_id] = {
|
||||||
|
"currency": currency_data["currency"].copy(),
|
||||||
|
"final_total": Decimal("0"),
|
||||||
|
}
|
||||||
|
result["items"][item_key]["total"]["currencies"][currency_id][
|
||||||
|
"final_total"
|
||||||
|
] += final_total
|
||||||
|
|
||||||
|
# Accumulate month total (across all items for this month)
|
||||||
|
if month not in result["month_totals"]:
|
||||||
|
result["month_totals"][month] = {"currencies": {}}
|
||||||
|
if currency_id not in result["month_totals"][month]["currencies"]:
|
||||||
|
result["month_totals"][month]["currencies"][currency_id] = {
|
||||||
|
"currency": currency_data["currency"].copy(),
|
||||||
|
"final_total": Decimal("0"),
|
||||||
|
}
|
||||||
|
result["month_totals"][month]["currencies"][currency_id]["final_total"] += (
|
||||||
|
final_total
|
||||||
|
)
|
||||||
|
|
||||||
|
# Accumulate grand total
|
||||||
|
if currency_id not in result["grand_total"]["currencies"]:
|
||||||
|
result["grand_total"]["currencies"][currency_id] = {
|
||||||
|
"currency": currency_data["currency"].copy(),
|
||||||
|
"final_total": Decimal("0"),
|
||||||
|
}
|
||||||
|
result["grand_total"]["currencies"][currency_id]["final_total"] += final_total
|
||||||
|
|
||||||
|
# Add currency conversion for item totals
|
||||||
|
for item_key, item_data in result["items"].items():
|
||||||
|
for currency_id, total_data in item_data["total"]["currencies"].items():
|
||||||
|
if currency_info[currency_id]["exchange_currency_id"]:
|
||||||
|
from_currency = Currency.objects.get(id=currency_id)
|
||||||
|
exchange_currency = Currency.objects.get(
|
||||||
|
id=currency_info[currency_id]["exchange_currency_id"]
|
||||||
|
)
|
||||||
|
converted_amount, prefix, suffix, decimal_places = convert(
|
||||||
|
amount=total_data["final_total"],
|
||||||
|
from_currency=from_currency,
|
||||||
|
to_currency=exchange_currency,
|
||||||
|
)
|
||||||
|
if converted_amount is not None:
|
||||||
|
total_data["exchanged"] = {
|
||||||
|
"final_total": converted_amount,
|
||||||
|
"currency": {
|
||||||
|
"prefix": prefix,
|
||||||
|
"suffix": suffix,
|
||||||
|
"decimal_places": decimal_places,
|
||||||
|
"code": exchange_currency.code,
|
||||||
|
"name": exchange_currency.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add currency conversion for month totals
|
||||||
|
for month, month_data in result["month_totals"].items():
|
||||||
|
for currency_id, total_data in month_data["currencies"].items():
|
||||||
|
if currency_info[currency_id]["exchange_currency_id"]:
|
||||||
|
from_currency = Currency.objects.get(id=currency_id)
|
||||||
|
exchange_currency = Currency.objects.get(
|
||||||
|
id=currency_info[currency_id]["exchange_currency_id"]
|
||||||
|
)
|
||||||
|
converted_amount, prefix, suffix, decimal_places = convert(
|
||||||
|
amount=total_data["final_total"],
|
||||||
|
from_currency=from_currency,
|
||||||
|
to_currency=exchange_currency,
|
||||||
|
)
|
||||||
|
if converted_amount is not None:
|
||||||
|
total_data["exchanged"] = {
|
||||||
|
"final_total": converted_amount,
|
||||||
|
"currency": {
|
||||||
|
"prefix": prefix,
|
||||||
|
"suffix": suffix,
|
||||||
|
"decimal_places": decimal_places,
|
||||||
|
"code": exchange_currency.code,
|
||||||
|
"name": exchange_currency.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add currency conversion for grand total
|
||||||
|
for currency_id, total_data in result["grand_total"]["currencies"].items():
|
||||||
|
if currency_info[currency_id]["exchange_currency_id"]:
|
||||||
|
from_currency = Currency.objects.get(id=currency_id)
|
||||||
|
exchange_currency = Currency.objects.get(
|
||||||
|
id=currency_info[currency_id]["exchange_currency_id"]
|
||||||
|
)
|
||||||
|
converted_amount, prefix, suffix, decimal_places = convert(
|
||||||
|
amount=total_data["final_total"],
|
||||||
|
from_currency=from_currency,
|
||||||
|
to_currency=exchange_currency,
|
||||||
|
)
|
||||||
|
if converted_amount is not None:
|
||||||
|
total_data["exchanged"] = {
|
||||||
|
"final_total": converted_amount,
|
||||||
|
"currency": {
|
||||||
|
"prefix": prefix,
|
||||||
|
"suffix": suffix,
|
||||||
|
"decimal_places": decimal_places,
|
||||||
|
"code": exchange_currency.code,
|
||||||
|
"name": exchange_currency.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -26,6 +26,8 @@ from apps.insights.utils.sankey import (
|
|||||||
generate_sankey_data_by_currency,
|
generate_sankey_data_by_currency,
|
||||||
)
|
)
|
||||||
from apps.insights.utils.transactions import get_transactions
|
from apps.insights.utils.transactions import get_transactions
|
||||||
|
from apps.insights.utils.year_by_year import get_year_by_year_data
|
||||||
|
from apps.insights.utils.month_by_month import get_month_by_month_data
|
||||||
from apps.transactions.models import TransactionCategory, Transaction
|
from apps.transactions.models import TransactionCategory, Transaction
|
||||||
from apps.transactions.utils.calculations import calculate_currency_totals
|
from apps.transactions.utils.calculations import calculate_currency_totals
|
||||||
|
|
||||||
@@ -333,3 +335,44 @@ def year_by_year(request):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def month_by_month(request):
|
||||||
|
# Handle year selection
|
||||||
|
if "year" in request.GET:
|
||||||
|
try:
|
||||||
|
year = int(request.GET["year"])
|
||||||
|
request.session["insights_month_by_month_year"] = year
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
year = request.session.get(
|
||||||
|
"insights_month_by_month_year", timezone.localdate(timezone.now()).year
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
year = request.session.get(
|
||||||
|
"insights_month_by_month_year", timezone.localdate(timezone.now()).year
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle group_by selection
|
||||||
|
if "group_by" in request.GET:
|
||||||
|
group_by = request.GET["group_by"]
|
||||||
|
request.session["insights_month_by_month_group_by"] = group_by
|
||||||
|
else:
|
||||||
|
group_by = request.session.get("insights_month_by_month_group_by", "categories")
|
||||||
|
|
||||||
|
# Validate group_by value
|
||||||
|
if group_by not in ("categories", "tags", "entities"):
|
||||||
|
group_by = "categories"
|
||||||
|
|
||||||
|
data = get_month_by_month_data(year=year, group_by=group_by)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"insights/fragments/month_by_month.html",
|
||||||
|
{
|
||||||
|
"data": data,
|
||||||
|
"group_by": group_by,
|
||||||
|
"selected_year": year,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
254
app/templates/insights/fragments/month_by_month.html
Normal file
254
app/templates/insights/fragments/month_by_month.html
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div hx-get="{% url 'insights_month_by_month' %}" hx-trigger="updated from:window" class="show-loading"
|
||||||
|
hx-swap="outerHTML" hx-include="#year-selector, #group-by-selector-month">
|
||||||
|
|
||||||
|
{# Hidden input to hold the year value #}
|
||||||
|
<input type="hidden" name="year" id="year-selector" value="{{ selected_year }}" _="on change trigger updated">
|
||||||
|
|
||||||
|
{# Tabs for Categories/Tags/Entities #}
|
||||||
|
<div class="h-full text-center mb-4">
|
||||||
|
<div class="tabs tabs-box mx-auto w-fit" role="group" id="group-by-selector-month" _="on change trigger updated">
|
||||||
|
<label class="tab">
|
||||||
|
<input type="radio"
|
||||||
|
name="group_by"
|
||||||
|
id="categories-view-month"
|
||||||
|
autocomplete="off"
|
||||||
|
value="categories"
|
||||||
|
aria-label="{% trans 'Categories' %}"
|
||||||
|
{% if group_by == "categories" %}checked{% endif %}>
|
||||||
|
<i class="fa-solid fa-icons fa-fw me-2"></i>
|
||||||
|
{% trans 'Categories' %}
|
||||||
|
</label>
|
||||||
|
<label class="tab">
|
||||||
|
<input type="radio"
|
||||||
|
name="group_by"
|
||||||
|
id="tags-view-month"
|
||||||
|
autocomplete="off"
|
||||||
|
value="tags"
|
||||||
|
aria-label="{% trans 'Tags' %}"
|
||||||
|
{% if group_by == "tags" %}checked{% endif %}>
|
||||||
|
<i class="fa-solid fa-hashtag fa-fw me-2"></i>
|
||||||
|
{% trans 'Tags' %}
|
||||||
|
</label>
|
||||||
|
<label class="tab">
|
||||||
|
<input type="radio"
|
||||||
|
name="group_by"
|
||||||
|
id="entities-view-month"
|
||||||
|
autocomplete="off"
|
||||||
|
value="entities"
|
||||||
|
aria-label="{% trans 'Entities' %}"
|
||||||
|
{% if group_by == "entities" %}checked{% endif %}>
|
||||||
|
<i class="fa-solid fa-user-group fa-fw me-2"></i>
|
||||||
|
{% trans 'Entities' %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if data.items %}
|
||||||
|
<div class="card bg-base-100 card-border">
|
||||||
|
<div class="card-body">
|
||||||
|
{# Year dropdown - left aligned #}
|
||||||
|
{% if data.available_years %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-ghost" type="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-calendar fa-fw me-1"></i>
|
||||||
|
{{ selected_year }}
|
||||||
|
<i class="fa-solid fa-chevron-down fa-fw ms-1"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu menu">
|
||||||
|
{% for year in data.available_years %}
|
||||||
|
<li>
|
||||||
|
<button class="{% if year == selected_year %}menu-active{% endif %}" type="button"
|
||||||
|
_="on click remove .menu-active from <li > button/> in the closest <ul/>
|
||||||
|
then add .menu-active to me
|
||||||
|
then set the value of #year-selector to '{{ year }}'
|
||||||
|
then trigger change on #year-selector">
|
||||||
|
{{ year }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="sticky left-0 bg-base-100 z-10">
|
||||||
|
{% if group_by == "categories" %}
|
||||||
|
{% trans 'Category' %}
|
||||||
|
{% elif group_by == "tags" %}
|
||||||
|
{% trans 'Tag' %}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'Entity' %}
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="text-center font-bold">{% trans 'Total' %}</th>
|
||||||
|
{% for month in data.months %}
|
||||||
|
<th scope="col" class="text-center">
|
||||||
|
{% if month == 1 %}{% trans 'Jan' %}
|
||||||
|
{% elif month == 2 %}{% trans 'Feb' %}
|
||||||
|
{% elif month == 3 %}{% trans 'Mar' %}
|
||||||
|
{% elif month == 4 %}{% trans 'Apr' %}
|
||||||
|
{% elif month == 5 %}{% trans 'May' %}
|
||||||
|
{% elif month == 6 %}{% trans 'Jun' %}
|
||||||
|
{% elif month == 7 %}{% trans 'Jul' %}
|
||||||
|
{% elif month == 8 %}{% trans 'Aug' %}
|
||||||
|
{% elif month == 9 %}{% trans 'Sep' %}
|
||||||
|
{% elif month == 10 %}{% trans 'Oct' %}
|
||||||
|
{% elif month == 11 %}{% trans 'Nov' %}
|
||||||
|
{% elif month == 12 %}{% trans 'Dec' %}
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item_id, item in data.items.items %}
|
||||||
|
<tr>
|
||||||
|
<th class="text-nowrap sticky left-0 bg-base-100 z-10">
|
||||||
|
{% if item.name %}
|
||||||
|
{{ item.name }}
|
||||||
|
{% else %}
|
||||||
|
{% if group_by == "categories" %}
|
||||||
|
{% trans 'Uncategorized' %}
|
||||||
|
{% elif group_by == "tags" %}
|
||||||
|
{% trans 'Untagged' %}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'No entity' %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
{# Total column for this item #}
|
||||||
|
<td class="text-nowrap text-center font-semibold bg-base-200">
|
||||||
|
{% for currency_id, currency_data in item.total.currencies.items %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency_data.final_total"
|
||||||
|
:prefix="currency_data.currency.prefix"
|
||||||
|
:suffix="currency_data.currency.suffix"
|
||||||
|
:decimal_places="currency_data.currency.decimal_places"
|
||||||
|
color="{% if currency_data.final_total < 0 %}red{% elif currency_data.final_total > 0 %}green{% endif %}"></c-amount.display>
|
||||||
|
{% if currency_data.exchanged %}
|
||||||
|
<div class="text-xs text-base-content/60">
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency_data.exchanged.final_total"
|
||||||
|
:prefix="currency_data.exchanged.currency.prefix"
|
||||||
|
:suffix="currency_data.exchanged.currency.suffix"
|
||||||
|
:decimal_places="currency_data.exchanged.currency.decimal_places"
|
||||||
|
color="{% if currency_data.exchanged.final_total < 0 %}red{% elif currency_data.exchanged.final_total > 0 %}green{% endif %}"></c-amount.display>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
-
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
{# Month columns #}
|
||||||
|
{% for month in data.months %}
|
||||||
|
<td class="text-nowrap text-center">
|
||||||
|
{% with month_data=item.month_totals %}
|
||||||
|
{% for m, m_data in month_data.items %}
|
||||||
|
{% if m == month %}
|
||||||
|
{% for currency_id, currency_data in m_data.currencies.items %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency_data.final_total"
|
||||||
|
:prefix="currency_data.currency.prefix"
|
||||||
|
:suffix="currency_data.currency.suffix"
|
||||||
|
:decimal_places="currency_data.currency.decimal_places"
|
||||||
|
color="{% if currency_data.final_total < 0 %}red{% elif currency_data.final_total > 0 %}green{% endif %}"></c-amount.display>
|
||||||
|
{% if currency_data.exchanged %}
|
||||||
|
<div class="text-xs text-base-content/60">
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency_data.exchanged.final_total"
|
||||||
|
:prefix="currency_data.exchanged.currency.prefix"
|
||||||
|
:suffix="currency_data.exchanged.currency.suffix"
|
||||||
|
:decimal_places="currency_data.exchanged.currency.decimal_places"
|
||||||
|
color="{% if currency_data.exchanged.final_total < 0 %}red{% elif currency_data.exchanged.final_total > 0 %}green{% endif %}"></c-amount.display>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
-
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
-
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="font-bold bg-base-200">
|
||||||
|
<th class="sticky left-0 bg-base-200 z-10">{% trans 'Total' %}</th>
|
||||||
|
{# Grand total #}
|
||||||
|
<td class="text-nowrap text-center bg-base-300">
|
||||||
|
{% for currency_id, currency_data in data.grand_total.currencies.items %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency_data.final_total"
|
||||||
|
:prefix="currency_data.currency.prefix"
|
||||||
|
:suffix="currency_data.currency.suffix"
|
||||||
|
:decimal_places="currency_data.currency.decimal_places"
|
||||||
|
color="{% if currency_data.final_total < 0 %}red{% elif currency_data.final_total > 0 %}green{% endif %}"></c-amount.display>
|
||||||
|
{% if currency_data.exchanged %}
|
||||||
|
<div class="text-xs text-base-content/60">
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency_data.exchanged.final_total"
|
||||||
|
:prefix="currency_data.exchanged.currency.prefix"
|
||||||
|
:suffix="currency_data.exchanged.currency.suffix"
|
||||||
|
:decimal_places="currency_data.exchanged.currency.decimal_places"
|
||||||
|
color="{% if currency_data.exchanged.final_total < 0 %}red{% elif currency_data.exchanged.final_total > 0 %}green{% endif %}"></c-amount.display>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
-
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
{# Month totals #}
|
||||||
|
{% for month in data.months %}
|
||||||
|
<td class="text-nowrap text-center">
|
||||||
|
{% with month_total=data.month_totals %}
|
||||||
|
{% for m, m_data in month_total.items %}
|
||||||
|
{% if m == month %}
|
||||||
|
{% for currency_id, currency_data in m_data.currencies.items %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency_data.final_total"
|
||||||
|
:prefix="currency_data.currency.prefix"
|
||||||
|
:suffix="currency_data.currency.suffix"
|
||||||
|
:decimal_places="currency_data.currency.decimal_places"
|
||||||
|
color="{% if currency_data.final_total < 0 %}red{% elif currency_data.final_total > 0 %}green{% endif %}"></c-amount.display>
|
||||||
|
{% if currency_data.exchanged %}
|
||||||
|
<div class="text-xs text-base-content/60">
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency_data.exchanged.final_total"
|
||||||
|
:prefix="currency_data.exchanged.currency.prefix"
|
||||||
|
:suffix="currency_data.exchanged.currency.suffix"
|
||||||
|
:decimal_places="currency_data.exchanged.currency.decimal_places"
|
||||||
|
color="{% if currency_data.exchanged.final_total < 0 %}red{% elif currency_data.exchanged.final_total > 0 %}green{% endif %}"></c-amount.display>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
-
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
-
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<c-msg.empty title="{% translate 'No transactions for this year' %}"></c-msg.empty>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -126,6 +126,11 @@
|
|||||||
hx-get="{% url 'insights_year_by_year' %}">
|
hx-get="{% url 'insights_year_by_year' %}">
|
||||||
{% trans 'Year by Year' %}
|
{% trans 'Year by Year' %}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-free justify-start text-start" data-bs-target="#v-pills-content"
|
||||||
|
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
|
||||||
|
hx-get="{% url 'insights_month_by_month' %}">
|
||||||
|
{% trans 'Month by Month' %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user