1028 lines
29 KiB
Python
1028 lines
29 KiB
Python
import logging
|
|
import calendar
|
|
from datetime import date, datetime
|
|
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, 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 _redirect_back(request, fallback):
|
|
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 redirect(next_url)
|
|
|
|
return redirect(fallback)
|
|
|
|
|
|
def sub_months(year, month, n):
|
|
month -= n
|
|
while month <= 0:
|
|
month += 12
|
|
year -= 1
|
|
return year, month
|
|
|
|
|
|
@login_required
|
|
def home(request):
|
|
expenses = Expense.objects.filter(owner=request.user)
|
|
|
|
# Last expenses
|
|
last_expenses = expenses.select_related("category").order_by("-date")[:5]
|
|
|
|
# Simple KPIs (current month)
|
|
today = date.today()
|
|
month_expenses = expenses.filter(date__year=today.year, date__month=today.month)
|
|
|
|
kpi_total = month_expenses.aggregate(total=Sum("amount"))["total"] or 0
|
|
|
|
kpi_count = month_expenses.count()
|
|
|
|
kpi_categories = month_expenses.values("category").distinct().count()
|
|
|
|
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),
|
|
}
|
|
)
|
|
|
|
goals = Goal.objects.filter(owner=request.user, show_on_home=True)
|
|
|
|
return render(
|
|
request,
|
|
"expenses/home.html",
|
|
{
|
|
"active_menu": "home",
|
|
"last_expenses": last_expenses,
|
|
"kpi_total": kpi_total,
|
|
"kpi_count": kpi_count,
|
|
"kpi_categories": kpi_categories,
|
|
"mini_chart_labels": [x["label"] for x in mini_data],
|
|
"mini_chart_data": [x["total"] for x in mini_data],
|
|
"goals": goals,
|
|
},
|
|
)
|
|
|
|
|
|
@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):
|
|
# sourcery skip: assign-if-exp, merge-else-if-into-elif
|
|
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)
|
|
|
|
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 = Account.objects.filter(owner=request.user)
|
|
return render(
|
|
request,
|
|
"expenses/account_list.html",
|
|
{"active_menu": "accounts", "accounts": accounts},
|
|
)
|
|
|
|
|
|
@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):
|
|
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, user=request.user)
|
|
|
|
if form.is_valid():
|
|
# Update expense
|
|
expense.date = 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 = request.POST.get("next") or request.GET.get("next", "")
|
|
|
|
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 = request.POST.get("next") or request.GET.get("next", "")
|
|
|
|
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",
|
|
"categories": 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)
|
|
|
|
return render(
|
|
request,
|
|
"goals/list.html",
|
|
{
|
|
"goals": goals,
|
|
"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"})
|