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

View File

@@ -41,5 +41,6 @@ urlpatterns = [
path("", include("apps.accounts.urls")),
path("", include("apps.net_worth.urls")),
path("", include("apps.monthly_overview.urls")),
path("", include("apps.yearly_overview.urls")),
path("", include("apps.currencies.urls")),
]

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)

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

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -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

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"
),
},
)

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

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})

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

View File

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"

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",
),
]

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,
# },
# )

View File

@@ -41,9 +41,7 @@
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'account_edit' pk=account.id %}"
hx-target="#generic-offcanvas"
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install tooltip">
_="install tooltip">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="text-danger text-decoration-none p-1 tag-action"
role="button"
@@ -51,9 +49,7 @@
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'account_delete' pk=account.id %}"
hx-trigger='delete_confirmed'
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install tooltip
_="install tooltip
on click
if event.ctrlKey trigger delete_confirmed
else

View File

@@ -39,9 +39,7 @@
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'currency_edit' pk=currency.id %}"
hx-target="#generic-offcanvas"
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install tooltip">
_="install tooltip">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="text-danger text-decoration-none p-1 tag-action"
role="button"
@@ -49,9 +47,7 @@
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'currency_delete' pk=currency.id %}"
hx-trigger='delete_confirmed'
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install tooltip
_="install tooltip
on click
if event.ctrlKey trigger delete_confirmed
else

View File

@@ -10,7 +10,7 @@
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 nav-underline" hx-push-url="true">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% active_link views='monthly_overview' %}"
<a class="nav-link dropdown-toggle {% active_link views='monthly_overview||yearly_overview' %}"
href="#"
role="button"
data-bs-toggle="dropdown"
@@ -19,6 +19,7 @@
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item {% active_link views='monthly_overview' %}" href="{% url 'monthly_index' %}">{%translate 'Monthly' %}</a></li>
<li><a class="dropdown-item {% active_link views='yearly_overview' %}" href="{% url 'yearly_index' %}">{%translate 'Yearly' %}</a></li>
</ul>
</li>
<li class="nav-item">

View File

