Compare commits
No commits in common. "b04a8db080e3bdb79bb63fa8c0af3dbbaf217189" and "e29c2bc5f2eaa86c148947f0e2eda7140e9f9ae9" have entirely different histories.
b04a8db080
...
e29c2bc5f2
@ -694,59 +694,10 @@ tr:hover {
|
||||
/* Forms */
|
||||
/* ========================= */
|
||||
|
||||
.app-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-field input:not([type="checkbox"]):not([type="radio"]),
|
||||
.form-field select,
|
||||
.form-field textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.form-field input:focus,
|
||||
.form-field select:focus,
|
||||
.form-field textarea:focus {
|
||||
outline: 2px solid var(--color-focus-ring);
|
||||
outline-offset: 1px;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-field-checkbox {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-field-checkbox input[type="checkbox"] {
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.form-errors {
|
||||
color: var(--color-danger-accent);
|
||||
font-size: 0.85rem;
|
||||
@ -909,115 +860,6 @@ tr:hover {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* ========================= */
|
||||
/* Home widgets */
|
||||
/* ========================= */
|
||||
|
||||
.home-section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.home-section > h2 {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.balance-total {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-success-accent);
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.balance-total.negative,
|
||||
.balance-amount.negative {
|
||||
color: var(--color-danger-accent);
|
||||
}
|
||||
|
||||
.balance-breakdown {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.balance-breakdown li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.4rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.movements-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.movement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.movement-date {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.movement-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.movement-account {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.movement-amount {
|
||||
flex-shrink: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.amount-positive {
|
||||
color: var(--color-success-accent);
|
||||
}
|
||||
|
||||
.amount-negative {
|
||||
color: var(--color-danger-accent);
|
||||
}
|
||||
|
||||
.comparison {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.movement-account {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================= */
|
||||
/* Responsive */
|
||||
/* ========================= */
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
{% block content %}
|
||||
<h2>Editar categoría</h2>
|
||||
|
||||
<form method="post" class="app-form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
{{ form.as_p }}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
<a class="btn btn-secondary" href="{% url 'category_list' %}">Cancelar</a>
|
||||
|
||||
@ -9,9 +9,9 @@
|
||||
<h1>Mis categorías</h1>
|
||||
|
||||
<h3> Nueva categoría</h3>
|
||||
<form method="post" class="app-form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Crear</button>
|
||||
</form>
|
||||
|
||||
@ -24,7 +24,6 @@
|
||||
<tr>
|
||||
<th>Categoría</th>
|
||||
<th>Categoría padre</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
{% for field in form %}
|
||||
<div class="form-field {% if field.field.widget.input_type == 'checkbox' %}form-field-checkbox{% endif %}">
|
||||
{% if field.name == "tags" %}
|
||||
{{ field.label_tag }}
|
||||
<div class="tag-chip-list">
|
||||
{% for checkbox in field %}
|
||||
<label class="tag-chip">
|
||||
{{ checkbox.tag }}
|
||||
<span>{{ checkbox.choice_label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
|
||||
{% if field.help_text %}
|
||||
<small class="form-help">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<div class="form-errors">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="form-errors">{{ form.non_field_errors }}</div>
|
||||
{% endif %}
|
||||
@ -10,9 +10,9 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<form method="post" class="app-form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
{{ form.as_p }}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
|
||||
@ -17,10 +17,34 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<form method="post" class="app-form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
{% for field in form %}
|
||||
{% if field.name == "tags" %}
|
||||
<div class="form-field">
|
||||
{{ field.label_tag }}
|
||||
<div class="tag-chip-list">
|
||||
{% for checkbox in field %}
|
||||
<label class="tag-chip">
|
||||
{{ checkbox.tag }}
|
||||
<span>{{ checkbox.choice_label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if field.errors %}
|
||||
<div class="form-errors">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
{{ field.label_tag }} {{ field }}
|
||||
{% if field.errors %}
|
||||
<span class="form-errors">{{ field.errors }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
||||
@ -8,111 +8,29 @@
|
||||
|
||||
<h1>Home</h1>
|
||||
|
||||
{% if has_alerts %}
|
||||
<div class="messages">
|
||||
{% for goal in exceeded_goals %}
|
||||
<div class="message warning">
|
||||
Presupuesto <strong>{{ goal.name }}</strong> excedido:
|
||||
{{ goal.progress|floatformat:2 }}€ de {{ goal.target_amount|floatformat:2 }}€.
|
||||
<a href="{% url 'goal_list' %}">Ver objetivos</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for item in negative_accounts %}
|
||||
<div class="message error">
|
||||
La cuenta <strong>{{ item.account.name }}</strong> está en negativo:
|
||||
{{ item.balance|floatformat:2 }}€.
|
||||
<a href="{% url 'account_list' %}">Ver cuentas</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<section class="home-section">
|
||||
<h2>Saldo total</h2>
|
||||
<p class="balance-total {% if total_balance < 0 %}negative{% endif %}">
|
||||
{{ total_balance|floatformat:2 }}€
|
||||
</p>
|
||||
|
||||
<ul class="balance-breakdown">
|
||||
{% for item in account_balances %}
|
||||
<li>
|
||||
<span class="balance-account">{{ item.account.name }}</span>
|
||||
<span class="balance-amount {% if item.balance < 0 %}negative{% endif %}">
|
||||
{{ item.balance|floatformat:2 }}€
|
||||
</span>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="muted">No tienes cuentas activas.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="home-section">
|
||||
<section>
|
||||
<h2>Resumen del mes</h2>
|
||||
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi-card">
|
||||
<span class="kpi-label">Gastado este mes</span>
|
||||
<span class="kpi-value">{{ kpi_total|floatformat:2 }}€</span>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<span class="kpi-label">Nº de gastos</span>
|
||||
<span class="kpi-value">{{ kpi_count }}</span>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<span class="kpi-label">Categorías usadas</span>
|
||||
<span class="kpi-value">{{ kpi_categories }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="comparison">
|
||||
{% if diff_pct is None %}
|
||||
<span class="muted">Sin gastos el mes pasado para comparar.</span>
|
||||
{% else %}
|
||||
Mes anterior: {{ prev_total|floatformat:2 }}€ —
|
||||
{% if diff_amount > 0 %}
|
||||
<span class="amount-negative">
|
||||
+{{ diff_amount|floatformat:2 }}€ ({{ diff_pct|floatformat:1 }}%)
|
||||
</span>
|
||||
{% elif diff_amount < 0 %}
|
||||
<span class="amount-positive">
|
||||
{{ diff_amount|floatformat:2 }}€ ({{ diff_pct|floatformat:1 }}%)
|
||||
</span>
|
||||
{% else %}
|
||||
sin variación
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br>
|
||||
<small class="muted">
|
||||
El mes en curso está incompleto: la comparación no es equivalente
|
||||
hasta que termine.
|
||||
</small>
|
||||
</p>
|
||||
<p>Total: {{ kpi_total|floatformat:2 }}</p>
|
||||
<p>Gastos: {{ kpi_count }}</p>
|
||||
<p>Categorías: {{ kpi_categories }}</p>
|
||||
</section>
|
||||
|
||||
<section class="home-section">
|
||||
<h2>Últimos movimientos</h2>
|
||||
<ul class="movements-list">
|
||||
{% for mov in movements %}
|
||||
<li class="movement">
|
||||
<span class="movement-date">{{ mov.date }}</span>
|
||||
<span class="movement-label">{{ mov.label }}</span>
|
||||
<span class="movement-account muted">{{ mov.account }}</span>
|
||||
<span class="movement-amount {% if mov.kind == 'income' %}amount-positive{% else %}amount-negative{% endif %}">
|
||||
{% if mov.kind == 'income' %}+{% else %}−{% endif %}{{ mov.amount|floatformat:2 }}€
|
||||
</span>
|
||||
<section>
|
||||
<h2>Últimos gastos</h2>
|
||||
<ul>
|
||||
{% for expense in last_expenses %}
|
||||
<li>
|
||||
{{ expense.date }} -
|
||||
{{ expense.category.name }} -
|
||||
{{ expense.amount }}
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="muted">Todavía no hay movimientos.</li>
|
||||
<li>No hay gastos</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="section-actions">
|
||||
<a class="btn btn-secondary" href="{% url 'expense_list' %}">Ver todos los gastos</a>
|
||||
<a class="btn btn-secondary" href="{% url 'income_list' %}">Ver todos los ingresos</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home-section">
|
||||
<section>
|
||||
<div class="dashboard-grid">
|
||||
<div class="card card-chart">
|
||||
<h2>Últimos meses</h2>
|
||||
@ -161,10 +79,10 @@
|
||||
applyCartesianColors(chart, colors);
|
||||
});
|
||||
</script>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="home-section">
|
||||
<h2>Objetivos</h2>
|
||||
<h3>Objetivos</h3>
|
||||
{% if goals %}
|
||||
<div class="goals-widget">
|
||||
{% for goal in goals %}
|
||||
@ -174,13 +92,7 @@
|
||||
<span class="badge badge-inactive">Excedido</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="progress-bar"
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="{{ goal.bar_width|unlocalize }}"
|
||||
aria-label="Progreso de {{ goal.name }}"
|
||||
aria-valuetext="{{ goal.progress|floatformat:1 }}€ de {{ goal.target_amount|floatformat:1 }}€ ({{ goal.percentage|floatformat:1 }}%)">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill {{ goal.progress_state }}"
|
||||
style="width: {{ goal.bar_width|unlocalize }}%"></div>
|
||||
</div>
|
||||
@ -192,8 +104,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">No tienes objetivos aún.</p>
|
||||
<p>No tienes objetivos aún.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -16,9 +16,9 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<form method="post" class="app-form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
{{ form.as_p }}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% if form.instance.pk %}
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
{% block content %}
|
||||
<h1>Etiqueta</h1>
|
||||
|
||||
<form method="post" class="app-form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
{{ form.as_p }}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
<a class="btn btn-secondary" href="{% url 'tag_list' %}">Volver</a>
|
||||
|
||||
@ -17,10 +17,10 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<form method="post" class="app-form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
{{ form.as_p }}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% if editing %}
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
{% extends "expenses/base.html" %}
|
||||
{% block title %}
|
||||
{{ title }}
|
||||
Nuevo objetivo
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
{{ title }}
|
||||
Nuevo objetivo
|
||||
</h1>
|
||||
|
||||
<form method="post" class="app-form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
{{ form.as_p }}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Guardar
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
|
||||
<h2>Iniciar sesión</h2>
|
||||
|
||||
<form method="post" class="app-form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Entrar</button>
|
||||
</form>
|
||||
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
{% extends "expenses/base.html" %}
|
||||
|
||||
{% block title %}Contraseña actualizada{% endblock %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Contraseña actualizada</h1>
|
||||
|
||||
<p>Tu contraseña se ha cambiado correctamente.</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<a class="btn btn-primary" href="{% url 'home' %}">Volver al inicio</a>
|
||||
</div>
|
||||
<h2>Contraseña actualizada correctamente</h2>
|
||||
<a href="/">Volver</a>
|
||||
{% endblock %}
|
||||
@ -1,16 +1,10 @@
|
||||
{% extends "expenses/base.html" %}
|
||||
|
||||
{% block title %}Cambiar contraseña{% endblock %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Cambiar contraseña</h1>
|
||||
|
||||
<form method="post" class="app-form">
|
||||
<h2>Cambiar contraseña</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include "expenses/_form_fields.html" %}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Cambiar contraseña</button>
|
||||
<a class="btn btn-secondary" href="{% url 'home' %}">Volver</a>
|
||||
</div>
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Cambiar</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -1,7 +1,6 @@
|
||||
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 (
|
||||
@ -59,108 +58,21 @@ def sub_months(year, month, n):
|
||||
|
||||
@login_required
|
||||
def home(request):
|
||||
today = date.today()
|
||||
expenses = Expense.objects.filter(owner=request.user)
|
||||
|
||||
# ---- KPIs del mes en curso ----
|
||||
# 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 Decimal("0")
|
||||
|
||||
kpi_total = month_expenses.aggregate(total=Sum("amount"))["total"] or 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 ----
|
||||
# Dos queries agregadas en lugar de N+1 (nada de account.current_balance() en bucle).
|
||||
accounts = list(Account.objects.filter(owner=request.user, active=True))
|
||||
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})
|
||||
|
||||
# ---- 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)
|
||||
@ -174,28 +86,28 @@ def home(request):
|
||||
]
|
||||
or 0
|
||||
)
|
||||
mini_data.append({"label": f"{m}/{y}", "total": float(total)})
|
||||
|
||||
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,
|
||||
"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],
|
||||
"goals": goals,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -61,19 +61,14 @@ if not DEBUG:
|
||||
|
||||
# Application definition
|
||||
|
||||
# 'expenses' va antes que 'django.contrib.admin' a proposito: el cargador de
|
||||
# plantillas por aplicacion recorre esta lista en orden y se queda con la
|
||||
# primera coincidencia. Si el admin va antes, sus plantillas de registration/
|
||||
# sombrean a las nuestras (paso con password_change_form.html y
|
||||
# password_change_done.html). No reordenar alfabeticamente.
|
||||
INSTALLED_APPS = [
|
||||
'expenses',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'expenses',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user