Procházet zdrojové kódy

Merge branch 'main' of https://forgejo.reymota.es/creylopez/misfinanzas

Celestino Rey před 5 měsíci
rodič
revize
63c8edc6cf

+ 124 - 126
src/finanzas/views.py

@@ -328,6 +328,10 @@ def listar_categorias(request):
 
     form_filtro = FiltroForm(request.GET or None)
 
+
+    fecha_fin = timezone.localtime().strftime('%Y-%m-%d')
+    fecha_inicio = timezone.localtime().date().replace(day=1).strftime('%Y-%m-%d')
+
     if form_filtro.is_valid():
         busqueda = form_filtro.cleaned_data.get('buscar')
         if busqueda:
@@ -335,132 +339,11 @@ def listar_categorias(request):
                 db_models.Q(nombre__icontains=busqueda)
             )
 
-    return render(request, 'listar_categorias.html', {'categorias': categorias, 'form_filtro': form_filtro})
-
-@login_required
-def informe_categoria(request):
-    categoria_id = request.GET.get('categoria_id')
-
-    categoria = get_object_or_404(Categoria, id=categoria_id)
-
-    form = FiltroInformeCuentaForm(request.GET or None)
-
-    if form.is_valid():
-        # obtener fechas desde GET o establecer por defecto (mes actual)
-        hoy = timezone.localtime()
-        primer_dia = hoy.replace(day=1)
-
-        fecha_inicio_str = request.GET.get('fecha_inicio')
-        fecha_fin_str = request.GET.get('fecha_fin')
-
-        if fecha_inicio_str and fecha_fin_str:
-            fecha_inicio = datetime.strptime(fecha_inicio_str, '%Y-%m-%d').date()
-            fecha_fin = datetime.strptime(fecha_fin_str, '%Y-%m-%d').date()
-        else:
-            fecha_inicio = primer_dia
-            fecha_fin = hoy
-
-        # Convertir a datetime aware para evitar warnings
-        fecha_inicio_dt = timezone.make_aware(datetime.combine(fecha_inicio, time.min))
-        fecha_fin_dt = timezone.make_aware(datetime.combine(fecha_fin, time.max))
-
-        movimientos = Movimiento.objects.filter(
-            categoria=categoria,
-            usuario=request.user,
-            fecha__range=(fecha_inicio_dt, fecha_fin_dt)
-        ).order_by('fecha')
-
-        context = {
-            'categoria': categoria,
-            'movimientos': movimientos,
-            'fecha_inicio': fecha_inicio_str,
-            'fecha_fin': fecha_fin_str,
-        }
-
-        return render(request, 'informe_categoria.html', context)
-    
-@login_required
-def informe_categoria_pdf(request):
-    categoria_id = request.GET.get('categoria_id')
-
-    categoria = get_object_or_404(Categoria, id=categoria_id)
-
-    # obtener fechas desde GET o establecer por defecto (mes actual)
-    hoy = timezone.localtime()
-    primer_dia = hoy.replace(day=1)
-
-    fecha_inicio_str = request.GET.get('fecha_inicio')
-    fecha_fin_str = request.GET.get('fecha_fin')
-
-    if fecha_inicio_str and fecha_fin_str:
-        fecha_inicio = datetime.strptime(fecha_inicio_str, '%Y-%m-%d').date()
-        fecha_fin = datetime.strptime(fecha_fin_str, '%Y-%m-%d').date()
-    else:
-        fecha_inicio = primer_dia
-        fecha_fin = hoy
-
-    # Convertir a datetime aware para evitar warnings
-    fecha_inicio_dt = timezone.make_aware(datetime.combine(fecha_inicio, time.min))
-    fecha_fin_dt = timezone.make_aware(datetime.combine(fecha_fin, time.max))
-
-    movimientos = Movimiento.objects.filter(
-        categoria=categoria,
-        usuario=request.user,
-        fecha__range=(fecha_inicio_dt, fecha_fin_dt)
-    ).order_by('fecha')
-
-    template = get_template('informe_categoria_pdf.html')
-    html = template.render({'categoria': categoria, 'fecha_inicio': fecha_inicio_str, 'fecha_fin': fecha_fin_str, 'movimientos': movimientos})
-
-    with tempfile.NamedTemporaryFile(delete=True) as output:
-        HTML(string=html).write_pdf(output.name)
-
-        output.seek(0)
-        response = HttpResponse(output.read(), content_type='application/pdf')
-        response['Content-Disposition'] = f'filename=informe_{categoria.nombre}.pdf'
-        return response
-
-@login_required
-def informe_categoria(request):
-    categoria_id = request.GET.get('categoria_id')
-
-    categoria = get_object_or_404(Categoria, id=categoria_id)
-
-    form = FiltroInformeCuentaForm(request.GET or None)
-
-    if form.is_valid():
-        # obtener fechas desde GET o establecer por defecto (mes actual)
-        hoy = timezone.localtime()
-        primer_dia = hoy.replace(day=1)
-
-        fecha_inicio_str = request.GET.get('fecha_inicio')
-        fecha_fin_str = request.GET.get('fecha_fin')
-
-        if fecha_inicio_str and fecha_fin_str:
-            fecha_inicio = datetime.strptime(fecha_inicio_str, '%Y-%m-%d').date()
-            fecha_fin = datetime.strptime(fecha_fin_str, '%Y-%m-%d').date()
-        else:
-            fecha_inicio = primer_dia
-            fecha_fin = hoy
-
-        # Convertir a datetime aware para evitar warnings
-        fecha_inicio_dt = timezone.make_aware(datetime.combine(fecha_inicio, time.min))
-        fecha_fin_dt = timezone.make_aware(datetime.combine(fecha_fin, time.max))
-
-        movimientos = Movimiento.objects.filter(
-            categoria=categoria,
-            usuario=request.user,
-            fecha__range=(fecha_inicio_dt, fecha_fin_dt)
-        ).order_by('fecha')
-
-        context = {
-            'categoria': categoria,
-            'movimientos': movimientos,
-            'fecha_inicio': fecha_inicio_str,
-            'fecha_fin': fecha_fin_str,
-        }
-
-        return render(request, 'informe_categoria.html', context)
+    return render(request, 'listar_categorias.html', 
+                  {'categorias': categorias, 
+                   'form_filtro': form_filtro, 
+                   'fecha_inicio': fecha_inicio, 
+                   'fecha_fin': fecha_fin})
 
 # API
 
