mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-01-11 20:00:26 +01:00
304 lines
12 KiB
Python
304 lines
12 KiB
Python
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 apps.currencies.models import Currency
|
|
from apps.currencies.utils.convert import convert
|
|
from apps.transactions.models import Transaction
|
|
|
|
|
|
def get_year_by_year_data(group_by="categories"):
|
|
"""
|
|
Aggregate transaction totals by year for categories, tags, or entities.
|
|
|
|
Args:
|
|
group_by: One of "categories", "tags", or "entities"
|
|
|
|
Returns:
|
|
{
|
|
"years": [2025, 2024, ...], # Sorted descending
|
|
"items": {
|
|
item_id: {
|
|
"name": "Item Name",
|
|
"year_totals": {
|
|
2025: {"currencies": {...}},
|
|
...
|
|
},
|
|
"total": {"currencies": {...}} # Sum across all years
|
|
},
|
|
...
|
|
},
|
|
"year_totals": { # Sum across all items for each year
|
|
2025: {"currencies": {...}},
|
|
...
|
|
},
|
|
"grand_total": {"currencies": {...}} # Sum of everything
|
|
}
|
|
"""
|
|
# Base queryset - all paid transactions, non-muted
|
|
transactions = Transaction.objects.filter(
|
|
is_paid=True,
|
|
account__is_archived=False,
|
|
).exclude(account__currency__is_archived=True)
|
|
|
|
# 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"
|
|
|
|
# Get all unique years with transactions
|
|
years = (
|
|
transactions.values_list("reference_date__year", flat=True)
|
|
.distinct()
|
|
.order_by("-reference_date__year")
|
|
)
|
|
years = list(years)
|
|
|
|
if not years:
|
|
return {
|
|
"years": [],
|
|
"items": {},
|
|
"year_totals": {},
|
|
"grand_total": {"currencies": {}},
|
|
}
|
|
|
|
# Aggregate by group, year, and currency
|
|
metrics = (
|
|
transactions.values(
|
|
group_field,
|
|
name_field,
|
|
"reference_date__year",
|
|
"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__year")
|
|
)
|
|
|
|
# Build result structure
|
|
result = {
|
|
"years": years,
|
|
"items": OrderedDict(),
|
|
"year_totals": {}, # Totals per year across all items
|
|
"grand_total": {"currencies": {}}, # Grand total across everything
|
|
}
|
|
|
|
# Store currency info for later use in totals
|
|
currency_info = {}
|
|
|
|
for metric in metrics:
|
|
item_id = metric[group_field]
|
|
item_name = metric[name_field]
|
|
year = metric["reference_date__year"]
|
|
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,
|
|
"year_totals": {},
|
|
"total": {"currencies": {}}, # Total for this item across all years
|
|
}
|
|
|
|
if year not in result["items"][item_key]["year_totals"]:
|
|
result["items"][item_key]["year_totals"][year] = {"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]["year_totals"][year]["currencies"][currency_id] = (
|
|
currency_data
|
|
)
|
|
|
|
# Accumulate item total (across all years 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 year total (across all items for this year)
|
|
if year not in result["year_totals"]:
|
|
result["year_totals"][year] = {"currencies": {}}
|
|
if currency_id not in result["year_totals"][year]["currencies"]:
|
|
result["year_totals"][year]["currencies"][currency_id] = {
|
|
"currency": currency_data["currency"].copy(),
|
|
"final_total": Decimal("0"),
|
|
}
|
|
result["year_totals"][year]["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 year totals
|
|
for year, year_data in result["year_totals"].items():
|
|
for currency_id, total_data in year_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
|