Configuración de automatización de CICD #35

Merged
jkuijperm merged 9 commits from dev into main 2026-09-29 16:11:55 +00:00
8 changed files with 238 additions and 6 deletions
Showing only changes of commit 0dd34ab5dc - Show all commits

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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