more changes

This commit is contained in:
Herculino Trotta
2024-10-13 12:10:50 -03:00
parent 1717d8a94e
commit d20897a28a
33 changed files with 1552 additions and 153 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ def account_reconciliation(request):
)
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, hide_offcanvas, toast"},
headers={"HX-Trigger": "updated, hide_offcanvas, toast"},
)
else:
formset = AccountBalanceFormSet(initial=initial_data)
+16 -5
View File
@@ -5,11 +5,17 @@ from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from apps.currencies.models import ExchangeRate
from apps.currencies.models import Currency
def convert(amount, from_currency, to_currency, date=None):
def convert(amount, from_currency: Currency, to_currency: Currency, date=None):
if from_currency == to_currency:
return amount
return (
amount,
to_currency.prefix,
to_currency.suffix,
to_currency.decimal_places,
)
if date is None:
date = timezone.localtime(timezone.now())
@@ -35,8 +41,13 @@ def convert(amount, from_currency, to_currency, date=None):
)
if exchange_rate is None:
return None
return None, None, None, None
return amount * exchange_rate.effective_rate
return (
amount * exchange_rate.effective_rate,
to_currency.prefix,
to_currency.suffix,
to_currency.decimal_places,
)
except ExchangeRate.DoesNotExist:
return None
return None, None, None, None
-3
View File
@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
@@ -2,10 +2,77 @@ from django.db.models import Sum
from decimal import Decimal
from django.db.models.functions import TruncMonth
from django.utils.translation import gettext_lazy as _
from apps.transactions.models import Transaction
from apps.accounts.models import Account
from apps.currencies.models import Currency
from apps.currencies.utils.convert import convert
def calculate_account_net_worth():
account_net_worth = {}
ungrouped_id = None # Special ID for ungrouped accounts
# Initialize the "Ungrouped" category
account_net_worth[ungrouped_id] = {"name": _("Ungrouped"), "accounts": {}}
# Get all accounts
accounts = Account.objects.all()
for account in accounts:
currency = account.currency
income = Transaction.objects.filter(
account=account, type=Transaction.Type.INCOME, is_paid=True
).aggregate(total=Sum("amount"))["total"] or Decimal("0")
expenses = Transaction.objects.filter(
account=account, type=Transaction.Type.EXPENSE, is_paid=True
).aggregate(total=Sum("amount"))["total"] or Decimal("0")
account_balance = income - expenses
account_data = {
"name": account.name,
"balance": account_balance,
"currency": {
"code": currency.code,
"name": currency.name,
"prefix": currency.prefix,
"suffix": currency.suffix,
"decimal_places": currency.decimal_places,
},
}
if account.exchange_currency:
converted_amount, prefix, suffix, decimal_places = convert(
amount=account_balance,
from_currency=account.currency,
to_currency=account.exchange_currency,
)
if converted_amount:
account_data["exchange"] = {
"amount": converted_amount,
"prefix": prefix,
"suffix": suffix,
"decimal_places": decimal_places,
}
if account.group:
group_id = account.group.id
group_name = account.group.name
if group_id not in account_net_worth:
account_net_worth[group_id] = {"name": group_name, "accounts": {}}
account_net_worth[group_id]["accounts"][account.id] = account_data
else:
account_net_worth[ungrouped_id]["accounts"][account.id] = account_data
# Remove the "Ungrouped" category if it's empty
if not account_net_worth[ungrouped_id]["accounts"]:
del account_net_worth[ungrouped_id]
return account_net_worth
def calculate_net_worth():
@@ -41,7 +108,7 @@ def calculate_historical_net_worth(start_date, end_date):
# Get all months between start_date and end_date
months = (
Transaction.objects.filter(account__in=asset_accounts)
.annotate(month=TruncMonth("date"))
.annotate(month=TruncMonth("reference_date"))
.values("month")
.distinct()
.order_by("month")
@@ -61,14 +128,14 @@ def calculate_historical_net_worth(start_date, end_date):
account=account,
type=Transaction.Type.INCOME,
is_paid=True,
date__lte=month,
reference_date__lte=month,
).aggregate(total=Sum("amount"))["total"] or Decimal("0.00")
expenses = Transaction.objects.filter(
account=account,
type=Transaction.Type.EXPENSE,
is_paid=True,
date__lte=month,
reference_date__lte=month,
).aggregate(total=Sum("amount"))["total"] or Decimal("0.00")
account_balance = income - expenses
+25 -1
View File
@@ -1,11 +1,14 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
from django.http import JsonResponse
from django.shortcuts import render
from django.utils import timezone
from apps.net_worth.utils.calculate_net_worth import (
calculate_net_worth,
calculate_historical_net_worth,
calculate_account_net_worth,
)
from apps.currencies.models import Currency
@@ -13,12 +16,14 @@ from apps.currencies.models import Currency
# Create your views here.
def net_worth_main(request):
net_worth = calculate_net_worth()
detailed_net_worth = calculate_account_net_worth()
# historical = calculate_historical_net_worth(
# start_date=datetime(day=1, month=1, year=2021).date(),
# end_date=datetime(day=1, month=1, year=2025).date(),
# )
# print(historical)
print(detailed_net_worth)
# Format the net worth with currency details
formatted_net_worth = []
for currency_code, amount in net_worth.items():
@@ -34,6 +39,25 @@ def net_worth_main(request):
}
)
end_date = timezone.now()
start_date = end_date - relativedelta(years=5) # Last year
# Calculate historical net worth
historical_data = calculate_historical_net_worth(start_date, end_date)
# Prepare data for the template
currencies = Currency.objects.all()
print(historical_data)
return render(
request, "net_worth/net_worth.html", {"currency_net_worth": formatted_net_worth}
request,
"net_worth/net_worth.html",
{
"currency_net_worth": formatted_net_worth,
"account_net_worth": detailed_net_worth,
"currencies": currencies,
"historical_data_json": JsonResponse(historical_data).content.decode(
"utf-8"
),
},
)
+6 -6
View File
@@ -119,18 +119,18 @@ class Transaction(models.Model):
def exchanged_amount(self):
if self.account.exchange_currency:
converted_amount = convert(
converted_amount, prefix, suffix, decimal_places = convert(
self.amount,
self.account.exchange_currency,
self.account.currency,
to_currency=self.account.exchange_currency,
from_currency=self.account.currency,
date=self.date,
)
if converted_amount:
return {
"amount": converted_amount,
"suffix": self.account.exchange_currency.suffix,
"prefix": self.account.exchange_currency.prefix,
"decimal_places": self.account.exchange_currency.decimal_places,
"prefix": prefix,
"suffix": suffix,
"decimal_places": decimal_places,
}
return None
+5 -5
View File
@@ -38,7 +38,7 @@ def transaction_add(request):
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, hide_offcanvas, toast"},
headers={"HX-Trigger": "updated, hide_offcanvas, toast"},
)
else:
form = TransactionForm(
@@ -69,7 +69,7 @@ def transaction_edit(request, transaction_id, **kwargs):
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, hide_offcanvas, toast"},
headers={"HX-Trigger": "updated, hide_offcanvas, toast"},
)
else:
form = TransactionForm(instance=transaction)
@@ -102,7 +102,7 @@ def transaction_delete(request, transaction_id, **kwargs):
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, toast"},
headers={"HX-Trigger": "updated, toast"},
)
@@ -127,7 +127,7 @@ def transactions_transfer(request):
messages.success(request, _("Transfer added successfully"))
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, toast, hide_offcanvas"},
headers={"HX-Trigger": "updated, toast, hide_offcanvas"},
)
else:
form = TransferForm(
@@ -175,7 +175,7 @@ class AddInstallmentPlanView(View):
return HttpResponse(
status=204,
headers={"HX-Trigger": "transaction_updated, hide_offcanvas, toast"},
headers={"HX-Trigger": "updated, hide_offcanvas, toast"},
)
return render(request, self.template_name, {"form": form})
+1 -1
View File
@@ -45,7 +45,7 @@ def toggle_amount_visibility(request):
messages.info(request, _("Transaction amounts are now displayed"))
response = render(request, "users/generic/hide_amounts.html")
response.headers["HX-Trigger"] = "transaction_updated, toast"
response.headers["HX-Trigger"] = "updated, toast"
return response
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class YearlyOverviewConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.yearly_overview"
+12
View File
@@ -0,0 +1,12 @@
from django.urls import path
from . import views
urlpatterns = [
path("yearly/", views.index, name="yearly_index"),
path(
"yearly/<int:year>/",
views.yearly_overview,
name="yearly_overview",
),
]
+468
View File
@@ -0,0 +1,468 @@
from datetime import date
from decimal import Decimal
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from django.utils import timezone
from django.db.models import Sum, F, Q, Value, CharField, DecimalField
from django.db.models.functions import TruncMonth, Coalesce
from django.db.models.expressions import Case, When
from django.db.models.functions import Concat
from apps.transactions.models import Transaction
# Create your views here.
@login_required
def index(request):
now = timezone.localdate(timezone.now())
return redirect(to="yearly_overview", year=now.year)
# def yearly_overview(request, year: int):
# transactions = Transaction.objects.filter(date__year=year)
#
# monthly_data = (
# transactions.annotate(month=TruncMonth("date"))
# .values(
# "month",
# "account__id",
# "account__name",
# "account__group__name",
# "account__currency__code",
# "account__currency__suffix",
# "account__currency__prefix",
# "account__currency__decimal_places",
# )
# .annotate(
# income_paid=Coalesce(
# Sum(
# Case(
# When(
# type=Transaction.Type.INCOME, is_paid=True, then=F("amount")
# ),
# default=Value(Decimal("0")),
# output_field=DecimalField(),
# )
# ),
# Value(Decimal("0")),
# output_field=DecimalField(),
# ),
# expense_paid=Coalesce(
# Sum(
# Case(
# When(
# type=Transaction.Type.EXPENSE,
# is_paid=True,
# then=F("amount"),
# ),
# default=Value(Decimal("0")),
# output_field=DecimalField(),
# )
# ),
# Value(Decimal("0")),
# output_field=DecimalField(),
# ),
# income_unpaid=Coalesce(
# Sum(
# Case(
# When(
# type=Transaction.Type.INCOME,
# is_paid=False,
# then=F("amount"),
# ),
# default=Value(Decimal("0")),
# output_field=DecimalField(),
# )
# ),
# Value(Decimal("0")),
# output_field=DecimalField(),
# ),
# expense_unpaid=Coalesce(
# Sum(
# Case(
# When(
# type=Transaction.Type.EXPENSE,
# is_paid=False,
# then=F("amount"),
# ),
# default=Value(Decimal("0")),
# output_field=DecimalField(),
# )
# ),
# Value(Decimal("0")),
# output_field=DecimalField(),
# ),
# )
# .annotate(
# balance_unpaid=F("income_unpaid") - F("expense_unpaid"),
# balance_paid=F("income_paid") - F("expense_paid"),
# balance_total=F("income_paid")
# + F("income_unpaid")
# - F("expense_paid")
# - F("expense_unpaid"),
# )
# .order_by("month", "account__group__name")
# )
#
# # Create a list of all months in the year
# all_months = [date(year, month, 1) for month in range(1, 13)]
#
# # Create a dictionary to store the final result
# result = {
# month: {
# "income_paid": [],
# "expense_paid": [],
# "income_unpaid": [],
# "expense_unpaid": [],
# "balance_unpaid": [],
# "balance_paid": [],
# "balance_total": [],
# }
# for month in all_months
# }
#
# # Fill in the data
# for entry in monthly_data:
# month = entry["month"]
# account_info = {
# "id": entry["account__id"],
# "name": entry["account__name"],
# "currency": entry["account__currency__code"],
# "suffix": entry["account__currency__suffix"],
# "prefix": entry["account__currency__prefix"],
# "decimal_places": entry["account__currency__decimal_places"],
# "group": entry["account__group__name"],
# }
#
# for field in [
# "income_paid",
# "expense_paid",
# "income_unpaid",
# "expense_unpaid",
# "balance_unpaid",
# "balance_paid",
# "balance_total",
# ]:
# result[month][field].append(
# {"account": account_info, "amount": entry[field]}
# )
#
# # Fill in missing months with empty lists
# for month in all_months:
# if not any(result[month].values()):
# result[month] = {
# "income_paid": [],
# "expense_paid": [],
# "income_unpaid": [],
# "expense_unpaid": [],
# "balance_unpaid": [],
# "balance_paid": [],
# "balance_total": [],
# }
#
# from pprint import pprint
#
# pprint(result)
#
# return render(
# request,
# "yearly_overview/pages/overview2.html",
# context={
# "year": year,
# # "next_month": next_month,
# # "next_year": next_year,
# # "previous_month": previous_month,
# # "previous_year": previous_year,
# "data": result,
# },
# )
def yearly_overview(request, year: int):
# First, let's create a base queryset for the given year
base_queryset = Transaction.objects.filter(date__year=year)
# Create a list of all months in the year
months = [f"{year}-{month:02d}-01" for month in range(1, 13)]
# Create the queryset with all the required annotations
queryset = (
base_queryset.annotate(month=TruncMonth("date"))
.values("month", "account__group__name")
.annotate(
income_paid=Coalesce(
Sum(
Case(
When(
Q(type=Transaction.Type.INCOME, is_paid=True),
then=F("amount"),
),
default=Value(0),
output_field=DecimalField(),
)
),
Value(0, output_field=DecimalField()),
),
expense_paid=Coalesce(
Sum(
Case(
When(
Q(type=Transaction.Type.EXPENSE, is_paid=True),
then=F("amount"),
),
default=Value(0),
output_field=DecimalField(),
)
),
Value(0, output_field=DecimalField()),
),
income_unpaid=Coalesce(
Sum(
Case(
When(
Q(type=Transaction.Type.INCOME, is_paid=False),
then=F("amount"),
),
default=Value(0),
output_field=DecimalField(),
)
),
Value(0, output_field=DecimalField()),
),
expense_unpaid=Coalesce(
Sum(
Case(
When(
Q(type=Transaction.Type.EXPENSE, is_paid=False),
then=F("amount"),
),
default=Value(0),
output_field=DecimalField(),
)
),
Value(0, output_field=DecimalField()),
),
)
.annotate(
balance_unpaid=F("income_unpaid") - F("expense_unpaid"),
balance_paid=F("income_paid") - F("expense_paid"),
balance_total=F("income_paid")
+ F("income_unpaid")
- F("expense_paid")
- F("expense_unpaid"),
)
.order_by("month", "account__group__name")
)
# Create a dictionary to store results
results = {month: {} for month in months}
# Populate the results dictionary
for entry in queryset:
month = entry["month"]
account_group = entry["account__group__name"]
if account_group not in results[month]:
results[month][account_group] = {
"income_paid": entry["income_paid"],
"expense_paid": entry["expense_paid"],
"income_unpaid": entry["income_unpaid"],
"expense_unpaid": entry["expense_unpaid"],
"balance_unpaid": entry["balance_unpaid"],
"balance_paid": entry["balance_paid"],
"balance_total": entry["balance_total"],
}
else:
# If the account group already exists, update the values
for key in [
"income_paid",
"expense_paid",
"income_unpaid",
"expense_unpaid",
"balance_unpaid",
"balance_paid",
"balance_total",
]:
results[month][account_group][key] += entry[key]
# Replace empty months with "-"
for month in results:
if not results[month]:
results[month] = "-"
from pprint import pprint
pprint(results)
return render(
request,
"yearly_overview/pages/overview2.html",
context={
"year": year,
# "next_month": next_month,
# "next_year": next_year,
# "previous_month": previous_month,
# "previous_year": previous_year,
"data": results,
},
)
# def yearly_overview(request, year: int):
# transactions = Transaction.objects.filter(reference_date__year=year)
#
# monthly_data = (
# transactions.annotate(month=TruncMonth("reference_date"))
# .values(
# "month",
# "account__currency__code",
# "account__currency__prefix",
# "account__currency__suffix",
# "account__currency__decimal_places",
# )
# .annotate(
# income_paid=Coalesce(
# Sum(
# Case(
# When(
# type=Transaction.Type.INCOME, is_paid=True, then=F("amount")
# ),
# default=Value(Decimal("0")),
# output_field=DecimalField(),
# )
# ),
# Value(Decimal("0")),
# output_field=DecimalField(),
# ),
# expense_paid=Coalesce(
# Sum(
# Case(
# When(
# type=Transaction.Type.EXPENSE,
# is_paid=True,
# then=F("amount"),
# ),
# default=Value(Decimal("0")),
# output_field=DecimalField(),
# )
# ),
# Value(Decimal("0")),
# output_field=DecimalField(),
# ),
# income_unpaid=Coalesce(
# Sum(
# Case(
# When(
# type=Transaction.Type.INCOME,
# is_paid=False,
# then=F("amount"),
# ),
# default=Value(Decimal("0")),
# output_field=DecimalField(),
# )
# ),
# Value(Decimal("0")),
# output_field=DecimalField(),
# ),
# expense_unpaid=Coalesce(
# Sum(
# Case(
# When(
# type=Transaction.Type.EXPENSE,
# is_paid=False,
# then=F("amount"),
# ),
# default=Value(Decimal("0")),
# output_field=DecimalField(),
# )
# ),
# Value(Decimal("0")),
# output_field=DecimalField(),
# ),
# )
# .annotate(
# balance_unpaid=F("income_unpaid") - F("expense_unpaid"),
# balance_paid=F("income_paid") - F("expense_paid"),
# balance_total=F("income_paid")
# + F("income_unpaid")
# - F("expense_paid")
# - F("expense_unpaid"),
# )
# .order_by("month", "account__currency__code")
# )
#
# # Create a list of all months in the year
# all_months = [date(year, month, 1) for month in range(1, 13)]
#
# # Create a dictionary to store the final result
# result = {
# month: {
# "income_paid": [],
# "expense_paid": [],
# "income_unpaid": [],
# "expense_unpaid": [],
# "balance_unpaid": [],
# "balance_paid": [],
# "balance_total": [],
# }
# for month in all_months
# }
#
# # Fill in the data
# for entry in monthly_data:
# month = entry["month"]
# currency_code = entry["account__currency__code"]
# prefix = entry["account__currency__prefix"]
# suffix = entry["account__currency__suffix"]
# decimal_places = entry["account__currency__decimal_places"]
#
# for field in [
# "income_paid",
# "expense_paid",
# "income_unpaid",
# "expense_unpaid",
# "balance_unpaid",
# "balance_paid",
# "balance_total",
# ]:
# result[month][field].append(
# {
# "code": currency_code,
# "prefix": prefix,
# "suffix": suffix,
# "decimal_places": decimal_places,
# "amount": entry[field],
# }
# )
#
# # Fill in missing months with empty lists
# for month in all_months:
# if not any(result[month].values()):
# result[month] = {
# "income_paid": [],
# "expense_paid": [],
# "income_unpaid": [],
# "expense_unpaid": [],
# "balance_unpaid": [],
# "balance_paid": [],
# "balance_total": [],
# }
#
# from pprint import pprint
#
# pprint(result)
#
# return render(
# request,
# "yearly_overview/pages/overview.html",
# context={
# "year": year,
# # "next_month": next_month,
# # "next_year": next_year,
# # "previous_month": previous_month,
# # "previous_year": previous_year,
# "totals": result,
# },
# )