@@ -749,3 +632,118 @@ def informe_cuenta_pdf(request):
         response = HttpResponse(output.read(), content_type='application/pdf')
         response['Content-Disposition'] = f'filename=informe_{cuenta.nombre}.pdf'
         return response
+
+@login_required
+def informe_categoria(request):
+    categoria_id = request.GET.get('categoria_id')
+
+    categoria = get_object_or_404(Categoria, id=categoria_id)
+
+    form = FiltroInformeCuentaForm(request.GET or None)
+
+    if form.is_valid():
+        # obtener fechas desde GET o establecer por defecto (mes actual)
+        hoy = timezone.localtime()
+        primer_dia = hoy.replace(day=1)
+
+        fecha_inicio_str = request.GET.get('fecha_inicio')
+        fecha_fin_str = request.GET.get('fecha_fin')
+
+        if fecha_inicio_str and fecha_fin_str:
+            fecha_inicio = datetime.strptime(fecha_inicio_str, '%Y-%m-%d').date()
+            fecha_fin = datetime.strptime(fecha_fin_str, '%Y-%m-%d').date()
+        else:
+            fecha_inicio = primer_dia
+            fecha_fin = hoy
+
+        # Convertir a datetime aware para evitar warnings
+        fecha_inicio_dt = timezone.make_aware(datetime.combine(fecha_inicio, time.min))
+        fecha_fin_dt = timezone.make_aware(datetime.combine(fecha_fin, time.max))
+
+        movimientos = Movimiento.objects.filter(
+            Q(categoria=categoria),
+            fecha__range=(fecha_inicio_dt, fecha_fin_dt)
+        ).order_by('fecha')
+
+        saldo = 0
+
+        movimientos_con_saldo = []
+
+        for mov in movimientos:
+            saldo += mov.monto
+           
+            movimientos_con_saldo.append({
+                'movimiento': mov,
+                'saldo_despues': saldo
+            })
+
+        saldo_final = saldo
+
+        context = {
+            'categoria': categoria,
+            'movimientos_con_saldo': movimientos_con_saldo,
+            'fecha_inicio': fecha_inicio_str,
+            'fecha_fin': fecha_fin_str,
+            'saldo_final': saldo
+        }
+
+        return render(request, 'informe_categoria.html', context)
+    
+@login_required
+def informe_categoria_pdf(request):
+    categoria_id = request.GET.get('categoria_id')
+
+    categoria = get_object_or_404(Categoria, id=categoria_id)
+
+    # obtener fechas desde GET o establecer por defecto (mes actual)
+    hoy = timezone.localtime()
+    primer_dia = hoy.replace(day=1)
+
+    fecha_inicio_str = request.GET.get('fecha_inicio')
+    fecha_fin_str = request.GET.get('fecha_fin')
+
+    if fecha_inicio_str and fecha_fin_str:
+        fecha_inicio = datetime.strptime(fecha_inicio_str, '%Y-%m-%d').date()
+        fecha_fin = datetime.strptime(fecha_fin_str, '%Y-%m-%d').date()
+    else:
+        fecha_inicio = primer_dia
+        fecha_fin = hoy
+
+    # Convertir a datetime aware para evitar warnings
+    fecha_inicio_dt = timezone.make_aware(datetime.combine(fecha_inicio, time.min))
+    fecha_fin_dt = timezone.make_aware(datetime.combine(fecha_fin, time.max))
+
+    movimientos = Movimiento.objects.filter(
+        categoria=categoria,
+        usuario=request.user,
+        fecha__range=(fecha_inicio_dt, fecha_fin_dt)
+    ).order_by('fecha')
+
+    saldo = 0
+
+    historial = []
+
+    for mov in movimientos:
+        saldo += mov.monto
+
+        historial.append({
+            'fecha': mov.fecha,
+            'descripcion': mov.descripcion,
+            'desde': mov.cuenta_origen.nombre if mov.cuenta_origen else 'N/A',
+            'hasta': mov.cuenta_destino.nombre if mov.cuenta_destino else 'N/A',
+            'monto': mov.monto,
+            'saldo_despues': saldo
+        })
+
+    saldo_final = saldo        
+
+    template = get_template('informe_categoria_pdf.html')
+    html = template.render({'categoria': categoria, 'fecha_inicio': fecha_inicio_str, 'fecha_fin': fecha_fin_str, 'saldo_final': saldo, 'historial': historial})
+
+    with tempfile.NamedTemporaryFile(delete=True) as output:
+        HTML(string=html).write_pdf(output.name)
+
+        output.seek(0)
+        response = HttpResponse(output.read(), content_type='application/pdf')
+        response['Content-Disposition'] = f'filename=informe_{categoria.nombre}.pdf'
+        return response

