expenses_manager/expenses_manager/expenses/views.py
JKuijperM e814bc2d03 Anade filtro por tipo en objetivos y unifica la barra de progreso
El listado de objetivos mezclaba pagos, presupuestos y ahorros sin forma de
separarlos. Anade un filtro por tipo, con el mismo patron que los filtros de
gastos, y ordena los objetivos por nombre. Un valor de "kind" que no este en
KIND_CHOICES se ignora en vez de romper.

De paso, la barra de progreso pasa a usar goal.progress_state en lugar de
decidir el color en la plantilla con un if sobre el porcentaje. La logica ya
vivia en el modelo, que ademas distingue presupuestos (donde acercarse al
100% es un aviso) del resto; la plantilla del dashboard seguia con las clases
antiguas low/medium/high, que se retiran del CSS.

Incluye estado vacio y thead/tbody en la tabla, que le faltaban.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:16:58 +02:00

1167 lines
34 KiB
Python

import logging
import calendar
from datetime import date, datetime
from decimal import Decimal
from django.contrib import messages
from .models import Account, Category, Expense, FuelEntry, Tag, Income, Goal
from .forms import (
ExpenseForm,
IncomeForm,
TagForm,
AccountForm,
FuelEntryForm,
CategoryForm,
GoalForm,
)
from django.core.paginator import Paginator
from django.db.models import Sum, Count, ProtectedError
from django.db.models.functions import ExtractMonth, ExtractYear, ExtractDay
from django.contrib.auth.decorators import login_required
from django.utils.http import url_has_allowed_host_and_scheme
from django.shortcuts import get_object_or_404, render, redirect
logger = logging.getLogger(__name__)
def _get_int(value):
try:
return int(value)
except (TypeError, ValueError):
return None
def _safe_next(request):
next_url = request.POST.get("next") or request.GET.get("next")
if next_url and url_has_allowed_host_and_scheme(
next_url,
allowed_hosts={request.get_host()},
require_https=request.is_secure(),
):
return next_url
return ""
def _redirect_back(request, fallback):
next_url = _safe_next(request)
return redirect(next_url or fallback)
def sub_months(year, month, n):
month -= n
while month <= 0:
month += 12
year -= 1
return year, month
def _category_tree(categories):
"""Aplana las categorías en preorden (padres antes que hijos, cada nivel
ordenado por nombre) junto con su profundidad, para poder indentarlas."""
by_parent = {}
for category in categories:
by_parent.setdefault(category.parent_id, []).append(category)
for children in by_parent.values():
children.sort(key=lambda c: c.name)
rows = []
def walk(parent_id, depth):
for category in by_parent.get(parent_id, []):
rows.append({"category": category, "depth": depth})
walk(category.id, depth + 1)
walk(None, 0)
return rows
def _account_balances(accounts):
"""Calcula el saldo de cada cuenta con dos queries agregadas (nada de
account.current_balance() en bucle, que dispararía N+1)."""
account_ids = [a.id for a in accounts]
expense_totals = {
row["account_id"]: row["total"]
for row in Expense.objects.filter(account_id__in=account_ids)
.values("account_id")
.annotate(total=Sum("amount"))
}
income_totals = {
row["account_id"]: row["total"]
for row in Income.objects.filter(account_id__in=account_ids)
.values("account_id")
.annotate(total=Sum("amount"))
}
account_balances = []
negative_accounts = []
total_balance = Decimal("0")
for acc in accounts:
balance = (
acc.initial_balance
+ (income_totals.get(acc.id) or Decimal("0"))
- (expense_totals.get(acc.id) or Decimal("0"))
)
total_balance += balance
account_balances.append({"account": acc, "balance": balance})
if balance < 0:
negative_accounts.append({"account": acc, "balance": balance})
return account_balances, negative_accounts, total_balance
@login_required
def home(request):
today = date.today()
expenses = Expense.objects.filter(owner=request.user)
# ---- KPIs del mes en curso ----
month_expenses = expenses.filter(date__year=today.year, date__month=today.month)
kpi_total = month_expenses.aggregate(total=Sum("amount"))["total"] or Decimal("0")
kpi_count = month_expenses.count()
kpi_categories = month_expenses.values("category").distinct().count()
# ---- Comparativa con el mes anterior ----
prev_year, prev_month = sub_months(today.year, today.month, 1)
prev_total = (
expenses.filter(date__year=prev_year, date__month=prev_month).aggregate(
total=Sum("amount")
)["total"]
or Decimal("0")
)
diff_amount = kpi_total - prev_total
# None cuando no hay mes anterior con gastos (evita división por cero)
diff_pct = None
if prev_total:
diff_pct = (diff_amount / prev_total) * 100
# ---- Saldos de cuentas ----
accounts = list(Account.objects.filter(owner=request.user, active=True))
account_balances, negative_accounts, total_balance = _account_balances(accounts)
# ---- Objetivos ----
# Una sola query reutilizada para "goals" (home) y "exceeded_goals" (alertas).
# is_exceeded() solo aplica a kind == budget, así que pago y ahorro nunca la disparan.
all_goals = list(
Goal.objects.filter(owner=request.user).select_related("category", "account")
)
goals = [g for g in all_goals if g.show_on_home]
exceeded_goals = [g for g in all_goals if g.is_exceeded()]
has_alerts = bool(exceeded_goals or negative_accounts)
# ---- Últimos movimientos (gastos + ingresos mezclados) ----
recent_expenses = expenses.select_related("category", "account").order_by(
"-date", "-id"
)[:8]
recent_incomes = (
Income.objects.filter(owner=request.user)
.select_related("account")
.order_by("-date", "-id")[:8]
)
movements = sorted(
[
{
"kind": "expense",
"date": e.date,
"label": e.category.name,
"account": e.account.name,
"amount": e.amount,
}
for e in recent_expenses
]
+ [
{
"kind": "income",
"date": i.date,
"label": i.name,
"account": i.account.name,
"amount": i.amount,
}
for i in recent_incomes
],
key=lambda m: m["date"],
reverse=True,
)[:8]
# ---- Mini-gráfico de 6 meses (sin cambios) ----
six_months = []
for i in range(5, -1, -1):
y, m = sub_months(today.year, today.month, i)
six_months.append((y, m))
mini_data = []
for y, m in six_months:
total = (
expenses.filter(date__year=y, date__month=m).aggregate(total=Sum("amount"))[
"total"
]
or 0
)
mini_data.append({"label": f"{m}/{y}", "total": float(total)})
return render(
request,
"expenses/home.html",
{
"active_menu": "home",
"kpi_total": kpi_total,
"kpi_count": kpi_count,
"kpi_categories": kpi_categories,
"prev_total": prev_total,
"diff_amount": diff_amount,
"diff_pct": diff_pct,
"total_balance": total_balance,
"account_balances": account_balances,
"negative_accounts": negative_accounts,
"goals": goals,
"exceeded_goals": exceeded_goals,
"has_alerts": has_alerts,
"movements": movements,
"mini_chart_labels": [x["label"] for x in mini_data],
"mini_chart_data": [x["total"] for x in mini_data],
},
)
@login_required
def expense_list(request):
expenses = Expense.objects.filter(owner=request.user)
categories = Category.objects.filter(owner=request.user)
year_list = Expense.objects.filter(owner=request.user).dates("date", "year")
months = list(range(1, 13))
# Filters
year = _get_int(request.GET.get("year"))
month = _get_int(request.GET.get("month"))
category = _get_int(request.GET.get("category"))
account_id = _get_int(request.GET.get("account"))
tag_ids = [
t_id
for t in request.GET.getlist("tag")
if (t_id:= _get_int(t)) is not None
]
if year:
expenses = expenses.filter(date__year=year)
if month:
expenses = expenses.filter(date__month=month)
if category:
expenses = expenses.filter(category_id=category)
if tag_ids:
expenses = expenses.filter(tags__id__in=tag_ids).distinct()
if account_id:
expenses = expenses.filter(account_id=account_id)
selected_tags = tag_ids or []
expenses = expenses.order_by("-date")
total_amount = expenses.aggregate(total=Sum("amount"))["total"] or 0
expense_count = expenses.count()
category_count = expenses.values("category").distinct().count()
# Pagination
paginator = Paginator(expenses, 10)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
query_params = request.GET.copy()
query_params.pop("page", None)
# tags with state
tags_with_state = []
for tag in Tag.objects.filter(owner=request.user):
if tag.id in selected_tags:
new_tags = [t for t in selected_tags if t != tag.id]
active = True
else:
new_tags = selected_tags + [tag.id]
active = False
query = "&".join([f"tag={t}" for t in new_tags])
tags_with_state.append(
{
"id": tag.id,
"name": tag.name,
"active": active,
"query": query,
}
)
advanced_filters_open = bool(category or selected_tags)
return render(
request,
"expenses/expense_list.html",
{
"active_menu": "expenses",
"expenses": page_obj,
"page_obj": page_obj,
"selected_year": year,
"selected_month": month,
"selected_category": category,
"categories": categories,
"year_list": [y.year for y in year_list],
"months": months,
"kpi_total": total_amount,
"kpi_count": expense_count,
"kpi_categories": category_count,
"selected_tags": selected_tags,
"tags": Tag.objects.filter(owner=request.user),
"tags_with_state": tags_with_state,
"accounts": Account.objects.filter(owner=request.user),
"selected_account": account_id,
"advanced_filters_open": advanced_filters_open,
"query_params": query_params.urlencode(),
},
)
@login_required
def expense_create(request):
if request.method == "POST":
form = ExpenseForm(request.POST, user=request.user)
if form.is_valid():
expense = form.save(commit=False)
expense.owner = request.user
expense.save()
form.save_m2m()
messages.success(request, "Gasto creado correctamente.")
return redirect("expense_list")
else:
form = ExpenseForm(user=request.user)
return render(
request,
"expenses/expense_form.html",
{"active_menu": "expenses", "form": form},
)
@login_required
def expense_edit(request, pk):
expense = get_object_or_404(
Expense,
pk=pk,
owner=request.user,
)
if request.method == "POST":
form = ExpenseForm(request.POST or None, instance=expense, user=request.user)
if form.is_valid():
expense = form.save()
messages.success(request, "Gasto actualizado")
return redirect("expense_list")
else:
form = ExpenseForm(instance=expense, user=request.user)
return render(
request,
"expenses/expense_form.html",
{
"active_menu": "expenses",
"form": form,
},
)
@login_required
def expense_delete(request, pk):
expense = get_object_or_404(
Expense,
pk=pk,
owner=request.user,
)
if request.method == "POST":
expense.delete()
messages.success(request, "Gasto eliminado correctamente.")
return redirect("expense_list")
return render(
request,
"expenses/expense_confirm_delete.html",
{"active_menu": "expenses", "expense": expense},
)
@login_required
def dashboard(request):
today = date.today()
period = request.GET.get("period", "")
account_id = _get_int(request.GET.get("account"))
compare_enabled = request.GET.get("compare") == "1"
# Time presets
if period == "this_month":
selected_year, selected_month = today.year, today.month
elif period == "last_month":
selected_year, selected_month = sub_months(today.year, today.month, 1)
elif period == "this_year":
selected_year, selected_month = today.year, None
else:
selected_year = _get_int(request.GET.get("year")) or _get_int(today.year)
selected_month = _get_int(request.GET.get("month"))
selected_month = _get_int(selected_month) if selected_month else None
# Accounts
accounts = Account.objects.filter(owner=request.user, active=True)
selected_account_obj = None
if account_id:
selected_account_obj = accounts.filter(id=account_id).first()
# Calculate the KPI
if selected_account_obj:
kpi_balance = selected_account_obj.current_balance()
else:
kpi_balance = sum(account.current_balance() for account in accounts)
# Filter by base expenses
expenses = Expense.objects.filter(owner=request.user)
if account_id:
expenses = expenses.filter(account_id=account_id)
expenses_filtered = expenses.filter(date__year=selected_year)
if selected_month:
expenses_filtered = expenses_filtered.filter(date__month=selected_month)
# Basic KPIs
total_amount = expenses_filtered.aggregate(total=Sum("amount"))["total"] or 0
expense_count = expenses_filtered.count()
category_count = expenses_filtered.values("category").distinct().count()
# Total by category
by_category_qs = by_category = (
expenses_filtered.values("category__name")
.annotate(total=Sum("amount"))
.order_by("-total")
)
CATEGORY_LIMIT = 10 # Top 10
by_category_chart = []
for index, row in enumerate(by_category_qs):
if index >= CATEGORY_LIMIT:
break
cat_name = row["category__name"] or "Sin categoría"
total_cat = float(row["total"])
by_category_chart.append({
"category__name": cat_name,
"total": total_cat
})
#Graphic
daily_average = 0
projected_end_of_month = 0
if selected_month:
by_day_qs = (
expenses_filtered.annotate(day=ExtractDay("date"))
.values("day")
.annotate(total=Sum("amount"))
)
day_totals = {row["day"]: float(row["total"]) for row in by_day_qs}
num_days = calendar.monthrange(selected_year, selected_month)[1]
if selected_year == today.year and selected_month == today.month:
days_passed = today.day
else:
days_passed = calendar.monthrange(selected_year, selected_month)[1]
daily_average = total_amount / days_passed if days_passed > 0 else 0
total_days_in_month = calendar.monthrange(selected_year, selected_month)[1]
projected_end_of_month = daily_average * total_days_in_month
chart_labels = list(range(1, num_days + 1))
chart_totals = [day_totals.get(d, 0) for d in chart_labels]
chart_type = "day"
else:
by_month_qs = (
expenses_filtered.annotate(month=ExtractMonth("date"))
.values("month")
.annotate(total=Sum("amount"))
)
month_totals = {row["month"]: float(row["total"]) for row in by_month_qs}
chart_labels = ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"]
chart_totals = [month_totals.get(m, 0) for m in range(1, 13)]
chart_type = "month"
year_list = (
Expense.objects.filter(owner=request.user)
.annotate(year=ExtractYear("date"))
.values_list("year", flat=True)
.distinct()
.order_by("-year")
)
# Comparison
previous_total = 0
kpi_difference = 0
percentage = 0
kpi_trend = "equal"
category_comparison = []
if compare_enabled:
previous_expenses = Expense.objects.filter(owner=request.user)
if account_id:
previous_expenses = previous_expenses.filter(account_id=account_id)
if selected_month:
prev_year, prev_month = sub_months(selected_year, selected_month, 1)
previous_expenses = previous_expenses.filter(date__year=prev_year, date__month=prev_month)
else:
prev_year = selected_year - 1
previous_expenses = previous_expenses.filter(date__year=prev_year)
previous_total = previous_expenses.aggregate(total=Sum("amount"))["total"] or 0
kpi_difference = total_amount - previous_total
if previous_total > 0:
percentage = (kpi_difference / previous_total) * 100
if kpi_difference > 0:
kpi_trend = "up"
elif kpi_difference < 0:
kpi_trend = "down"
# Comparison by category
previous_by_category = previous_expenses.values("category__name").annotate(total=Sum("amount"))
current_map = {row["category__name"]: float(row["total"]) for row in by_category}
previous_map = {row["category__name"]: float(row["total"]) for row in previous_by_category}
for cat in set(current_map) | set(previous_map):
cur_t = current_map.get(cat, 0)
prev_t = previous_map.get(cat, 0)
category_comparison.append({
"category": cat,
"current": cur_t,
"previous": prev_t,
"difference": cur_t - prev_t,
"difference_abs": abs(cur_t - prev_t)
})
# Anual evolution by accounts
accounts_charts = []
for acc in accounts:
try:
monthly_data = acc.monthly_balance(selected_year)
m_balance = [float(row["balance"]) for row in monthly_data]
except Exception:
logger.exception(
"Error calculando monthly_balance para la cuenta %s (año %s)",
acc.id, selected_year
)
m_balance = [0] * 12
if selected_year == today.year:
current_month_index = today.month - 1
if current_month_index < len(m_balance):
m_balance[current_month_index] = float(acc.current_balance())
m_balance = m_balance[:today.month]
accounts_charts.append({
"id": acc.id,
"name": acc.name,
"data": m_balance,
"current_balance": acc.current_balance(),
})
# Goals
goals = Goal.objects.filter(owner=request.user)
# Recent expenses
recent_expenses = Expense.objects.filter(owner=request.user).order_by('-date', '-id')[:7]
# Send the data to the dashboard
return render(
request,
"expenses/dashboard.html",
{
"active_menu": "dashboard",
"today": today,
"by_category": by_category,
"by_category_chart": by_category_chart,
"chart_labels": chart_labels,
"chart_data": chart_totals,
"year_list": year_list,
"months": list(range(1, 13)),
"selected_year": selected_year,
"selected_month": selected_month,
"kpi_total": total_amount,
"kpi_count": expense_count,
"kpi_categories": category_count,
"compare_enabled": compare_enabled,
"kpi_previous_total": previous_total,
"kpi_difference": kpi_difference,
"kpi_difference_abs": abs(kpi_difference),
"kpi_percentage": percentage,
"category_comparison": category_comparison,
"kpi_trend": kpi_trend,
"accounts": accounts,
"selected_account": account_id,
"selected_account_obj": selected_account_obj,
"kpi_balance": kpi_balance,
"accounts_charts": accounts_charts,
"period": period,
"goals": goals,
"recent_expenses": recent_expenses,
"daily_average": daily_average,
"projected_end_of_month": projected_end_of_month,
},
)
@login_required
def tag_list(request):
tags = (
Tag.objects.filter(owner=request.user)
.annotate(expense_count=Count("expenses"))
.order_by("name")
)
return render(
request, "expenses/tag_list.html", {"active_menu": "settings", "tags": tags}
)
@login_required
def tag_create(request):
if request.method == "POST":
form = TagForm(request.POST)
if form.is_valid():
tag = form.save(commit=False)
tag.owner = request.user
tag.save()
messages.success(request, "Etiqueta creada correctamente.")
return redirect("tag_list")
else:
form = TagForm()
return render(
request, "expenses/tag_form.html", {"active_menu": "settings", "form": form}
)
@login_required
def tag_edit(request, pk):
tag = get_object_or_404(Tag, pk=pk, owner=request.user)
if request.method == "POST":
form = TagForm(request.POST, instance=tag)
if form.is_valid():
form.save()
messages.success(request, "Etiqueta actualizada.")
return redirect("tag_list")
else:
form = TagForm(instance=tag)
return render(
request, "expenses/tag_form.html", {"active_menu": "settings", "form": form}
)
@login_required
def tag_delete(request, pk):
tag = get_object_or_404(Tag, pk=pk, owner=request.user)
if request.method == "POST":
tag.delete()
messages.success(request, "Etiqueta eliminada.")
return redirect("tag_list")
return render(
request,
"expenses/tag_confirm_delete.html",
{"active_menu": "settings", "tag": tag},
)
@login_required
def account_list(request):
accounts = list(Account.objects.filter(owner=request.user).order_by("name"))
account_rows, _, _ = _account_balances(accounts)
return render(
request,
"expenses/account_list.html",
{"active_menu": "accounts", "account_rows": account_rows},
)
@login_required
def account_create(request):
if request.method == "POST":
form = AccountForm(request.POST)
if form.is_valid():
account = form.save(commit=False)
account.owner = request.user
account.save()
messages.success(request, "Cuenta creada correctamente.")
return redirect("account_list")
else:
form = AccountForm()
return render(
request, "expenses/account_form.html", {"active_menu": "accounts", "form": form}
)
@login_required
def account_edit(request, pk):
account = get_object_or_404(Account, pk=pk, owner=request.user)
if request.method == "POST":
form = AccountForm(request.POST, instance=account)
if form.is_valid():
form.save()
messages.success(request, "Cuenta actualizada.")
return redirect("account_list")
else:
form = AccountForm(instance=account)
return render(
request, "expenses/account_form.html", {"active_menu": "accounts", "form": form}
)
@login_required
def account_delete(request, pk):
"""Delete an account via *soft-delete* (sets active=False).
Unlike expense_delete, tag_delete or income_delete, which delete the
record for real, accounts are never physically removed: they are
deactivated. Historical expenses and incomes keep their ForeignKey to
the account, so balances and charts for past periods remain correct.
An inactive account disappears from forms (querysets filtered by
active=True) but stays visible and dimmed in the account list.
"""
account = get_object_or_404(Account, pk=pk, owner=request.user)
if request.method == "POST":
account.active = False
account.save()
messages.success(request, "Cuenta eliminada.")
return redirect("account_list")
return render(
request,
"expenses/account_confirm_delete.html",
{"active_menu": "accounts", "account": account},
)
@login_required
def income_create(request):
if request.method == "POST":
form = IncomeForm(request.POST, user=request.user)
if form.is_valid():
income = form.save(commit=False)
income.owner = request.user
income.save()
messages.success(request, "Ingreso creado correctamente.")
return redirect("income_list")
else:
form = IncomeForm(user=request.user)
return render(
request, "expenses/income_form.html", {"active_menu": "incomes", "form": form}
)
@login_required
def income_list(request):
incomes = Income.objects.filter(owner=request.user)
return render(
request,
"expenses/income_list.html",
{"active_menu": "incomes", "incomes": incomes},
)
@login_required
def income_edit(request, pk):
income = get_object_or_404(Income, pk=pk, owner=request.user)
if request.method == "POST":
form = IncomeForm(request.POST, instance=income, user=request.user)
if form.is_valid():
form.save()
messages.success(request, "Ingreso actualizado.")
return redirect("income_list")
else:
form = IncomeForm(instance=income, user=request.user)
return render(
request, "expenses/income_form.html", {"active_menu": "incomes", "form": form}
)
@login_required
def income_delete(request, pk):
income = get_object_or_404(Income, pk=pk, owner=request.user)
if request.method == "POST":
income.delete()
messages.success(request, "Ingreso eliminado.")
return redirect("income_list")
return render(
request,
"expenses/income_confirm_delete.html",
{"active_menu": "incomes", "income": income},
)
@login_required
def fuel_create(request):
if request.method == "POST":
form = FuelEntryForm(request.POST, user=request.user)
if form.is_valid():
category, _ = Category.objects.get_or_create(
slug="gasolina",
owner=request.user,
defaults={"name": "Gasolina"},
)
expense = form.save(commit=False)
expense.owner = request.user
expense.category = category
expense.description = "Repostaje"
expense.save()
FuelEntry.objects.create(
expense=expense,
odometer=form.cleaned_data["odometer"],
liters=form.cleaned_data["liters"],
)
return redirect("fuel_list")
else:
form = FuelEntryForm(user=request.user)
return render(
request, "fuel/create.html", {"active_menu": "fuel", "form": form}
)
@login_required
def fuel_list(request):
selected_year = request.GET.get("year")
current_year = datetime.now().year
selected_year = int(selected_year) if selected_year else current_year
fuels = (
FuelEntry.objects.filter(
expense__owner=request.user, expense__date__year=selected_year
)
.select_related("expense")
.order_by("expense__date")
)
monthly_expenses = (
fuels.annotate(month=ExtractMonth("expense__date"))
.values("month")
.annotate(total=Sum("expense__amount"))
)
month_map = {m["month"]: float(m["total"]) for m in monthly_expenses}
monthly_data = [month_map.get(month, 0) for month in range(1, 13)]
year_list = (
FuelEntry.objects.filter(
expense__owner=request.user,
)
.annotate(year=ExtractYear("expense__date"))
.values_list("year", flat=True)
.distinct()
.order_by("year")
)
km_data = []
dates = []
for fuel in fuels:
km = fuel.km_since_previous()
if km:
km_data.append(km)
dates.append(fuel.expense.date.strftime("%Y-%m-%d"))
return render(
request,
"fuel/list.html",
{
"active_menu": "fuel",
"fuels": fuels,
"monthly_data": monthly_data,
"km_data": km_data,
"km_dates": dates,
"selected_year": selected_year,
"year_list": year_list,
},
)
@login_required
def fuel_edit(request, pk):
expense = get_object_or_404(Expense, pk=pk, owner=request.user)
fuel = get_object_or_404(FuelEntry, expense=expense)
if request.method == "POST":
form = FuelEntryForm(request.POST, instance=expense, user=request.user)
if form.is_valid():
# Update expense
expense = form.save(commit=False)
expense.description = "Repostaje"
expense.save()
# Update FuelEntry
fuel.odometer = form.cleaned_data["odometer"]
fuel.liters = form.cleaned_data["liters"]
fuel.save()
return _redirect_back(request, "expense_list")
else:
form = FuelEntryForm(instance=expense, user=request.user)
next_url = _safe_next(request)
return render(
request,
"fuel/create.html",
{
"active_menu": "fuel" if next_url.startswith("/fuel") else "expenses",
"form": form,
"editing": True,
"next": next_url
},
)
@login_required
def fuel_delete(request, pk):
expense = get_object_or_404(Expense, pk=pk, owner=request.user)
fuel = get_object_or_404(FuelEntry, expense=expense)
if request.method == "POST":
expense.delete()
messages.success(request, "Repostaje eliminado.")
return _redirect_back(request, "fuel_list")
next_url = _safe_next(request)
return render(
request,
"fuel/confirm_delete.html",
{
"active_menu":"fuel" if next_url.startswith("/fuel") else "expenses",
"fuel": fuel,
"next": next_url
},
)
@login_required
def category_list(request):
categories = Category.objects.filter(owner=request.user)
if request.method == "POST":
form = CategoryForm(request.POST, user=request.user)
if form.is_valid():
category = form.save(commit=False)
category.owner = request.user
category.save()
return redirect("category_list")
else:
form = CategoryForm(user=request.user)
return render(
request,
"categories/list.html",
{
"active_menu": "settings",
"category_rows": _category_tree(categories),
"form": form,
},
)
@login_required
def category_edit(request, pk):
category = get_object_or_404(Category, pk=pk, owner=request.user)
if request.method == "POST":
form = CategoryForm(request.POST, instance=category, user=request.user)
if form.is_valid():
form.save()
messages.success(request, "Categoría actualizada")
return redirect("category_list")
else:
form = CategoryForm(instance=category, user=request.user)
return render(
request,
"categories/form.html",
{
"active_menu": "settings",
"form":form,
"category": category,
}
)
@login_required
def category_delete(request, pk):
category = get_object_or_404(Category, pk=pk, owner=request.user)
if request.method == "POST":
try:
category.delete()
except ProtectedError:
messages.error(
request,
f"No se puede eliminar «{category.name}»; tiene gastos asociados. "
"Reasigna esos gastos a otra categoría antes de borrarla."
)
else:
messages.success(request, "Categoría eliminada.")
return redirect("category_list")
return render(
request,
"categories/confirm_delete.html",
{
"active_menu": "settings",
"category": category,
"children": category.children.all(),
"expense_count": category.expenses.count(),
"goal_count": category.goal_set.count(),
},
)
@login_required
def goal_list(request):
goals = Goal.objects.filter(owner=request.user)
selected_kind = request.GET.get("kind") or ""
if selected_kind in dict(Goal.KIND_CHOICES):
goals = goals.filter(kind=selected_kind)
else:
selected_kind = ""
goals = goals.order_by("name")
return render(
request,
"goals/list.html",
{
"goals": goals,
"kind_choices": Goal.KIND_CHOICES,
"selected_kind": selected_kind,
"active_menu": "settings",
},
)
@login_required
def goal_create(request):
if request.method == "POST":
form = GoalForm(request.POST, user=request.user)
if form.is_valid():
goal = form.save(commit=False)
goal.owner = request.user
goal.save()
return redirect("goal_list")
else:
form = GoalForm(user=request.user)
return render(
request,
"goals/form.html",
{
"form": form,
"title": "Nuevo objetivo",
"active_menu": "settings",
},
)
@login_required
def goal_edit(request, pk):
goal = get_object_or_404(Goal,
pk=pk,
owner=request.user)
if request.method == "POST":
form = GoalForm(request.POST, instance=goal, user=request.user)
if form.is_valid():
form.save()
return redirect("goal_list")
else:
form = GoalForm(instance=goal, user=request.user)
return render(
request,
"goals/form.html",
{
"form": form,
"title": "Editar objetivo",
"active_menu": "settings",
},
)
@login_required
def goal_delete(request, pk):
goal = get_object_or_404(Goal,
pk=pk,
owner=request.user)
if request.method == "POST":
goal.delete()
messages.success(request, "Objetivo eliminado.")
return redirect("goal_list")
return render(
request,
"goals/confirm_delete.html",
{"active_menu":"settings", "goal":goal}
)
@login_required
def settings_index(request):
return render(request, "settings/index.html", {"active_menu": "settings"})