mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-03-25 10:51:44 +01:00
more changes
This commit is contained in:
@@ -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")),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
0
app/apps/yearly_overview/__init__.py
Normal file
0
app/apps/yearly_overview/__init__.py
Normal file
6
app/apps/yearly_overview/apps.py
Normal file
6
app/apps/yearly_overview/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class YearlyOverviewConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.yearly_overview"
|
||||
0
app/apps/yearly_overview/migrations/__init__.py
Normal file
0
app/apps/yearly_overview/migrations/__init__.py
Normal file
12
app/apps/yearly_overview/urls.py
Normal file
12
app/apps/yearly_overview/urls.py
Normal 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
app/apps/yearly_overview/views.py
Normal file
468
app/apps/yearly_overview/views.py
Normal 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,
|
||||
# },
|
||||
# )
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>#}
|
||||
{##}
|
||||
|
||||
@@ -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
|
||||
|
||||
39
app/templates/yearly_overview/fragments/list.html
Normal file
39
app/templates/yearly_overview/fragments/list.html
Normal 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>
|
||||
@@ -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 %}
|
||||
166
app/templates/yearly_overview/fragments/monthly_summary.html
Normal file
166
app/templates/yearly_overview/fragments/monthly_summary.html
Normal 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>#}
|
||||
197
app/templates/yearly_overview/pages/overview.html
Normal file
197
app/templates/yearly_overview/pages/overview.html
Normal 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 %}
|
||||
203
app/templates/yearly_overview/pages/overview2.html
Normal file
203
app/templates/yearly_overview/pages/overview2.html
Normal 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 %}
|
||||
41
frontend/package-lock.json
generated
41
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user