Merge pull request #353

feat(insights:category-overview): add "No entity" totals
This commit is contained in:
Herculino Trotta
2025-08-29 23:08:34 -03:00
committed by GitHub
2 changed files with 113 additions and 101 deletions

View File

@@ -12,7 +12,10 @@ from apps.currencies.utils.convert import convert
def get_categories_totals(
transactions_queryset, ignore_empty=False, show_entities=False
):
# First get the category totals as before
# Step 1: Aggregate transaction data by category and currency.
# This query calculates the total current and projected income/expense for each
# category by grouping transactions and summing up their amounts based on their
# type (income/expense) and payment status (paid/unpaid).
category_currency_metrics = (
transactions_queryset.values(
"category",
@@ -76,7 +79,10 @@ def get_categories_totals(
.order_by("category__name")
)
# Get tag totals within each category with currency details
# Step 2: Aggregate transaction data by tag, category, and currency.
# This is similar to the category metrics but adds tags to the grouping,
# allowing for a breakdown of totals by tag within each category. It also
# handles untagged transactions, where the 'tags' field is None.
tag_metrics = transactions_queryset.values(
"category",
"tags",
@@ -131,10 +137,12 @@ def get_categories_totals(
),
)
# Process the results to structure by category
# Step 3: Initialize the main dictionary to structure the final results.
# The data will be organized hierarchically: category -> currency -> tags -> entities.
result = {}
# Process category totals first
# Step 4: Process the aggregated category metrics to build the initial result structure.
# This loop iterates through each category's metrics and populates the `result` dict.
for metric in category_currency_metrics:
# Skip empty categories if ignore_empty is True
if ignore_empty and all(
@@ -185,7 +193,7 @@ def get_categories_totals(
"total_final": total_final,
}
# Add exchanged values if exchange_currency exists
# Step 4a: Handle currency conversion for category totals if an exchange currency is defined.
if metric["account__currency__exchange_currency"]:
from_currency = Currency.objects.get(id=currency_id)
exchange_currency = Currency.objects.get(
@@ -224,7 +232,7 @@ def get_categories_totals(
result[category_id]["currencies"][currency_id] = currency_data
# Process tag totals and add them to the result, including untagged
# Step 5: Process the aggregated tag metrics and integrate them into the result structure.
for tag_metric in tag_metrics:
category_id = tag_metric["category"]
tag_id = tag_metric["tags"] # Will be None for untagged transactions
@@ -281,7 +289,7 @@ def get_categories_totals(
"total_final": tag_total_final,
}
# Add exchange currency support for tags
# Step 5a: Handle currency conversion for tag totals.
if tag_metric["account__currency__exchange_currency"]:
from_currency = Currency.objects.get(id=currency_id)
exchange_currency = Currency.objects.get(
@@ -322,6 +330,7 @@ def get_categories_totals(
currency_id
] = tag_currency_data
# Step 6: If requested, aggregate and process entity-level data.
if show_entities:
entity_metrics = transactions_queryset.values(
"category",
@@ -389,14 +398,15 @@ def get_categories_totals(
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"]
entity_key = entity_id if entity_id is not None else "no_entity"
entity_name = (
entity_metric["entities__name"]
if entity_id is not None
else None
)
if "entities" not in result[category_id]["tags"][tag_key]:
result[category_id]["tags"][tag_key]["entities"] = {}

View File

@@ -268,95 +268,97 @@
<!-- 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>
{% if entity.name or not entity.name and tag.entities.values|length > 1 %}
<tr class="table-row-nested-2">
<td class="ps-5">
<i class="fa-solid fa-user-group fa-fw me-2 text-muted"></i>{% if entity.name %}{{ entity.name }}{% else %}{% trans 'No entity' %}{% endif %}
</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>
{% endif %}
{% endfor %}
{% endif %}
{% endif %}