+ 77 - 0
src/templates/informe_categoria.html

@@ -0,0 +1,77 @@
+{% extends "base.html" %}
+{% load humanize %}
+{% load filtros_de_entorno %}
+
+{% block content %}
+
+<div class="container-xl">
+    <div class="position-relative mb-3">  
+        <h3 class="i-name">
+            Informe de categoría: {{ categoria.nombre }}
+        </h3>
+        <h4 style="padding: 30px 30px 0 30px;">
+        Periodo del {{ fecha_inicio }} hasta {{ fecha_fin }} -  Saldo final: {{ saldo_final|formato_numero:2  }} €
+        </h4> 
+
+        <div class="row g-4 mb-4">
+            <div class="form-group">
+                <form method="get" class="form-inline mb-3">
+                    <div class="row g-2 align-items-end">
+                        <input type="hidden" name="categoria_id" value="{{ categoria.id }}">
+                        <div class="col-auto">
+                            <label>Desde:</label>
+                            <input type="date" name="fecha_inicio" value="{{ fecha_inicio|date:'Y-m-d' }}">
+                        </div>
+                        <div class="col-auto">
+                            <label>Hasta:</label>
+                            <input type="date" name="fecha_fin" value="{{ fecha_fin|date:'Y-m-d' }}">
+                        </div>
+                        <div class="col-auto ">
+                            <button type="submit" class="btn btn-primary">Filtrar</button>
+                        </div>
+                    </div>
+                </form>
+            </div>
+
+            <table width="100%" class="table app-table-hover mb-0 text-left">
+                <thead>
+                    <tr>
+                        <td>Fecha</td>
+                        <td>Descripción</td>
+                        <td>Origen</td>
+                        <td>Destino</td>
+                        <td class="text-end">Monto (€)</td>
+                        <td class="text-end">Saldo Acumulado (€)</td>
+                    </tr>
+                </thead>
+
+                <tbody>
+                {% for movimiento in movimientos_con_saldo %}
+                    <tr class="{% cycle 'fila-impar' 'fila-par' %}">
+                        <td>{{ movimiento.movimiento.fecha|date:"Y-m-d H:i" }}</td>
+                        <td>{{ movimiento.movimiento.descripcion }}</td>
+                        <td>{{ movimiento.movimiento.cuenta_origen }}</td>
+                        <td>{{ movimiento.movimiento.cuenta_destino }}</td>
+                        {% if movimiento.movimiento.tipo == 'Ingreso' %}
+                        <td style="color: green" class="text-end">{{ movimiento.movimiento.monto|formato_numero:2 }}</td>
+                        {% else %}
+                        <td style="color: red" class="text-end">-{{ movimiento.movimiento.monto|formato_numero:2 }}</td>
+                        {% endif %}
+                        <td class="text-end">{{ movimiento.saldo_despues|formato_numero:2 }}</td>
+                    </tr>
+                {% empty %}
+                    <tr><td colspan="3">No hay movimientos para esta categoría en el periodo seleccionado.</td></tr>
+                {% endfor %}
+                </tbody>
+            </table>
+
+            <div class="row g-4 mb-4">
+                <a href="{% url 'listar_categorias' %}" class="btn btn-secondary">Volver</a>
+                <a href="{% url 'informe_categoria_pdf'%}?categoria_id={{ categoria.id }}&fecha_inicio={{ fecha_inicio }}&fecha_fin={{ fecha_fin }}" class="btn btn-outline-primary" target="_blank">
+                Descargar PDF
+                </a>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 44 - 0
src/templates/informe_categoria_pdf.html