@@ -10,6 +10,7 @@
{% include 'includes/scripts/hyperscript/htmx_error_handler.html' %}
{% javascript_pack 'htmx' attrs="defer" %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;

View File

@@ -14,7 +14,7 @@
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'today' %}</div>
</div>
<div class="text-start font-monospace">
<div class="text-end font-monospace">
{% for entry in totals.daily_spending_allowance %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
@@ -38,7 +38,7 @@
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current' %}</div>
</div>
<div class="d-flex justify-content-between text-start font-monospace">
<div class="text-end font-monospace">
{% for entry in totals.paid_income %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
@@ -51,7 +51,7 @@
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected' %}</div>
</div>
<div class="text-start font-monospace">
<div class="text-end font-monospace">
{% for entry in totals.projected_income %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
@@ -75,7 +75,7 @@
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current' %}</div>
</div>
<div class="text-start font-monospace">
<div class="text-end font-monospace">
{% for entry in totals.paid_expenses %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
@@ -88,7 +88,7 @@
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected' %}</div>
</div>
<div class="text-start font-monospace">
<div class="text-end font-monospace">
{% for entry in totals.projected_expenses %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
@@ -112,7 +112,7 @@
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current' %}</div>
</div>
<div class="text-start font-monospace">
<div class="text-end font-monospace">
{% for entry in totals.total_current %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
@@ -124,7 +124,7 @@
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected' %}</div>
</div>
<div class="text-start font-monospace">
<div class="text-end font-monospace">
{% for entry in totals.total_projected %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
@@ -134,7 +134,7 @@
</div>
<hr class="my-1">
<div class="d-flex justify-content-end">
<div class="text-start font-monospace">
<div class="text-end font-monospace">
{% for entry in totals.total_final %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}

View File

@@ -5,7 +5,7 @@
{% load static %}
{% load webpack_loader %}
{% block title %}Monthly Overview :: {{ month|month_name }}/{{ year }}{% endblock %}
{% block title %}{% translate 'Monthly Overview' %} :: {{ month|month_name }}/{{ year }}{% endblock %}
{% block body_hyperscript %}
on keyup[code is 'KeyE' and target.nodeName is 'BODY'] from body trigger 'add_expense' end
@@ -95,7 +95,7 @@
<div class="row gx-xl-4 gy-3">
<div class="col-12 col-xl-4 order-0 order-xl-2">
<div id="summary" hx-get="{% url 'monthly_summary' month=month year=year %}" class="sticky-sidebar"
hx-trigger="load, transaction_updated from:window, monthly_summary_update from:window">
hx-trigger="load, updated from:window, monthly_summary_update from:window">
</div>
</div>
<div class="col-12 col-xl-8 order-2 order-xl-1">
@@ -118,7 +118,7 @@
</div>
<div id="transactions"
hx-get="{% url 'monthly_transactions_list' month=month year=year %}"
hx-trigger="load, transaction_updated from:window" hx-include="#filter"></div>
hx-trigger="load, updated from:window" hx-include="#filter"></div>
</div>
</div>
</div>

View File

@@ -10,28 +10,154 @@
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="row">
<div class="col-6 row-gap-5">
{% for currency in currency_net_worth %}
<div class="row">
<div class="row gx-xl-4 gy-3">
<div class="col-12 col-xl-5">
<div class="row row-cols-1 g-4 mb-3">
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-yellow-300 tw-text-yellow-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-coins"></i>
</div>
<div class="card-body">
<div class="card-title">
<div class="row">
<div class="col-6">
{{ currency.name }}
<h5 class="tw-text-yellow-400 fw-bold mb-3">{% translate 'By currency' %}</h5>
{% for currency in currency_net_worth %}
<div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100">
<div class="currency-name text-start font-monospace tw-text-gray-300">{{ currency.name }}</div>
<div class="dotted-line flex-grow-1"></div>
<div class="amount text-end font-monospace" data-original-value="{% currency_display amount=currency.amount prefix=currency.prefix suffix=currency.suffix decimal_places=currency.decimal_places %}"></div>
</div>
<div class="col-6 text-end">
<div class="amount" data-original-value="{% currency_display amount=currency.amount prefix=currency.prefix suffix=currency.suffix decimal_places=currency.decimal_places %}"></div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-blue-300 tw-text-blue-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-wallet"></i>
</div>
<div class="card-body">
<h5 class="tw-text-blue-400 fw-bold mb-3">{% translate 'By account' %}</h5>
{% for group_id, group_data in account_net_worth.items %}
{% if group_id %}
<div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100">
<div class="text-start font-monospace tw-text-gray-300"><span class="badge !tw-bg-blue-300
!tw-text-blue-800">
{{ group_data.name }}</span></div>
</div>
</div>
{% for account_id, account_data in group_data.accounts.items %}
<div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100">
<div class="text-start font-monospace tw-text-gray-300">
<span class="hierarchy-line-icon-4"></span>{{ account_data.name }}</div>
<div class="dotted-line flex-grow-1"></div>
<div class="amount" data-original-value="{% currency_display amount=account_data.balance prefix=account_data.currency.prefix suffix=account_data.currency.suffix decimal_places=account_data.currency.decimal_places%}"></div>
</div>
</div>
{% if account_data.exchange %}
<div class="amount text-end tw-text-gray-400" data-original-value=
"{% currency_display amount=account_data.exchange.amount prefix=account_data.exchange.prefix suffix=account_data.exchange.suffix decimal_places=account_data.exchange.decimal_places%}"></div>
{% endif %}
{% endfor %}
{% else %}
{% for account_id, account_data in group_data.accounts.items %}
<div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100">
<div class="currency-name text-start font-monospace tw-text-gray-300">{{ account_data.name }}</div>
<div class="dotted-line flex-grow-1"></div>
<div class="amount" data-original-value="{% currency_display amount=account_data.balance prefix=account_data.currency.prefix suffix=account_data.currency.suffix decimal_places=account_data.currency.decimal_places%}"></div>
{% if account_data.exchange %}
<div class="amount text-end tw-text-gray-400" data-original-value=
"{% currency_display amount=account_data.balance prefix=account_data.currency.prefix suffix=account_data.currency.suffix decimal_places=account_data.currency.decimal_places%}"></div>
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-7 h-100">
<canvas id="historicalNetWorthChart"></canvas>
</div>
<div class="col-6">oi</div>
</div>
<script>
var historicalData = {{ historical_data_json|safe }};
var currencies = [
{% for currency in currencies %}
"{{ currency.code }}",
{% endfor %}
];
var labels = Object.keys(historicalData);
var datasets = currencies.map((currency, index) => ({
label: currency,
data: labels.map(date => parseFloat(historicalData[date][currency])),
color: `hsl(${index * 360 / currencies.length}, 70%, 50%)`,
yAxisID: `y-axis-${index}`,
}));
var ctx = document.getElementById('historicalNetWorthChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: datasets
},
options: {
responsive: true,
interaction: {
mode: 'index',
intersect: false,
},
scales: {
x: {
type: 'category',
title: {
display: true,
text: 'Date'
}
},
...currencies.reduce((acc, currency, index) => {
acc[`y-axis-${index}`] = {
type: 'linear',
beginAtZero: true,
grace: '50%',
display: true,
grid: {
drawOnChartArea: false,
},
ticks: {
display: false // This hides the tick labels (numbers)
},
};
return acc;
}, {})
},
plugins: {
title: {
display: true,
text: 'Historical Net Worth by Currency'
},
tooltip: {
mode: 'index',
intersect: false,
}
}
}
});
</script>
</div>
</div>
{#<canvas id="chart" width="500" height="300"></canvas>#}
{##}

View File

@@ -38,9 +38,7 @@
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'tag_edit' tag_id=tag.id %}"
hx-target="#generic-offcanvas"
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install tooltip">
_="install tooltip">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="text-danger text-decoration-none p-1 tag-action"
role="button"
@@ -48,9 +46,7 @@
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'tag_delete' tag_id=tag.id %}"
hx-trigger='delete_confirmed'
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install tooltip
_="install tooltip
on click
if event.ctrlKey trigger delete_confirmed
else

View File

@@ -0,0 +1,39 @@
{% load natural %}
{% load i18n %}
{% regroup transactions by date|customnaturaldate as transactions_by_date %}
<div>
{% for x in transactions_by_date %}
<div>
<div class="my-3 w-100 tw-text-base border-bottom bg-body">
<a class="text-decoration-none d-inline-block w-100"
role="button"
data-bs-toggle="collapse"
data-bs-target="#{{ x.grouper|slugify }}"
id="#{{ x.grouper|slugify }}-collapsible"
aria-expanded="true"
aria-controls="collapseExample">
{{ x.grouper }}
</a>
</div>
<div class="collapse show" id="{{ x.grouper|slugify }}">
<div class="ps-3">
{% for trans in x.list %}
{% include 'transactions/fragments/item.html' with transaction=trans %}
{% endfor %}
</div>
</div>
</div>
{% empty %}
<div class="row p-5">
<div class="col p-5">
<div class="text-center">
<i class="fa-solid fa-circle-xmark tw-text-6xl"></i>
<p class="lead mt-4 mb-0">{% translate "No transactions this month" %}</p>
<p class="tw-text-gray-500">{% translate "Try adding one" %}</p>
</div>
</div>
</div>
{% endfor %}
</div>

View File

@@ -0,0 +1,51 @@
{% extends 'extends/offcanvas.html' %}
{% load month_name %}
{% load i18n %}
{% block title %}{% translate 'Pick a month' %}{% endblock %}
{% block body %}
{% regroup month_year_data by year as years_list %}
<ul class="nav nav-pills nav-fill" id="yearTabs" role="tablist">
{% for x in years_list %}
<li class="nav-item" role="presentation">
<button class="nav-link{% if x.grouper == current_year %} active{% endif %}"
id="{{ x.grouper }}"
data-bs-toggle="tab"
data-bs-target="#{{ x.grouper }}-pane"
type="button"
role="tab"
aria-controls="{{ x.grouper }}-pane"
aria-selected="{% if x.grouper == current_year %}true{% else %}false{% endif %}">
{{ x.grouper }}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="yearTabsContent" hx-boost="true">
{% for x in years_list %}
<div class="tab-pane fade{% if x.grouper == current_year %} show active{% endif %} mt-2"
id="{{ x.grouper }}-pane"
role="tabpanel"
aria-labelledby="{{ x.grouper }}"
tabindex="0">
<ul class="list-group list-group-flush" id="month-year-list">
{% for month_data in x.list %}
<li class="list-group-item hover:tw-bg-zinc-900
{% if month_data.month == current_month and month_data.year == current_year %} disabled bg-primary{% endif %}"
{% if month_data.month == current_month and month_data.year == current_year %}aria-disabled="true"{% endif %}>
<div class="d-flex justify-content-between">
<a class="text-decoration-none stretched-link {% if month_data.month == current_month and month_data.year == current_year %} text-black{% endif %}"
href="{% url "monthly_overview" month=month_data.month year=month_data.year %}">
{{ month_data.month|month_name }}</a>
<span class="badge text-bg-secondary">{{ month_data.transaction_count }}</span>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,166 @@
{% load i18n %}
{% load currency_display %}
<div class="row row-cols-1 g-4 mb-3">
{# Daily Spending#}
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-yellow-300 tw-text-yellow-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-calendar-day"></i>
</div>
<div class="card-body">
<h5 class="tw-text-yellow-400 fw-bold">{% translate 'Daily Spending Allowance' %}{% include 'includes/help_icon.html' with content=_('This is the final total divided by the remaining days in the month') %}</h5>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'today' %}</div>
</div>
<div class="text-end font-monospace">
{% for entry in totals.daily_spending_allowance %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{# Income#}
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-green-300 tw-text-green-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-arrow-right-to-bracket"></i>
</div>
<div class="card-body">
<h5 class="tw-text-green-400 fw-bold">{% translate 'Income' %}</h5>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current' %}</div>
</div>
<div class="text-end font-monospace">
{% for entry in totals.paid_income %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<hr class="my-1">
<div class="d-flex justify-content-between">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected' %}</div>
</div>
<div class="text-end font-monospace">
{% for entry in totals.projected_income %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{# Expenses#}
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-red-300 tw-text-red-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-arrow-right-from-bracket"></i>
</div>
<div class="card-body">
<h5 class="tw-text-red-400">{% translate 'Expenses' %}</h5>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current' %}</div>
</div>
<div class="text-end font-monospace">
{% for entry in totals.paid_expenses %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<hr class="my-1">
<div class="d-flex justify-content-between">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected' %}</div>
</div>
<div class="text-end font-monospace">
{% for entry in totals.projected_expenses %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{# Total#}
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-blue-300 tw-text-blue-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-scale-balanced"></i>
</div>
<div class="card-body">
<h5 class="tw-text-blue-400">{% translate 'Total' %}</h5>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current' %}</div>
</div>
<div class="text-end font-monospace">
{% for entry in totals.total_current %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected' %}</div>
</div>
<div class="text-end font-monospace">
{% for entry in totals.total_projected %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<hr class="my-1">
<div class="d-flex justify-content-end">
<div class="text-end font-monospace">
{% for entry in totals.total_final %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{#<div class="p-2 rounded-2 shadow tw-text-sm card mt-4">#}
{# <p class="font-monospace text-light text-uppercase text-center fw-bold m-0 tw-text-base">#}
{# {% translate "Account Overview" %}</p>#}
{# <hr class="my-1">#}
{# <div>#}
{# {% for account in account_summary %}#}
{# <div class="row">#}
{# <div class="col-6">#}
{# <div class="font-monospace text-primary text-start align-self-end fw-bold m-0">{{ account.name }}</div>#}
{# </div>#}
{# <div class="col-6 text-end font-monospace">#}
{# <div class="amount" data-original-value="{% currency_display amount=account.balance prefix=account.currency__prefix suffix=account.currency__suffix decimal_places=account.currency__decimal_places %}"></div>#}
{# </div>#}
{# </div>#}
{# <div class="my-1"></div>#}
{# {% endfor %}#}
{# </div>#}
{#</div>#}

View File

@@ -0,0 +1,197 @@
{% extends "layouts/base.html" %}
{% load currency_display %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load month_name %}
{% load static %}
{% load webpack_loader %}
{% block title %}{% translate 'Yearly Overview' %} :: {{ year }}{% endblock %}
{% block body_hyperscript %}
on keyup[code is 'KeyE' and target.nodeName is 'BODY'] from body trigger 'add_expense' end
on keyup[code is 'KeyI' and target.nodeName is 'BODY'] from body trigger 'add_income' end
on keyup[code is 'KeyB' and target.nodeName is 'BODY'] from body trigger 'balance' end
on keyup[code is 'KeyT' and target.nodeName is 'BODY'] from body trigger 'add_transfer' end
on keyup[code is 'KeyN' and target.nodeName is 'BODY'] from body trigger 'installment' end
on keyup[code is 'ArrowLeft' and target.nodeName is 'BODY'] from body trigger 'previous_month' end
on keyup[code is 'ArrowRight' and target.nodeName is 'BODY'] from body trigger 'next_month' end
{% endblock %}
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
{# <div class="row mb-3 gx-xl-4 gy-3 mb-4">#}
{# Date picker#}
{# <div class="col-12 col-xl-4 flex-row align-items-center d-flex">#}
{# <div class="tw-text-base h-100 align-items-center d-flex">#}
{# <a role="button"#}
{# class="pe-4 py-2"#}
{# hx-boost="true"#}
{# hx-trigger="click, previous_month from:window"#}
{# href="{% url 'monthly_overview' month=previous_month year=previous_year %}"><i#}
{# class="fa-solid fa-chevron-left"></i></a>#}
{# </div>#}
{# <div class="tw-text-3xl fw-bold font-monospace tw-w-full text-center"#}
{# hx-get="{% url 'available_dates' %}"#}
{# hx-target="#generic-offcanvas-left"#}
{# hx-trigger="click, date_picker from:window"#}
{# hx-vals='{"month": {{ month }}, "year": {{ year }}}' role="button">#}
{# {{ month|month_name }} {{ year }}#}
{# </div>#}
{# <div class="tw-text-base mx-2 h-100 align-items-center d-flex">#}
{# <a role="button"#}
{# class="ps-3 py-2"#}
{# hx-boost="true"#}
{# hx-trigger="click, next_month from:window"#}
{# href="{% url 'monthly_overview' month=next_month year=next_year %}">#}
{# <i class="fa-solid fa-chevron-right"></i>#}
{# </a>#}
{# </div>#}
{# </div>#}
{# Action buttons#}
{# <div class="col-12 col-xl-8">#}
{# <div class="d-grid gap-2 d-xl-flex justify-content-xl-end">#}
{# <button class="btn btn-sm btn-outline-success"#}
{# hx-get="{% url 'transaction_add' %}"#}
{# hx-target="#generic-offcanvas"#}
{# hx-trigger="click, add_income from:window"#}
{# hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "IN"}'>#}
{# <i class="fa-solid fa-arrow-right-to-bracket me-2"></i>#}
{# {% translate "Income" %}#}
{# </button>#}
{# <button class="btn btn-sm btn-outline-danger"#}
{# hx-get="{% url 'transaction_add' %}"#}
{# hx-target="#generic-offcanvas"#}
{# hx-trigger="click, add_expense from:window"#}
{# hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "EX"}'>#}
{# <i class="fa-solid fa-arrow-right-from-bracket me-2"></i>#}
{# {% translate "Expense" %}#}
{# </button>#}
{# <button class="btn btn-sm btn-outline-warning"#}
{# hx-get="{% url 'installments_add' %}"#}
{# hx-trigger="click, installment from:window"#}
{# hx-target="#generic-offcanvas">#}
{# <i class="fa-solid fa-divide me-2"></i>#}
{# {% translate "Installment" %}#}
{# </button>#}
{# <button class="btn btn-sm btn-outline-info"#}
{# hx-get="{% url 'transactions_transfer' %}"#}
{# hx-target="#generic-offcanvas"#}
{# hx-trigger="click, add_transfer from:window"#}
{# hx-vals='{"year": {{ year }}, "month": {{ month }}}'>#}
{# <i class="fa-solid fa-money-bill-transfer me-2"></i>#}
{# {% translate "Transfer" %}#}
{# </button>#}
{# <button class="btn btn-sm btn-outline-info"#}
{# hx-get="{% url 'account_reconciliation' %}"#}
{# hx-trigger="click, balance from:window"#}
{# hx-target="#generic-offcanvas">#}
{# <i class="fa-solid fa-scale-balanced me-2"></i>#}
{# {% translate "Balance" %}#}
{# </button>#}
{# </div>#}
{# </div>#}
{# </div>#}
{# Monthly summary#}
<div class="row gx-xl-4 gy-3">
{# <div class="col-12 col-xl-4 order-0 order-xl-2">#}
{# <div id="summary" hx-get="{% url 'monthly_summary' month=month year=year %}" class="sticky-sidebar"#}
{# hx-trigger="load, updated from:window, monthly_summary_update from:window">#}
{# </div>#}
{# </div>#}
<div class="col-12">
{# Filter transactions#}
{# <div class="row mb-1">#}
{# <div class="col-12">#}
{# <div class="dropdown">#}
{# <button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown"#}
{# aria-expanded="false" data-bs-auto-close="false">#}
{# <i class="fa-solid fa-filter fa-fw me-2"></i>{% translate 'Filter transactions' %}#}
{# </button>#}
{# <form hx-get="{% url 'monthly_transactions_list' month=month year=year %}" hx-trigger="change, submit, search"#}
{# hx-target="#transactions" id="filter" hx-indicator="#transactions"#}
{# class="dropdown-menu p-4 w-lg-50 tw-w-full lg:tw-w-2/4"#}
{# _="install init_tom_select">#}
{# {% crispy filter.form %}#}
{# </form>#}
{# </div>#}
{# </div>#}
{# </div>#}
<div class="row">
<div class="no-more-tables">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{% translate 'Month' %}</th>
<td>{% translate 'Projected Income' %}</td>
<td>{% translate 'Projected Expenses' %}</td>
<td>{% translate 'Projected Total' %}</td>
<td>{% translate 'Current Income' %}</td>
<td>{% translate 'Current Expenses' %}</td>
<td>{% translate 'Current Total' %}</td>
<td>{% translate 'Final Total' %}</td>
</tr>
</thead>
<tbody>
{% for date, x in totals.items %}
<tr>
<th scope="row">{{ date.month|month_name }}</th>
<td class="!tw-text-green-400">
{% for data in x.income_unpaid %}
<div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</td>
<td class="!tw-text-red-400">
{% for data in x.expense_unpaid %}
<div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</td>
<td>
{% for data in x.balance_unpaid %}
<div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</td>
<td class="!tw-text-green-400">
{% for data in x.income_paid %}
<div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</td>
<td class="!tw-text-red-400">
{% for data in x.expense_paid %}
<div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</td>
<td>
{% for data in x.balance_paid %}
<div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</td>
<td>
{% for data in x.balance_total %}
<div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,203 @@
{% extends "layouts/base.html" %}
{% load currency_display %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load month_name %}
{% load static %}
{% load webpack_loader %}
{% block title %}{% translate 'Yearly Overview' %} :: {{ year }}{% endblock %}
{% block body_hyperscript %}
on keyup[code is 'ArrowLeft' and target.nodeName is 'BODY'] from body trigger 'previous_month' end
on keyup[code is 'ArrowRight' and target.nodeName is 'BODY'] from body trigger 'next_month' end
{% endblock %}
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
{# <div class="row mb-3 gx-xl-4 gy-3 mb-4">#}
{# Date picker#}
{# <div class="col-12 col-xl-4 flex-row align-items-center d-flex">#}
{# <div class="tw-text-base h-100 align-items-center d-flex">#}
{# <a role="button"#}
{# class="pe-4 py-2"#}
{# hx-boost="true"#}
{# hx-trigger="click, previous_month from:window"#}
{# href="{% url 'monthly_overview' month=previous_month year=previous_year %}"><i#}
{# class="fa-solid fa-chevron-left"></i></a>#}
{# </div>#}
{# <div class="tw-text-3xl fw-bold font-monospace tw-w-full text-center"#}
{# hx-get="{% url 'available_dates' %}"#}
{# hx-target="#generic-offcanvas-left"#}
{# hx-trigger="click, date_picker from:window"#}
{# hx-vals='{"month": {{ month }}, "year": {{ year }}}' role="button">#}
{# {{ month|month_name }} {{ year }}#}
{# </div>#}
{# <div class="tw-text-base mx-2 h-100 align-items-center d-flex">#}
{# <a role="button"#}
{# class="ps-3 py-2"#}
{# hx-boost="true"#}
{# hx-trigger="click, next_month from:window"#}
{# href="{% url 'monthly_overview' month=next_month year=next_year %}">#}
{# <i class="fa-solid fa-chevron-right"></i>#}
{# </a>#}
{# </div>#}
{# </div>#}
{# Action buttons#}
{# <div class="col-12 col-xl-8">#}
{# <div class="d-grid gap-2 d-xl-flex justify-content-xl-end">#}
{# <button class="btn btn-sm btn-outline-success"#}
{# hx-get="{% url 'transaction_add' %}"#}
{# hx-target="#generic-offcanvas"#}
{# hx-trigger="click, add_income from:window"#}
{# hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "IN"}'>#}
{# <i class="fa-solid fa-arrow-right-to-bracket me-2"></i>#}
{# {% translate "Income" %}#}
{# </button>#}
{# <button class="btn btn-sm btn-outline-danger"#}
{# hx-get="{% url 'transaction_add' %}"#}
{# hx-target="#generic-offcanvas"#}
{# hx-trigger="click, add_expense from:window"#}
{# hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "EX"}'>#}
{# <i class="fa-solid fa-arrow-right-from-bracket me-2"></i>#}
{# {% translate "Expense" %}#}
{# </button>#}
{# <button class="btn btn-sm btn-outline-warning"#}
{# hx-get="{% url 'installments_add' %}"#}
{# hx-trigger="click, installment from:window"#}
{# hx-target="#generic-offcanvas">#}
{# <i class="fa-solid fa-divide me-2"></i>#}
{# {% translate "Installment" %}#}
{# </button>#}
{# <button class="btn btn-sm btn-outline-info"#}
{# hx-get="{% url 'transactions_transfer' %}"#}
{# hx-target="#generic-offcanvas"#}
{# hx-trigger="click, add_transfer from:window"#}
{# hx-vals='{"year": {{ year }}, "month": {{ month }}}'>#}
{# <i class="fa-solid fa-money-bill-transfer me-2"></i>#}
{# {% translate "Transfer" %}#}
{# </button>#}
{# <button class="btn btn-sm btn-outline-info"#}
{# hx-get="{% url 'account_reconciliation' %}"#}
{# hx-trigger="click, balance from:window"#}
{# hx-target="#generic-offcanvas">#}
{# <i class="fa-solid fa-scale-balanced me-2"></i>#}
{# {% translate "Balance" %}#}
{# </button>#}
{# </div>#}
{# </div>#}
{# </div>#}
{# Monthly summary#}
<div class="row gx-xl-4 gy-3">
{# <div class="col-12 col-xl-4 order-0 order-xl-2">#}
{# <div id="summary" hx-get="{% url 'monthly_summary' month=month year=year %}" class="sticky-sidebar"#}
{# hx-trigger="load, updated from:window, monthly_summary_update from:window">#}
{# </div>#}
{# </div>#}
<div class="col-12">
{# Filter transactions#}
{# <div class="row mb-1">#}
{# <div class="col-12">#}
{# <div class="dropdown">#}
{# <button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown"#}
{# aria-expanded="false" data-bs-auto-close="false">#}
{# <i class="fa-solid fa-filter fa-fw me-2"></i>{% translate 'Filter transactions' %}#}
{# </button>#}
{# <form hx-get="{% url 'monthly_transactions_list' month=month year=year %}" hx-trigger="change, submit, search"#}
{# hx-target="#transactions" id="filter" hx-indicator="#transactions"#}
{# class="dropdown-menu p-4 w-lg-50 tw-w-full lg:tw-w-2/4"#}
{# _="install init_tom_select">#}
{# {% crispy filter.form %}#}
{# </form>#}
{# </div>#}
{# </div>#}
{# </div>#}
<div class="row">
{{ year }}
{% for date, x in data.items %}
<div class="card shadow mb-3 mt-3">
<div class="card-body">
<h5 class="card-title">{{ date.month|month_name }}</h5>
{{ x }}
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
{% endfor %}
{# <div class="no-more-tables">#}
{# <table class="table table-hover">#}
{# <thead>#}
{# <tr>#}
{# <th scope="col">{% translate 'Month' %}</th>#}
{# <td>{% translate 'Projected Income' %}</td>#}
{# <td>{% translate 'Projected Expenses' %}</td>#}
{# <td>{% translate 'Projected Total' %}</td>#}
{# <td>{% translate 'Current Income' %}</td>#}
{# <td>{% translate 'Current Expenses' %}</td>#}
{# <td>{% translate 'Current Total' %}</td>#}
{# <td>{% translate 'Final Total' %}</td>#}
{# </tr>#}
{# </thead>#}
{# <tbody>#}
{# {% for date, x in totals.items %}#}
{# <tr>#}
{# <th scope="row">{{ date.month|month_name }}</th>#}
{# <td class="!tw-text-green-400">#}
{# {% for data in x.income_unpaid %}#}
{# <div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>#}
{# {% empty %}#}
{# <div>-</div>#}
{# {% endfor %}#}
{# </td>#}
{# <td class="!tw-text-red-400">#}
{# {% for data in x.expense_unpaid %}#}
{# <div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>#}
{# {% empty %}#}
{# <div>-</div>#}
{# {% endfor %}#}
{# </td>#}
{# <td>#}
{# {% for data in x.balance_unpaid %}#}
{# <div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>#}
{# {% empty %}#}
{# <div>-</div>#}
{# {% endfor %}#}
{# </td>#}
{# <td class="!tw-text-green-400">#}
{# {% for data in x.income_paid %}#}
{# <div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>#}
{# {% empty %}#}
{# <div>-</div>#}
{# {% endfor %}#}
{# </td>#}
{# <td class="!tw-text-red-400">#}
{# {% for data in x.expense_paid %}#}
{# <div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>#}
{# {% empty %}#}
{# <div>-</div>#}
{# {% endfor %}#}
{# </td>#}
{# <td>#}
{# {% for data in x.balance_paid %}#}
{# <div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>#}
{# {% empty %}#}
{# <div>-</div>#}
{# {% endfor %}#}
{# </td>#}
{# <td>#}
{# {% for data in x.balance_total %}#}
{# <div class="amount" data-original-value="{% currency_display amount=data.amount prefix=data.prefix suffix=data.suffix decimal_places=data.decimal_places %}"></div>#}
{# {% empty %}#}
{# <div>-</div>#}
{# {% endfor %}#}
{# </td>#}
{# </tr>#}
{# {% endfor %}#}
{# </tbody>#}
{# </table>#}
{# </div>#}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -27,6 +27,7 @@
"core-js": "^3.20.3",
"cross-env": "^7.0.3",
"css-loader": "^6.8.1",
"daisyui": "^4.12.13",
"eslint": "^8.7.0",
"eslint-webpack-plugin": "^3.1.1",
"htmx.org": "^2.0.1",
@@ -3654,6 +3655,15 @@
"postcss": "^8.4"
}
},
"node_modules/css-selector-tokenizer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz",
"integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==",
"dependencies": {
"cssesc": "^3.0.0",
"fastparse": "^1.1.2"
}
},
"node_modules/cssdb": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz",
@@ -3680,6 +3690,32 @@
"node": ">=4"
}
},
"node_modules/culori": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/daisyui": {
"version": "4.12.13",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.13.tgz",
"integrity": "sha512-BnXyQoOByUF/7wSdIKubyhXxbtL8gxwY3u2cNMkxGP39TSVJqMmlItqtpY903fQnLI/NokC+bc+ZV+PEPsppPw==",
"dependencies": {
"css-selector-tokenizer": "^0.8",
"culori": "^3",
"picocolors": "^1",
"postcss-js": "^4"
},
"engines": {
"node": ">=16.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/daisyui"
}
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@@ -4504,6 +4540,11 @@
"node": ">= 4.9.1"
}
},
"node_modules/fastparse": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ=="
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",

View File

@@ -40,6 +40,7 @@
"core-js": "^3.20.3",
"cross-env": "^7.0.3",
"css-loader": "^6.8.1",
"daisyui": "^4.12.13",
"eslint": "^8.7.0",
"eslint-webpack-plugin": "^3.1.1",
"htmx.org": "^2.0.1",

View File

@@ -33,84 +33,6 @@ $theme-colors: map-merge($theme-colors, (
width: 1%;
white-space: nowrap;
}
//
////body {
//// background-color: $background-color;
////}
//
//
//// ===== Scrollbar CSS =====
//* {
// scrollbar-width: thin;
// scrollbar-color: $scroll-thumb-color #dfe9eb;
//}
//
//// Chrome, Edge and Safari
//*::-webkit-scrollbar {
// height: 10px;
// width: 10px;
//}
//
//*::-webkit-scrollbar-track {
// border-radius: 5px;
// background-color: $scroll-track-color;
//}
//
//*::-webkit-scrollbar-track:hover {
// background-color: $scroll-thumb-active-color;
//}
//
//*::-webkit-scrollbar-track:active {
// background-color: $scroll-thumb-active-color;
//}
//
//*::-webkit-scrollbar-thumb {
// border-radius: 5px;
// background-color: $scroll-thumb-color;
//}
//
//*::-webkit-scrollbar-thumb:hover {
// background-color: $scroll-thumb-active-color;
//}
//
//*::-webkit-scrollbar-thumb:active {
// background-color: $scroll-thumb-active-color;
//}
//
//// ===== Scrollbar CSS =====
//
//.gradient-background {
// background: linear-gradient(109deg, #652e7c, #1a7976);
// background-size: 120% 120%;
// animation: gradient-animation 8s ease infinite;
//}
//
//@keyframes gradient-animation {
// 0% {
// background-position: 0% 50%;
// }
//
// 50% {
// background-position: 100% 50%;
// }
//
// 100% {
// background-position: 0% 50%;
// }
//}
//
//
//.fade-me-in.htmx-added {
// opacity: 0;
//}
//
//.fade-me-in {
// opacity: 1;
// transition: opacity 500ms ease-out;
//}
.offcanvas-size-xl {
--#{$prefix}offcanvas-width: min(95vw, 700px) !important;
@@ -124,3 +46,53 @@ $theme-colors: map-merge($theme-colors, (
.offcanvas-size-sm {
--#{$prefix}offcanvas-width: min(95vw, 250px) !important;
}
@media only screen and (max-width: 800px) {
/* Force table to not be like tables anymore */
.no-more-tables table,
.no-more-tables thead,
.no-more-tables tbody,
.no-more-tables th,
.no-more-tables td,
.no-more-tables tr {
display: block;
}
/* Hide table headers (but not display: none;, for accessibility) */
.no-more-tables thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
.no-more-tables tr { border: 1px solid #ccc; }
.no-more-tables td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-left: 50%;
white-space: normal;
text-align:left;
}
.no-more-tables td:before {
/* Now like a table header */
position: absolute;
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
text-align:left;
font-weight: bold;
}
/*
Label the data
*/
.no-more-tables td:before { content: attr(data-title); }
}

View File

@@ -24,3 +24,23 @@ select[multiple] {
color: $gray-800;
background-color: $primary;
}
.dotted-line {
border-bottom: 2px dotted $gray-700; /* Adjust color as needed */
margin: 0 5px;
position: relative;
}
.hierarchy-line-icon-4 {
width: 8px;
height: 10px;
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTMiIGhlaWdodD0iNSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiPjxwYXRoIHN0cm9rZT0iIzlCOUI5QiIgc3Ryb2tlLWRhc2hhcnJheT0iNCIgZD0iTS41IDQuNWgxMiIvPjxwYXRoIHN0cm9rZT0iIzk3OTc5NyIgZD0iTS41IDQuNXYtNCIvPjwvZz48L3N2Zz4=);
background-size: contain;
background-repeat: no-repeat;
background-position: top;
display: inline-block;
margin-left: 15px;
margin-right: 7px;
margin-top: 4px;
padding: 0 8px !important;
}

View File

@@ -6,9 +6,19 @@ module.exports = {
variants: {
extend: {},
},
plugins: [],
plugins: [require('daisyui')],
prefix: 'tw-',
corePlugins: {
preflight: false,
},
}
daisyui: {
themes: false, // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"]
darkTheme: "dark", // name of one of the included themes for dark mode
base: false, // applies background color and foreground color for root element by default
styled: true, // include daisyUI colors and design decisions for all components
utils: true, // adds responsive and modifier utility classes
prefix: "ds-", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors)
logs: true, // Shows info about daisyUI version and used config in the console when building your CSS
themeRoot: ":root", // The element that receives theme color CSS variables
},
};