Compare commits
No commits in common. "b04a8db080e3bdb79bb63fa8c0af3dbbaf217189" and "e29c2bc5f2eaa86c148947f0e2eda7140e9f9ae9" have entirely different histories.
b04a8db080
...
e29c2bc5f2
@ -694,59 +694,10 @@ tr:hover {
|
|||||||
/* Forms */
|
/* Forms */
|
||||||
/* ========================= */
|
/* ========================= */
|
||||||
|
|
||||||
.app-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field {
|
.form-field {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
margin-bottom: 1rem;
|
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 {
|
.form-errors {
|
||||||
color: var(--color-danger-accent);
|
color: var(--color-danger-accent);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@ -909,115 +860,6 @@ tr:hover {
|
|||||||
color: var(--color-text-muted);
|
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 */
|
/* Responsive */
|
||||||
/* ========================= */
|
/* ========================= */
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Editar categoría</h2>
|
<h2>Editar categoría</h2>
|
||||||
|
|
||||||
<form method="post" class="app-form">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "expenses/_form_fields.html" %}
|
{{ form.as_p }}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||||
<a class="btn btn-secondary" href="{% url 'category_list' %}">Cancelar</a>
|
<a class="btn btn-secondary" href="{% url 'category_list' %}">Cancelar</a>
|
||||||
|
|||||||
@ -9,9 +9,9 @@
|
|||||||
<h1>Mis categorías</h1>
|
<h1>Mis categorías</h1>
|
||||||
|
|
||||||
<h3> Nueva categoría</h3>
|
<h3> Nueva categoría</h3>
|
||||||
<form method="post" class="app-form">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "expenses/_form_fields.html" %}
|
{{ form.as_p }}
|
||||||
<button type="submit" class="btn btn-primary">Crear</button>
|
<button type="submit" class="btn btn-primary">Crear</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -24,7 +24,6 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Categoría</th>
|
<th>Categoría</th>
|
||||||
<th>Categoría padre</th>
|
<th>Categoría padre</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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 %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<form method="post" class="app-form">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "expenses/_form_fields.html" %}
|
{{ form.as_p }}
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||||
|
|||||||
@ -17,10 +17,34 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<form method="post" class="app-form">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% 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">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
|
|||||||
@ -8,111 +8,29 @@
|
|||||||
|
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
|
|
||||||
{% if has_alerts %}
|
<section>
|
||||||
<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">
|
|
||||||
<h2>Resumen del mes</h2>
|
<h2>Resumen del mes</h2>
|
||||||
|
<p>Total: {{ kpi_total|floatformat:2 }}</p>
|
||||||
<div class="kpi-grid">
|
<p>Gastos: {{ kpi_count }}</p>
|
||||||
<div class="kpi-card">
|
<p>Categorías: {{ kpi_categories }}</p>
|
||||||
<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>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="home-section">
|
<section>
|
||||||
<h2>Últimos movimientos</h2>
|
<h2>Últimos gastos</h2>
|
||||||
<ul class="movements-list">
|
<ul>
|
||||||
{% for mov in movements %}
|
{% for expense in last_expenses %}
|
||||||
<li class="movement">
|
<li>
|
||||||
<span class="movement-date">{{ mov.date }}</span>
|
{{ expense.date }} -
|
||||||
<span class="movement-label">{{ mov.label }}</span>
|
{{ expense.category.name }} -
|
||||||
<span class="movement-account muted">{{ mov.account }}</span>
|
{{ expense.amount }}
|
||||||
<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>
|
|
||||||
</li>
|
</li>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<li class="muted">Todavía no hay movimientos.</li>
|
<li>No hay gastos</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</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>
|
||||||
|
|
||||||
<section class="home-section">
|
<section>
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<div class="card card-chart">
|
<div class="card card-chart">
|
||||||
<h2>Últimos meses</h2>
|
<h2>Últimos meses</h2>
|
||||||
@ -161,10 +79,10 @@
|
|||||||
applyCartesianColors(chart, colors);
|
applyCartesianColors(chart, colors);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="home-section">
|
<h3>Objetivos</h3>
|
||||||
<h2>Objetivos</h2>
|
|
||||||
{% if goals %}
|
{% if goals %}
|
||||||
<div class="goals-widget">
|
<div class="goals-widget">
|
||||||
{% for goal in goals %}
|
{% for goal in goals %}
|
||||||
@ -174,13 +92,7 @@
|
|||||||
<span class="badge badge-inactive">Excedido</span>
|
<span class="badge badge-inactive">Excedido</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="progress-bar"
|
<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-fill {{ goal.progress_state }}"
|
<div class="progress-fill {{ goal.progress_state }}"
|
||||||
style="width: {{ goal.bar_width|unlocalize }}%"></div>
|
style="width: {{ goal.bar_width|unlocalize }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -192,8 +104,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="muted">No tienes objetivos aún.</p>
|
<p>No tienes objetivos aún.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -16,9 +16,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<form method="post" class="app-form">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "expenses/_form_fields.html" %}
|
{{ form.as_p }}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{% if form.instance.pk %}
|
{% if form.instance.pk %}
|
||||||
|
|||||||
@ -3,9 +3,9 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Etiqueta</h1>
|
<h1>Etiqueta</h1>
|
||||||
|
|
||||||
<form method="post" class="app-form">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "expenses/_form_fields.html" %}
|
{{ form.as_p }}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||||
<a class="btn btn-secondary" href="{% url 'tag_list' %}">Volver</a>
|
<a class="btn btn-secondary" href="{% url 'tag_list' %}">Volver</a>
|
||||||
|
|||||||
@ -17,10 +17,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<form method="post" class="app-form">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
|
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
|
||||||
{% include "expenses/_form_fields.html" %}
|
{{ form.as_p }}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{% if editing %}
|
{% if editing %}
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
{% extends "expenses/base.html" %}
|
{% extends "expenses/base.html" %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ title }}
|
Nuevo objetivo
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>
|
<h1>
|
||||||
{{ title }}
|
Nuevo objetivo
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<form method="post" class="app-form">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "expenses/_form_fields.html" %}
|
{{ form.as_p }}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
Guardar
|
Guardar
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
<h2>Iniciar sesión</h2>
|
<h2>Iniciar sesión</h2>
|
||||||
|
|
||||||
<form method="post" class="app-form">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "expenses/_form_fields.html" %}
|
{{ form.as_p }}
|
||||||
<button type="submit" class="btn btn-primary">Entrar</button>
|
<button type="submit" class="btn btn-primary">Entrar</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,6 @@
|
|||||||
{% extends "expenses/base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Contraseña actualizada{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Contraseña actualizada</h1>
|
<h2>Contraseña actualizada correctamente</h2>
|
||||||
|
<a href="/">Volver</a>
|
||||||
<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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,16 +1,10 @@
|
|||||||
{% extends "expenses/base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Cambiar contraseña{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Cambiar contraseña</h1>
|
<h2>Cambiar contraseña</h2>
|
||||||
|
<form method="post">
|
||||||
<form method="post" class="app-form">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "expenses/_form_fields.html" %}
|
{{ form.as_p }}
|
||||||
<div class="form-actions">
|
<button type="submit" class="btn btn-primary">Cambiar</button>
|
||||||
<button type="submit" class="btn btn-primary">Cambiar contraseña</button>
|
|
||||||
<a class="btn btn-secondary" href="{% url 'home' %}">Volver</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import calendar
|
import calendar
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from decimal import Decimal
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from .models import Account, Category, Expense, FuelEntry, Tag, Income, Goal
|
from .models import Account, Category, Expense, FuelEntry, Tag, Income, Goal
|
||||||
from .forms import (
|
from .forms import (
|
||||||
@ -59,108 +58,21 @@ def sub_months(year, month, n):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def home(request):
|
def home(request):
|
||||||
today = date.today()
|
|
||||||
expenses = Expense.objects.filter(owner=request.user)
|
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)
|
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_count = month_expenses.count()
|
||||||
|
|
||||||
kpi_categories = month_expenses.values("category").distinct().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 = []
|
six_months = []
|
||||||
for i in range(5, -1, -1):
|
for i in range(5, -1, -1):
|
||||||
y, m = sub_months(today.year, today.month, i)
|
y, m = sub_months(today.year, today.month, i)
|
||||||
@ -174,28 +86,28 @@ def home(request):
|
|||||||
]
|
]
|
||||||
or 0
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
"expenses/home.html",
|
"expenses/home.html",
|
||||||
{
|
{
|
||||||
"active_menu": "home",
|
"active_menu": "home",
|
||||||
|
"last_expenses": last_expenses,
|
||||||
"kpi_total": kpi_total,
|
"kpi_total": kpi_total,
|
||||||
"kpi_count": kpi_count,
|
"kpi_count": kpi_count,
|
||||||
"kpi_categories": kpi_categories,
|
"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_labels": [x["label"] for x in mini_data],
|
||||||
"mini_chart_data": [x["total"] 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
|
# 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 = [
|
INSTALLED_APPS = [
|
||||||
'expenses',
|
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'expenses',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user