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 '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 '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 '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">
|
||||
|
||||
@ -145,7 +145,10 @@
|
||||
{% endfor %}
|
||||
</td>
|
||||
<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_delete' expense.id %}?next={{ request.get_full_path|urlencode }}" class="danger">Eliminar</a>
|
||||
{% else %}
|
||||
|
||||
@ -93,8 +93,13 @@
|
||||
<td>{{ income.amount|floatformat:2 }}</td>
|
||||
<td>{{ income.date }}</td>
|
||||
<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_delete' income.id %}" class="danger">Eliminar</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% 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/<int:pk>/edit/', views.fuel_edit, name='fuel_edit'),
|
||||
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/<int:pk>/edit/', views.category_edit, name='category_edit'),
|
||||
path('categories/<int:pk>/delete/', views.category_delete, name='category_delete'),
|
||||
|
||||
@ -3,7 +3,16 @@ import calendar
|
||||
from datetime import date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
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 (
|
||||
ExpenseForm,
|
||||
IncomeForm,
|
||||
@ -12,6 +21,7 @@ from .forms import (
|
||||
FuelEntryForm,
|
||||
CategoryForm,
|
||||
GoalForm,
|
||||
TransferForm,
|
||||
)
|
||||
|
||||
from django.core.paginator import Paginator
|
||||
@ -353,7 +363,11 @@ def home(request):
|
||||
|
||||
@login_required
|
||||
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)
|
||||
|
||||
@ -984,7 +998,9 @@ def income_create(request):
|
||||
|
||||
@login_required
|
||||
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")
|
||||
|
||||
@ -1436,3 +1452,90 @@ def goal_delete(request, pk):
|
||||
@login_required
|
||||
def settings_index(request):
|
||||
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