@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    {% load filtros_de_entorno %}
+    <style>
+        body { font-family: sans-serif; font-size: 12px; }
+        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
+        th, td { border: 1px solid #ccc; padding: 4px; text-align: right; }
+        th { background: #f0f0f0; }
+        td.text-left, th.text-left { text-align: left; }
+    </style>
+</head>
+<body>
+    <h2 style="color: chocolate;">Informe de categoría: {{ categoria.nombre }}</h2>
+    <h4>
+        Periodo del {{ fecha_inicio }} hasta {{ fecha_fin }} -  Saldo final: {{ saldo_final|formato_numero:2 }} €
+    </h4>
+    <table>
+        <thead>
+            <tr>
+                <th class="text-left">Fecha</th>
+                <th class="text-left">Descripción</th>
+                <th class="text-left">Origen</th>
+                <th class="text-left">Destino</th>
+                <th>Monto (€)</th>
+                <th>Saldo después (€)</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for item in historial %}
+            <tr>
+                <td class="text-left">{{ item.fecha|date:"Y-m-d H:i" }}</td>
+                <td class="text-left">{{ item.descripcion }}</td>
+                <td class="text-left">{{ item.desde }}</td>
+                <td class="text-left">{{ item.hasta }}</td>
+                <td style="color: green" >{{ item.monto|formato_numero:2 }}</td>
+                <td>{{ item.saldo_despues|formato_numero:2 }}</td>
+            </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+</body>
+</html>

+ 1 - 3
src/templates/listar_categorias.html

@@ -15,7 +15,6 @@
                 <thead>
                     <tr>
                         <td>Nombre</td>
-                        <td>Tipo</td>
                         <td>Acciones</td>
                     </tr>
                 </thead>
@@ -27,8 +26,7 @@
                             <a href="{% url 'informe_categoria' %}?categoria_id={{ categoria.id }}&fecha_inicio={{ fecha_inicio }}&fecha_fin={{ fecha_fin }}">{{ categoria.nombre }}</a>
                         </td>
                         <td class="movimiento">
-                            {{ categoria.tipo }}
-                        </td>
+
                         <td class="edit">                      
                             <a href="{% url 'informe_categoria' %}?categoria_id={{ categoria.id }}&fecha_inicio={{ fecha_inicio }}&fecha_fin={{ fecha_fin }}">
                                 <i class="bi bi-filetype-pdf"></i>