feat(insights:category-overview): display entities on table

This commit is contained in:
Herculino Trotta
2025-08-16 17:36:19 -03:00
parent 0c63552d1b
commit e096912e41
3 changed files with 307 additions and 15 deletions

View File

@@ -9,7 +9,9 @@ from apps.currencies.models import Currency
from apps.currencies.utils.convert import convert
def get_categories_totals(transactions_queryset, ignore_empty=False):
def get_categories_totals(
transactions_queryset, ignore_empty=False, show_entities=False
):
# First get the category totals as before
category_currency_metrics = (
transactions_queryset.values(
@@ -240,6 +242,7 @@ def get_categories_totals(transactions_queryset, ignore_empty=False):
result[category_id]["tags"][tag_key] = {
"name": tag_name,
"currencies": {},
"entities": {},
}
currency_id = tag_metric["account__currency"]
@@ -319,4 +322,173 @@ def get_categories_totals(transactions_queryset, ignore_empty=False):
currency_id
] = tag_currency_data
if show_entities:
entity_metrics = transactions_queryset.values(
"category",
"tags",
"entities",
"entities__name",
"account__currency",
"account__currency__code",
"account__currency__name",
"account__currency__decimal_places",
"account__currency__prefix",
"account__currency__suffix",
"account__currency__exchange_currency",
).annotate(
expense_current=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE, is_paid=True, then="amount"
),
default=Value(0),
output_field=models.DecimalField(),
)
),
Decimal("0"),
),
expense_projected=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE, is_paid=False, then="amount"
),
default=Value(0),
output_field=models.DecimalField(),
)
),
Decimal("0"),
),
income_current=Coalesce(
Sum(
Case(
When(type=Transaction.Type.INCOME, is_paid=True, then="amount"),
default=Value(0),
output_field=models.DecimalField(),
)
),
Decimal("0"),
),
income_projected=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.INCOME, is_paid=False, then="amount"
),
default=Value(0),
output_field=models.DecimalField(),
)
),
Decimal("0"),
),
)
for entity_metric in entity_metrics:
category_id = entity_metric["category"]
tag_id = entity_metric["tags"]
entity_id = entity_metric["entities"]
if not entity_id:
continue
if category_id in result:
tag_key = tag_id if tag_id is not None else "untagged"
if tag_key in result[category_id]["tags"]:
entity_key = entity_id
entity_name = entity_metric["entities__name"]
if "entities" not in result[category_id]["tags"][tag_key]:
result[category_id]["tags"][tag_key]["entities"] = {}
if (
entity_key
not in result[category_id]["tags"][tag_key]["entities"]
):
result[category_id]["tags"][tag_key]["entities"][entity_key] = {
"name": entity_name,
"currencies": {},
}
currency_id = entity_metric["account__currency"]
entity_total_current = (
entity_metric["income_current"]
- entity_metric["expense_current"]
)
entity_total_projected = (
entity_metric["income_projected"]
- entity_metric["expense_projected"]
)
entity_total_income = (
entity_metric["income_current"]
+ entity_metric["income_projected"]
)
entity_total_expense = (
entity_metric["expense_current"]
+ entity_metric["expense_projected"]
)
entity_total_final = entity_total_current + entity_total_projected
entity_currency_data = {
"currency": {
"code": entity_metric["account__currency__code"],
"name": entity_metric["account__currency__name"],
"decimal_places": entity_metric[
"account__currency__decimal_places"
],
"prefix": entity_metric["account__currency__prefix"],
"suffix": entity_metric["account__currency__suffix"],
},
"expense_current": entity_metric["expense_current"],
"expense_projected": entity_metric["expense_projected"],
"total_expense": entity_total_expense,
"income_current": entity_metric["income_current"],
"income_projected": entity_metric["income_projected"],
"total_income": entity_total_income,
"total_current": entity_total_current,
"total_projected": entity_total_projected,
"total_final": entity_total_final,
}
if entity_metric["account__currency__exchange_currency"]:
from_currency = Currency.objects.get(id=currency_id)
exchange_currency = Currency.objects.get(
id=entity_metric["account__currency__exchange_currency"]
)
exchanged = {}
for field in [
"expense_current",
"expense_projected",
"income_current",
"income_projected",
"total_income",
"total_expense",
"total_current",
"total_projected",
"total_final",
]:
amount, prefix, suffix, decimal_places = convert(
amount=entity_currency_data[field],
from_currency=from_currency,
to_currency=exchange_currency,
)
if amount is not None:
exchanged[field] = amount
if "currency" not in exchanged:
exchanged["currency"] = {
"prefix": prefix,
"suffix": suffix,
"decimal_places": decimal_places,
"code": exchange_currency.code,
"name": exchange_currency.name,
}
if exchanged:
entity_currency_data["exchanged"] = exchanged
result[category_id]["tags"][tag_key]["entities"][entity_key][
"currencies"
][currency_id] = entity_currency_data
return result

View File

