- Yearly view: replace N*12 queries with annotate/ExtractMonth (6 queries) - Fixed expenses: bulk_create instead of get_or_create loop - Use DB aggregate(Sum) instead of Python sum() for totals - Remove 10 unused CRUD views, 13 URLs, 6 templates, 3 forms - Fix overly broad Exception catch in save_expense - Move update_budget to app/monthly/services.py (no cross-handler imports)
97 lines
2.9 KiB
Python
97 lines
2.9 KiB
Python
from decimal import Decimal, InvalidOperation
|
|
|
|
from django.db.models import Sum
|
|
from django.template.loader import render_to_string
|
|
from liveview import liveview_handler, send
|
|
|
|
from app.expenses.models import FixedExpenseConcept
|
|
from app.expenses.templatetags.money import format_money
|
|
from app.monthly.models import MonthlyFixedExpense
|
|
from app.monthly.services import update_budget
|
|
|
|
|
|
def _ensure_entries_exist(year, month):
|
|
"""Create missing MonthlyFixedExpense entries in bulk."""
|
|
concepts = FixedExpenseConcept.objects.all()
|
|
existing_ids = set(
|
|
MonthlyFixedExpense.objects.filter(year=year, month=month).values_list(
|
|
"concept_id", flat=True
|
|
)
|
|
)
|
|
to_create = [
|
|
MonthlyFixedExpense(year=year, month=month, concept=c)
|
|
for c in concepts
|
|
if c.id not in existing_ids
|
|
]
|
|
if to_create:
|
|
MonthlyFixedExpense.objects.bulk_create(to_create)
|
|
|
|
|
|
def _render_fixed_expenses_table(year, month):
|
|
_ensure_entries_exist(year, month)
|
|
entries = MonthlyFixedExpense.objects.filter(year=year, month=month).select_related(
|
|
"concept"
|
|
)
|
|
total = entries.aggregate(total=Sum("amount"))["total"] or Decimal("0")
|
|
return render_to_string(
|
|
"pages/monthly/partials/fixed_expenses_table.html",
|
|
{"entries": entries, "total": total, "year": year, "month": month},
|
|
)
|
|
|
|
|
|
@liveview_handler("save_monthly_fixed_expense")
|
|
def save_monthly_fixed_expense(consumer, content):
|
|
form = content.get("form", {})
|
|
entry_id = form.get("entry_id", "")
|
|
year = int(form.get("year", 0))
|
|
month = int(form.get("month", 0))
|
|
amount_raw = form.get("fe_amount", "").replace(",", ".")
|
|
|
|
if not entry_id or not year or not month:
|
|
return
|
|
|
|
try:
|
|
amount = Decimal(amount_raw) if amount_raw else Decimal("0")
|
|
entry = MonthlyFixedExpense.objects.get(id=entry_id)
|
|
entry.amount = amount
|
|
entry.save()
|
|
except (InvalidOperation, ValueError, MonthlyFixedExpense.DoesNotExist):
|
|
return
|
|
|
|
total = MonthlyFixedExpense.objects.filter(year=year, month=month).aggregate(
|
|
total=Sum("amount")
|
|
)["total"] or Decimal("0")
|
|
send(consumer, {"target": "#fe-total", "html": f"{format_money(total)} €"})
|
|
update_budget(consumer, year, month)
|
|
|
|
|
|
@liveview_handler("clone_fixed_expenses")
|
|
def clone_fixed_expenses(consumer, content):
|
|
form = content.get("form", {})
|
|
source = form.get("clone_fe_source", "")
|
|
year = int(form.get("year", 0))
|
|
month = int(form.get("month", 0))
|
|
|
|
if not source or not year or not month:
|
|
return
|
|
|
|
try:
|
|
src_year, src_month = source.split("-")
|
|
src_year, src_month = int(src_year), int(src_month)
|
|
except ValueError:
|
|
return
|
|
|
|
source_entries = MonthlyFixedExpense.objects.filter(
|
|
year=src_year, month=src_month
|
|
).select_related("concept")
|
|
|
|
_ensure_entries_exist(year, month)
|
|
for src in source_entries:
|
|
MonthlyFixedExpense.objects.filter(
|
|
year=year, month=month, concept=src.concept
|
|
).update(amount=src.amount)
|
|
|
|
html = _render_fixed_expenses_table(year, month)
|
|
send(consumer, {"target": "#fixed-expenses-table", "html": html})
|
|
update_budget(consumer, year, month)
|