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
+22 -12
View File
@@ -12,7 +12,10 @@ from apps.currencies.utils.convert import convert
def get_categories_totals( def get_categories_totals(
transactions_queryset, ignore_empty=False, show_entities=False 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 = ( category_currency_metrics = (
transactions_queryset.values( transactions_queryset.values(
"category", "category",
@@ -76,7 +79,10 @@ def get_categories_totals(
.order_by("category__name") .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( tag_metrics = transactions_queryset.values(
"category", "category",
"tags", "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 = {} 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: for metric in category_currency_metrics:
# Skip empty categories if ignore_empty is True # Skip empty categories if ignore_empty is True
if ignore_empty and all( if ignore_empty and all(
@@ -185,7 +193,7 @@ def get_categories_totals(
"total_final": total_final, "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"]: if metric["account__currency__exchange_currency"]:
from_currency = Currency.objects.get(id=currency_id) from_currency = Currency.objects.get(id=currency_id)
exchange_currency = Currency.objects.get( exchange_currency = Currency.objects.get(
@@ -224,7 +232,7 @@ def get_categories_totals(
result[category_id]["currencies"][currency_id] = currency_data 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: for tag_metric in tag_metrics:
category_id = tag_metric["category"] category_id = tag_metric["category"]
tag_id = tag_metric["tags"] # Will be None for untagged transactions tag_id = tag_metric["tags"] # Will be None for untagged transactions
@@ -281,7 +289,7 @@ def get_categories_totals(
"total_final": tag_total_final, "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"]: if tag_metric["account__currency__exchange_currency"]:
from_currency = Currency.objects.get(id=currency_id) from_currency = Currency.objects.get(id=currency_id)
exchange_currency = Currency.objects.get( exchange_currency = Currency.objects.get(
@@ -322,6 +330,7 @@ def get_categories_totals(
currency_id currency_id
] = tag_currency_data ] = tag_currency_data
# Step 6: If requested, aggregate and process entity-level data.
if show_entities: if show_entities:
entity_metrics = transactions_queryset.values( entity_metrics = transactions_queryset.values(
"category", "category",
@@ -389,14 +398,15 @@ def get_categories_totals(
tag_id = entity_metric["tags"] tag_id = entity_metric["tags"]
entity_id = entity_metric["entities"] entity_id = entity_metric["entities"]
if not entity_id:
continue
if category_id in result: if category_id in result:
tag_key = tag_id if tag_id is not None else "untagged" tag_key = tag_id if tag_id is not None else "untagged"
if tag_key in result[category_id]["tags"]: if tag_key in result[category_id]["tags"]:
entity_key = entity_id entity_key = entity_id if entity_id is not None else "no_entity"
entity_name = entity_metric["entities__name"] entity_name = (
entity_metric["entities__name"]
if entity_id is not None
else None
)
if "entities" not in result[category_id]["tags"][tag_key]: if "entities" not in result[category_id]["tags"][tag_key]:
result[category_id]["tags"][tag_key]["entities"] = {} result[category_id]["tags"][tag_key]["entities"] = {}
@@ -268,95 +268,97 @@
<!-- Entity rows --> <!-- Entity rows -->
{% if show_entities %} {% if show_entities %}
{% for entity_id, entity in tag.entities.items %} {% for entity_id, entity in tag.entities.items %}
<tr class="table-row-nested-2"> {% if entity.name or not entity.name and tag.entities.values|length > 1 %}
<td class="ps-5"> <tr class="table-row-nested-2">
<i class="fa-solid fa-user-group fa-fw me-2 text-muted"></i>{{ entity.name }} <td class="ps-5">
</td> <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 %} <td>
{% if showing == 'current' and currency.income_current != 0 %} {% for currency in entity.currencies.values %}
<c-amount.display {% if showing == 'current' and currency.income_current != 0 %}
:amount="currency.income_current" <c-amount.display
:prefix="currency.currency.prefix" :amount="currency.income_current"
:suffix="currency.currency.suffix" :prefix="currency.currency.prefix"
:decimal_places="currency.currency.decimal_places" :suffix="currency.currency.suffix"
color="green"></c-amount.display> :decimal_places="currency.currency.decimal_places"
{% elif showing == 'projected' and currency.income_projected != 0 %} color="green"></c-amount.display>
<c-amount.display {% elif showing == 'projected' and currency.income_projected != 0 %}
:amount="currency.income_projected" <c-amount.display
:prefix="currency.currency.prefix" :amount="currency.income_projected"
:suffix="currency.currency.suffix" :prefix="currency.currency.prefix"
:decimal_places="currency.currency.decimal_places" :suffix="currency.currency.suffix"
color="green"></c-amount.display> :decimal_places="currency.currency.decimal_places"
{% elif showing == 'final' and currency.total_income != 0 %} color="green"></c-amount.display>
<c-amount.display {% elif showing == 'final' and currency.total_income != 0 %}
:amount="currency.total_income" <c-amount.display
:prefix="currency.currency.prefix" :amount="currency.total_income"
:suffix="currency.currency.suffix" :prefix="currency.currency.prefix"
:decimal_places="currency.currency.decimal_places" :suffix="currency.currency.suffix"
color="green"></c-amount.display> :decimal_places="currency.currency.decimal_places"
{% else %} color="green"></c-amount.display>
<div>-</div> {% else %}
{% endif %} <div>-</div>
{% endfor %} {% endif %}
</td> {% endfor %}
<td> </td>
{% for currency in entity.currencies.values %} <td>
{% if showing == 'current' and currency.expense_current != 0 %} {% for currency in entity.currencies.values %}
<c-amount.display {% if showing == 'current' and currency.expense_current != 0 %}
:amount="currency.expense_current" <c-amount.display
:prefix="currency.currency.prefix" :amount="currency.expense_current"
:suffix="currency.currency.suffix" :prefix="currency.currency.prefix"
:decimal_places="currency.currency.decimal_places" :suffix="currency.currency.suffix"
color="red"></c-amount.display> :decimal_places="currency.currency.decimal_places"
{% elif showing == 'projected' and currency.expense_projected != 0 %} color="red"></c-amount.display>
<c-amount.display {% elif showing == 'projected' and currency.expense_projected != 0 %}
:amount="currency.expense_projected" <c-amount.display
:prefix="currency.currency.prefix" :amount="currency.expense_projected"
:suffix="currency.currency.suffix" :prefix="currency.currency.prefix"
:decimal_places="currency.currency.decimal_places" :suffix="currency.currency.suffix"
color="red"></c-amount.display> :decimal_places="currency.currency.decimal_places"
{% elif showing == 'final' and currency.total_expense != 0 %} color="red"></c-amount.display>
<c-amount.display {% elif showing == 'final' and currency.total_expense != 0 %}
:amount="currency.total_expense" <c-amount.display
:prefix="currency.currency.prefix" :amount="currency.total_expense"
:suffix="currency.currency.suffix" :prefix="currency.currency.prefix"
:decimal_places="currency.currency.decimal_places" :suffix="currency.currency.suffix"
color="red"></c-amount.display> :decimal_places="currency.currency.decimal_places"
{% else %} color="red"></c-amount.display>
<div>-</div> {% else %}
{% endif %} <div>-</div>
{% endfor %} {% endif %}
</td> {% endfor %}
<td> </td>
{% for currency in entity.currencies.values %} <td>
{% if showing == 'current' and currency.total_current != 0 %} {% for currency in entity.currencies.values %}
<c-amount.display {% if showing == 'current' and currency.total_current != 0 %}
:amount="currency.total_current" <c-amount.display
:prefix="currency.currency.prefix" :amount="currency.total_current"
:suffix="currency.currency.suffix" :prefix="currency.currency.prefix"
:decimal_places="currency.currency.decimal_places" :suffix="currency.currency.suffix"
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display> :decimal_places="currency.currency.decimal_places"
{% elif showing == 'projected' and currency.total_projected != 0 %} color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
<c-amount.display {% elif showing == 'projected' and currency.total_projected != 0 %}
:amount="currency.total_projected" <c-amount.display
:prefix="currency.currency.prefix" :amount="currency.total_projected"
:suffix="currency.currency.suffix" :prefix="currency.currency.prefix"
:decimal_places="currency.currency.decimal_places" :suffix="currency.currency.suffix"
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display> :decimal_places="currency.currency.decimal_places"
{% elif showing == 'final' and currency.total_final != 0 %} color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
<c-amount.display {% elif showing == 'final' and currency.total_final != 0 %}
:amount="currency.total_final" <c-amount.display
:prefix="currency.currency.prefix" :amount="currency.total_final"
:suffix="currency.currency.suffix" :prefix="currency.currency.prefix"
:decimal_places="currency.currency.decimal_places" :suffix="currency.currency.suffix"
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display> :decimal_places="currency.currency.decimal_places"
{% else %} color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
<div>-</div> {% else %}
{% endif %} <div>-</div>
{% endfor %} {% endif %}
</td> {% endfor %}
</tr> </td>
</tr>
{% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endif %} {% endif %}