@@ -180,6 +180,14 @@ def category_overview(request):
else:
show_tags = request.session.get("insights_category_explorer_show_tags", True)
if "show_entities" in request.GET:
show_entities = request.GET["show_entities"] == "on"
request.session["insights_category_explorer_show_entities"] = show_entities
else:
show_entities = request.session.get(
"insights_category_explorer_show_entities", False
)
if "showing" in request.GET:
showing = request.GET["showing"]
request.session["insights_category_explorer_showing"] = showing
@@ -190,7 +198,9 @@ def category_overview(request):
transactions = get_transactions(request, include_silent=True)
total_table = get_categories_totals(
transactions_queryset=transactions, ignore_empty=False
transactions_queryset=transactions,
ignore_empty=False,
show_entities=show_entities,
)
return render(
@@ -200,6 +210,7 @@ def category_overview(request):
"total_table": total_table,
"view_type": view_type,
"show_tags": show_tags,
"show_entities": show_entities,
"showing": showing,
},
)

View File

@@ -1,7 +1,7 @@
{% load i18n %}
<div hx-get="{% url 'category_overview' %}" hx-trigger="updated from:window" class="show-loading" hx-swap="outerHTML"
hx-include="#picker-form, #picker-type, #view-type, #show-tags, #showing">
hx-include="#picker-form, #picker-type, #view-type, #show-tags, #showing, #show-entities">
<div class="h-100 text-center mb-4">
<div class="btn-group gap-3" role="group" id="view-type" _="on change trigger updated">
<input type="radio" class="btn-check"
@@ -25,19 +25,34 @@
</div>
</div>
<div class="mt-3 mb-1 d-flex flex-column flex-md-row justify-content-between">
<div class="form-check form-switch" id="show-tags">
<div class="d-flex gap-4">
{% if view_type == 'table' %}
<input type="hidden" name="show_tags" value="off">
<input class="form-check-input" type="checkbox" role="switch" id="show-tags-switch" name="show_tags"
_="on change trigger updated" {% if show_tags %}checked{% endif %}>
{% spaceless %}
<label class="form-check-label" for="show-tags-switch">
{% trans 'Tags' %}
</label>
<c-ui.help-icon
content="{% trans 'Transaction amounts associated with multiple tags will be counted once for each tag' %}"
icon="fa-solid fa-circle-exclamation"></c-ui.help-icon>
{% endspaceless %}
<div class="form-check form-switch" id="show-tags">
<input type="hidden" name="show_tags" value="off">
<input class="form-check-input" type="checkbox" role="switch" id="show-tags-switch" name="show_tags"
_="on change trigger updated" {% if show_tags %}checked{% endif %}>
{% spaceless %}
<label class="form-check-label" for="show-tags-switch">
{% trans 'Tags' %}
</label>
<c-ui.help-icon
content="{% trans 'Transaction amounts associated with multiple tags will be counted once for each tag' %}"
icon="fa-solid fa-circle-exclamation"></c-ui.help-icon>
{% endspaceless %}
</div>
<div class="form-check form-switch" id="show-entities" {% if not show_tags %}style="display: none;"{% endif %}>
<input type="hidden" name="show_entities" value="off">
<input class="form-check-input" type="checkbox" role="switch" id="show-entities-switch" name="show_entities"
_="on change trigger updated" {% if show_entities %}checked{% endif %}>
{% spaceless %}
<label class="form-check-label" for="show-entities-switch">
{% trans 'Entities' %}
</label>
<c-ui.help-icon
content="{% trans 'Transaction amounts associated with multiple tags and entities will be counted once for each tag' %}"
icon="fa-solid fa-circle-exclamation"></c-ui.help-icon>
{% endspaceless %}
</div>
{% endif %}
</div>
<div class="btn-group btn-group-sm" role="group" id="showing" _="on change trigger updated">
@@ -250,6 +265,100 @@
{% endfor %}
</td>
</tr>
<!-- Entity rows -->
{% if show_entities %}
{% for entity_id, entity in tag.entities.items %}
<tr class="table-row-nested-2">
<td class="ps-5">
<i class="fa-solid fa-user-group fa-fw me-2 text-muted"></i>{{ entity.name }}
</td>
<td>
{% for currency in entity.currencies.values %}
{% if showing == 'current' and currency.income_current != 0 %}
<c-amount.display
:amount="currency.income_current"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="green"></c-amount.display>
{% elif showing == 'projected' and currency.income_projected != 0 %}
<c-amount.display
:amount="currency.income_projected"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="green"></c-amount.display>
{% elif showing == 'final' and currency.total_income != 0 %}
<c-amount.display
:amount="currency.total_income"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="green"></c-amount.display>
{% else %}
<div>-</div>
{% endif %}
{% endfor %}
</td>
<td>
{% for currency in entity.currencies.values %}
{% if showing == 'current' and currency.expense_current != 0 %}
<c-amount.display
:amount="currency.expense_current"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="red"></c-amount.display>
{% elif showing == 'projected' and currency.expense_projected != 0 %}
<c-amount.display
:amount="currency.expense_projected"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="red"></c-amount.display>
{% elif showing == 'final' and currency.total_expense != 0 %}
<c-amount.display
:amount="currency.total_expense"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="red"></c-amount.display>
{% else %}
<div>-</div>
{% endif %}
{% endfor %}
</td>
<td>
{% for currency in entity.currencies.values %}
{% if showing == 'current' and currency.total_current != 0 %}
<c-amount.display
:amount="currency.total_current"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
{% elif showing == 'projected' and currency.total_projected != 0 %}
<c-amount.display
:amount="currency.total_projected"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
{% elif showing == 'final' and currency.total_final != 0 %}
<c-amount.display
:amount="currency.total_final"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
{% else %}
<div>-</div>
{% endif %}
{% endfor %}
</td>
</tr>
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}