Compare commits

..

No commits in common. "b04a8db080e3bdb79bb63fa8c0af3dbbaf217189" and "e29c2bc5f2eaa86c148947f0e2eda7140e9f9ae9" have entirely different histories.

16 changed files with 129 additions and 489 deletions

View File

@ -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 */
/* ========================= */ /* ========================= */

View File

@ -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>

View File

@ -8,10 +8,10 @@
<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>
@ -19,16 +19,15 @@
<h3>Listado</h3> <h3>Listado</h3>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead>
<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> {% for category in categories %}
{% for category in categories %}
<tr> <tr>
<td>{{ category.name }}</td> <td>{{ category.name }}</td>
<td>{% if category.parent %}{{ category.parent.name }}{% endif %}</td> <td>{% if category.parent %}{{ category.parent.name }}{% endif %}</td>
@ -37,9 +36,9 @@
<a href="{% url 'category_delete' category.id %}" class="danger">Eliminar</a> <a href="{% url 'category_delete' category.id %}" class="danger">Eliminar</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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">

View File

@ -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,39 +79,32 @@
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 %} <div class="goal-card">
<div class="goal-card"> <strong>{{ goal.name }}</strong>
<strong>{{ goal.name }}</strong> {% if goal.is_exceeded %}
{% if goal.is_exceeded %} <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" <div class="progress-fill {{ goal.progress_state }}"
aria-valuemin="0" style="width: {{ goal.bar_width|unlocalize }}%"></div>
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 }}"
style="width: {{ goal.bar_width|unlocalize }}%"></div>
</div>
<span class="progress-label">
{{ goal.progress|floatformat:1 }}€ / {{ goal.target_amount|floatformat:1 }}€ ({{ goal.percentage|floatformat:1 }}%)
</span>
</div> </div>
{% endfor %}
</div> <span class="progress-label">
{% else %} {{ goal.progress|floatformat:1 }}€ / {{ goal.target_amount|floatformat:1 }}€ ({{ goal.percentage|floatformat:1 }}%)
<p class="muted">No tienes objetivos aún.</p> </span>
{% endif %} </div>
</section> {% endfor %}
</div>
{% else %}
<p>No tienes objetivos aún.</p>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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,
}, },
) )

View File

@ -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 = [