feat: CRUD de traspasos
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9af6e749d5
commit
0dd34ab5dc
@ -33,6 +33,7 @@
|
|||||||
<a href="{% url 'dashboard' %}" class="nav-item {% if active_menu == 'dashboard' %}active{% endif %} ">Dashboard</a>
|
<a href="{% url 'dashboard' %}" class="nav-item {% if active_menu == 'dashboard' %}active{% endif %} ">Dashboard</a>
|
||||||
<a href="{% url 'expense_list' %}" class="nav-item {% if active_menu == 'expenses' %}active{% endif %} ">Gastos</a>
|
<a href="{% url 'expense_list' %}" class="nav-item {% if active_menu == 'expenses' %}active{% endif %} ">Gastos</a>
|
||||||
<a href="{% url 'income_list' %}" class="nav-item {% if active_menu == 'incomes' %}active{% endif %} ">Ingresos</a>
|
<a href="{% url 'income_list' %}" class="nav-item {% if active_menu == 'incomes' %}active{% endif %} ">Ingresos</a>
|
||||||
|
<a href="{% url 'transfer_list' %}" class="nav-item {% if active_menu == 'transfers' %}active{% endif %} ">Traspasos</a>
|
||||||
<a href="{% url 'fuel_list' %}" class="nav-item {% if active_menu == 'fuel' %}active{% endif %} ">Repostajes</a>
|
<a href="{% url 'fuel_list' %}" class="nav-item {% if active_menu == 'fuel' %}active{% endif %} ">Repostajes</a>
|
||||||
<a href="{% url 'account_list' %}" class="nav-item {% if active_menu == 'accounts' %}active{% endif %} ">Cuentas</a>
|
<a href="{% url 'account_list' %}" class="nav-item {% if active_menu == 'accounts' %}active{% endif %} ">Cuentas</a>
|
||||||
<div class="nav-item dropdown {% if active_menu == 'settings' %}active{% endif %}" id="settingsDropdown">
|
<div class="nav-item dropdown {% if active_menu == 'settings' %}active{% endif %}" id="settingsDropdown">
|
||||||
|
|||||||
@ -145,7 +145,10 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="table-actions">
|
<td class="table-actions">
|
||||||
{% if expense.fuel_data %}
|
{% if expense.transfer %}
|
||||||
|
{# Pata de un traspaso: se edita y se borra desde el traspaso. #}
|
||||||
|
<a href="{% url 'transfer_edit' expense.transfer.id %}">Editar traspaso</a>
|
||||||
|
{% elif expense.fuel_data %}
|
||||||
<a href="{% url 'fuel_edit' expense.id %}?next={{ request.get_full_path|urlencode }}">Editar</a>
|
<a href="{% url 'fuel_edit' expense.id %}?next={{ request.get_full_path|urlencode }}">Editar</a>
|
||||||
<a href="{% url 'fuel_delete' expense.id %}?next={{ request.get_full_path|urlencode }}" class="danger">Eliminar</a>
|
<a href="{% url 'fuel_delete' expense.id %}?next={{ request.get_full_path|urlencode }}" class="danger">Eliminar</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@ -93,8 +93,13 @@
|
|||||||
<td>{{ income.amount|floatformat:2 }}</td>
|
<td>{{ income.amount|floatformat:2 }}</td>
|
||||||
<td>{{ income.date }}</td>
|
<td>{{ income.date }}</td>
|
||||||
<td class="table-actions">
|
<td class="table-actions">
|
||||||
|
{% if income.transfer %}
|
||||||
|
{# Pata de un traspaso: se edita y se borra desde el traspaso. #}
|
||||||
|
<a href="{% url 'transfer_edit' income.transfer.id %}">Editar traspaso</a>
|
||||||
|
{% else %}
|
||||||
<a href="{% url 'income_edit' income.id %}">Editar</a>
|
<a href="{% url 'income_edit' income.id %}">Editar</a>
|
||||||
<a href="{% url 'income_delete' income.id %}" class="danger">Eliminar</a>
|
<a href="{% url 'income_delete' income.id %}" class="danger">Eliminar</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
{% extends "expenses/_confirm_delete.html" %}
|
||||||
|
|
||||||
|
{% block title %}Traspasos{% endblock %}
|
||||||
|
|
||||||
|
{% block delete_heading %}Eliminar traspaso{% endblock %}
|
||||||
|
|
||||||
|
{% block delete_question %}
|
||||||
|
¿Seguro que quieres eliminar el traspaso de
|
||||||
|
<strong>{{ transfer.amount|floatformat:2 }}€</strong>
|
||||||
|
de <strong>{{ transfer.from_account.name }}</strong>
|
||||||
|
a <strong>{{ transfer.to_account.name }}</strong>
|
||||||
|
del {{ transfer.date }}?
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block delete_warnings %}
|
||||||
|
<p class="form-errors">
|
||||||
|
Se eliminarán también el gasto de la cuenta de origen y el ingreso de la de
|
||||||
|
destino{% if transfer.fee_amount %}, además del gasto de la comisión{% endif %}.
|
||||||
|
Los saldos de las dos cuentas volverán a como estaban.
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block cancel_url %}{% url 'transfer_list' %}{% endblock %}
|
||||||
29
expenses_manager/expenses/templates/transfers/form.html
Normal file
29
expenses_manager/expenses/templates/transfers/form.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% extends "expenses/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
|
{% if form.instance.pk %}
|
||||||
|
<p>
|
||||||
|
Al guardar se actualizan los dos movimientos del traspaso y su comisión.
|
||||||
|
No hace falta tocarlos por separado en Gastos ni en Ingresos.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" class="app-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% include "expenses/_form_fields.html" %}
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% if form.instance.pk %}
|
||||||
|
Guardar traspaso
|
||||||
|
{% else %}
|
||||||
|
Crear traspaso
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-secondary" href="{% url 'transfer_list' %}">Volver</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
64
expenses_manager/expenses/templates/transfers/list.html
Normal file
64
expenses_manager/expenses/templates/transfers/list.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{% extends "expenses/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Traspasos
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Mis traspasos</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Un traspaso mueve dinero entre dos cuentas propias: no cuenta como gasto ni
|
||||||
|
como ingreso en los análisis, pero sí cambia el saldo de las dos cuentas.
|
||||||
|
La comisión, si la hay, sí es un gasto real.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="section-actions">
|
||||||
|
<a class="btn btn-primary" href="{% url 'transfer_create' %}">➕ Nuevo traspaso</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Origen</th>
|
||||||
|
<th>Destino</th>
|
||||||
|
<th>Importe</th>
|
||||||
|
<th>Comisión</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for transfer in transfers %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ transfer.date }}</td>
|
||||||
|
<td>{{ transfer.from_account.name }}</td>
|
||||||
|
<td>{{ transfer.to_account.name }}</td>
|
||||||
|
<td>{{ transfer.amount|floatformat:2 }}€</td>
|
||||||
|
<td>
|
||||||
|
{% if transfer.fee_amount %}
|
||||||
|
{{ transfer.fee_amount|floatformat:2 }}€
|
||||||
|
{% else %}
|
||||||
|
<span>-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="table-actions">
|
||||||
|
<a href="{% url 'transfer_edit' transfer.id %}">Editar</a>
|
||||||
|
<a href="{% url 'transfer_delete' transfer.id %}" class="danger">Eliminar</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="empty-state">
|
||||||
|
<p>No hay traspasos</p>
|
||||||
|
<a href="{% url 'transfer_create' %}">Añade el primero</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -25,6 +25,10 @@ urlpatterns = [
|
|||||||
path('fuel/create/', views.fuel_create, name='fuel_create'),
|
path('fuel/create/', views.fuel_create, name='fuel_create'),
|
||||||
path('fuel/<int:pk>/edit/', views.fuel_edit, name='fuel_edit'),
|
path('fuel/<int:pk>/edit/', views.fuel_edit, name='fuel_edit'),
|
||||||
path('fuel/<int:pk>/delete/', views.fuel_delete, name='fuel_delete'),
|
path('fuel/<int:pk>/delete/', views.fuel_delete, name='fuel_delete'),
|
||||||
|
path('transfers/', views.transfer_list, name='transfer_list'),
|
||||||
|
path('transfers/new/', views.transfer_create, name='transfer_create'),
|
||||||
|
path('transfers/<int:pk>/edit/', views.transfer_edit, name='transfer_edit'),
|
||||||
|
path('transfers/<int:pk>/delete/', views.transfer_delete, name='transfer_delete'),
|
||||||
path('categories/', views.category_list, name='category_list'),
|
path('categories/', views.category_list, name='category_list'),
|
||||||
path('categories/<int:pk>/edit/', views.category_edit, name='category_edit'),
|
path('categories/<int:pk>/edit/', views.category_edit, name='category_edit'),
|
||||||
path('categories/<int:pk>/delete/', views.category_delete, name='category_delete'),
|
path('categories/<int:pk>/delete/', views.category_delete, name='category_delete'),
|
||||||
|
|||||||
@ -3,7 +3,16 @@ import calendar
|
|||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from decimal import Decimal
|
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,
|
||||||
|
Transfer,
|
||||||
|
)
|
||||||
from .forms import (
|
from .forms import (
|
||||||
ExpenseForm,
|
ExpenseForm,
|
||||||
IncomeForm,
|
IncomeForm,
|
||||||
@ -12,6 +21,7 @@ from .forms import (
|
|||||||
FuelEntryForm,
|
FuelEntryForm,
|
||||||
CategoryForm,
|
CategoryForm,
|
||||||
GoalForm,
|
GoalForm,
|
||||||
|
TransferForm,
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
@ -353,7 +363,11 @@ def home(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def expense_list(request):
|
def expense_list(request):
|
||||||
expenses = Expense.objects.filter(owner=request.user)
|
# Las relaciones de traspaso van en select_related porque la tabla consulta
|
||||||
|
# expense.transfer en cada fila para decidir los enlaces de acción.
|
||||||
|
expenses = Expense.objects.filter(owner=request.user).select_related(
|
||||||
|
"transfer_out", "transfer_fee"
|
||||||
|
)
|
||||||
|
|
||||||
categories = Category.objects.filter(owner=request.user)
|
categories = Category.objects.filter(owner=request.user)
|
||||||
|
|
||||||
@ -984,7 +998,9 @@ def income_create(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def income_list(request):
|
def income_list(request):
|
||||||
incomes = Income.objects.filter(owner=request.user).select_related("account")
|
incomes = Income.objects.filter(owner=request.user).select_related(
|
||||||
|
"account", "transfer_in"
|
||||||
|
)
|
||||||
|
|
||||||
year_list = Income.objects.filter(owner=request.user).dates("date", "year")
|
year_list = Income.objects.filter(owner=request.user).dates("date", "year")
|
||||||
|
|
||||||
@ -1436,3 +1452,90 @@ def goal_delete(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
def settings_index(request):
|
def settings_index(request):
|
||||||
return render(request, "settings/index.html", {"active_menu": "settings"})
|
return render(request, "settings/index.html", {"active_menu": "settings"})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def transfer_list(request):
|
||||||
|
transfers = Transfer.objects.filter(owner=request.user).select_related(
|
||||||
|
"from_account", "to_account"
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"transfers/list.html",
|
||||||
|
{
|
||||||
|
"active_menu": "transfers",
|
||||||
|
"transfers": transfers,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def transfer_create(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
form = TransferForm(request.POST, user=request.user)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
transfer = form.save(commit=False)
|
||||||
|
transfer.owner = request.user
|
||||||
|
# sync() guarda el traspaso y crea las patas en una sola transacción.
|
||||||
|
transfer.sync()
|
||||||
|
|
||||||
|
messages.success(request, "Traspaso creado.")
|
||||||
|
return redirect("transfer_list")
|
||||||
|
else:
|
||||||
|
form = TransferForm(user=request.user)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"transfers/form.html",
|
||||||
|
{
|
||||||
|
"active_menu": "transfers",
|
||||||
|
"form": form,
|
||||||
|
"title": "Nuevo traspaso",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def transfer_edit(request, pk):
|
||||||
|
transfer = get_object_or_404(Transfer, pk=pk, owner=request.user)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = TransferForm(request.POST, instance=transfer, user=request.user)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
transfer = form.save(commit=False)
|
||||||
|
transfer.sync()
|
||||||
|
|
||||||
|
messages.success(request, "Traspaso actualizado.")
|
||||||
|
return redirect("transfer_list")
|
||||||
|
else:
|
||||||
|
form = TransferForm(instance=transfer, user=request.user)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"transfers/form.html",
|
||||||
|
{
|
||||||
|
"active_menu": "transfers",
|
||||||
|
"form": form,
|
||||||
|
"title": "Editar traspaso",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def transfer_delete(request, pk):
|
||||||
|
transfer = get_object_or_404(Transfer, pk=pk, owner=request.user)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
# Transfer.delete() se lleva también las dos patas y la comisión.
|
||||||
|
transfer.delete()
|
||||||
|
messages.success(request, "Traspaso eliminado.")
|
||||||
|
return redirect("transfer_list")
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"transfers/confirm_delete.html",
|
||||||
|
{"active_menu": "transfers", "transfer": transfer},
|
||||||
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user