Преглед изворни кода

Informes en pdf y cambio de versión

Celestino Rey пре 1 година
родитељ
комит
20235b60c5

+ 1 - 1
Dockerfile

@@ -2,7 +2,7 @@
 FROM python:3.11-slim
 
 # install nginx y otras cosas
-RUN apt-get update && apt-get install nginx netcat-openbsd curl vim jq rsync -y
+RUN apt-get update && apt-get install nginx netcat-openbsd curl vim jq rsync libpango-1.0-0 libpangocairo-1.0-0 libcairo2 -y
 
 # copy our nginx configuration to overwrite nginx defaults
 RUN rm /etc/nginx/sites-enabled/default

+ 1 - 1
Dockerfile.k8s

@@ -40,7 +40,7 @@ RUN mkdir -p $APP_HOME
 WORKDIR $APP_HOME
 
 # install system dependencies
-RUN apt-get update && apt-get install -y sqlite3 netcat vim procps curl jq
+RUN apt-get update && apt-get install -y sqlite3 netcat vim procps curl jq libpango-1.0-0 libpangocairo-1.0-0 libcairo2
 COPY --from=builder /app/wheels /wheels
 COPY --from=builder /app/requirements.txt .
 

+ 1 - 1
K8S/Makefile

@@ -1,6 +1,6 @@
 export ARQUITECTURA := $(shell lscpu |grep itectur | tr -d ' '| cut -f2 -d':')
 export REGISTRY=registry.reymota.es
-export IMG_VERSION = 0.36
+export IMG_VERSION = 0.37
 export IMG_NGINX_VERSION = 1.0
 
 # limpia todo

+ 23 - 0
src/finanzas/forms.py

@@ -140,3 +140,26 @@ class FiltroCuentaForm(forms.Form):
         required=False,
         widget=forms.Select(attrs={'class': 'form-control'})
     )
+
+
+class InformeCuentaForm(forms.Form):
+    cuenta = forms.ModelChoiceField(queryset=Cuenta.objects.all(), label="Cuenta")
+    fecha_inicio = forms.DateField(label="Desde", widget=forms.DateInput(attrs={'type': 'date'}))
+    fecha_fin = forms.DateField(label="Hasta", widget=forms.DateInput(attrs={'type': 'date'}))
+
+    def __init__(self, *args, cuenta_id=None, **kwargs):
+        super().__init__(*args, **kwargs)
+        hoy = date.today()
+        primero_mes = hoy.replace(day=1)
+
+        self.fields['fecha_inicio'].initial = primero_mes
+        self.fields['fecha_fin'].initial = hoy
+
+        if cuenta_id:
+            try:
+                self.fields['cuenta'].initial = Cuenta.objects.get(id=cuenta_id)
+            except Cuenta.DoesNotExist:
+                pass  # o lanza un error si prefieres
+
+        for field in self.fields.values():
+            field.widget.attrs['class'] = 'form-control'

+ 4 - 0
src/finanzas/urls.py

@@ -15,6 +15,10 @@ urlpatterns = [
     path('cuentas/<int:cuenta_id>/editar/', views.editar_cuenta, name='editar_cuenta'),
     path('cuentas/<int:cuenta_id>/eliminar/', views.eliminar_cuenta, name='eliminar_cuenta'),
     path('cuenta/<int:cuenta_id>/', views.ver_cuenta, name='ver_cuenta'),
+    path('cuentas/<int:cuenta_id>/informe/', views.informe_cuenta, name='informe_cuenta'),
+    path('cuentas/<int:cuenta_id>/informe/pdf/', views.informe_cuenta_pdf, name='informe_cuenta_pdf'),
+    path('cuentas/informe-cuenta/', views.informe_cuenta_form, name='informe_cuenta_form'),
+    path('cuentas/informe-cuenta/pdf/<int:cuenta_id>/<str:fecha_inicio>/<str:fecha_fin>/', views.informe_cuenta_pdf_rango, name='informe_cuenta_pdf_rango'),
 
     # API
     path('api/datos/', views.api_datos_completos, name='api_datos_completos'),

+ 151 - 2
src/finanzas/views.py

@@ -11,15 +11,19 @@ from django.conf import settings
 from django.utils.timezone import localtime, now, make_aware, is_naive
 from calendar import monthrange
 from django.utils import timezone
-from django.db.models import Sum
+from django.db.models import Q
 from collections import defaultdict
+from django.template.loader import get_template
+from django.http import HttpResponse
+from weasyprint import HTML
+import tempfile
 
 import logging
 import re
 
 from accounts import models
 from .models import Movimiento, Cuenta, MovimientoFrecuente, Categoria, TransaccionRecurrente
-from .forms import MovimientoForm, FiltroForm, CuentaForm, FiltroMesAnioForm, FiltroCuentaForm
+from .forms import MovimientoForm, FiltroForm, CuentaForm, FiltroMesAnioForm, FiltroCuentaForm, InformeCuentaForm
 from .recurrentes import procesar_transacciones_recurrentes
 
 
@@ -452,3 +456,148 @@ def resumen_saldos_mensuales(request):
         'meses': meses_ordenados,
         'saldos_mensuales': saldos_mensuales,
     })
+
+
+# Informes
+
+
+@login_required
+def informe_cuenta(request, cuenta_id):
+    cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
+
+    movimientos = Movimiento.objects.filter(
+        Q(cuenta_origen=cuenta) | Q(cuenta_destino=cuenta)
+    ).order_by('fecha')
+
+    saldo = 0
+    historial = []
+
+    for mov in movimientos:
+        if mov.cuenta_origen == cuenta:
+            saldo -= mov.monto
+            tipo = 'Salida'
+        elif mov.cuenta_destino == cuenta:
+            saldo += mov.monto
+            tipo = 'Entrada'
+        else:
+            tipo = 'Otro'  # Por seguridad, aunque no debería ocurrir
+
+        historial.append({
+            'fecha': mov.fecha,
+            'descripcion': mov.descripcion,
+            'monto': mov.monto,
+            'tipo': tipo,
+            'saldo_despues': saldo,
+        })
+
+    context = {
+        'cuenta': cuenta,
+        'historial': historial,
+    }
+    return render(request, 'informe_cuenta.html', context)
+
+
+@login_required
+def informe_cuenta_pdf(request, cuenta_id):
+    cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
+
+    movimientos = Movimiento.objects.filter(
+        Q(cuenta_origen=cuenta) | Q(cuenta_destino=cuenta)
+    ).order_by('fecha')
+
+    saldo = 0
+    historial = []
+
+    for mov in movimientos:
+        if mov.cuenta_origen == cuenta:
+            saldo -= mov.monto
+            tipo = 'Salida'
+        elif mov.cuenta_destino == cuenta:
+            saldo += mov.monto
+            tipo = 'Entrada'
+        else:
+            tipo = 'Otro'
+
+        historial.append({
+            'fecha': mov.fecha,
+            'descripcion': mov.descripcion,
+            'monto': mov.monto,
+            'tipo': tipo,
+            'saldo_despues': saldo,
+        })
+
+    template = get_template('informe_cuenta_pdf.html')
+    html = template.render({'cuenta': cuenta, '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'] = 'inline; filename=informe_{cuenta.nombre}.pdf '
+        return response
+
+
+@login_required
+def informe_cuenta_form(request):
+    if request.method == 'POST':
+        cuenta_id = request.GET.get('cuenta_id')
+
+        form = InformeCuentaForm(request.GET or None, cuenta_id=cuenta_id)
+        
+        if form.is_valid():
+            cuenta = form.cleaned_data['cuenta']
+            fecha_inicio = form.cleaned_data['fecha_inicio']
+            fecha_fin = form.cleaned_data['fecha_fin']
+            return redirect('informe_cuenta_pdf_rango', cuenta.id, fecha_inicio.isoformat(), fecha_fin.isoformat())
+    else:
+        form = InformeCuentaForm(user=request.user)
+
+    return render(request, 'informe_cuenta_form.html', {'form': form})
+
+
+@login_required
+def informe_cuenta_pdf_rango(request, cuenta_id, fecha_inicio, fecha_fin):
+    cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
+    fi = datetime.fromisoformat(fecha_inicio)
+    ff = datetime.fromisoformat(fecha_fin)
+
+    movimientos = Movimiento.objects.filter(
+        Q(cuenta_origen=cuenta) | Q(cuenta_destino=cuenta),
+        fecha__date__range=(fi.date(), ff.date())
+    ).order_by('fecha')
+
+    saldo = 0
+    historial = []
+
+    for mov in movimientos:
+        if mov.cuenta_origen == cuenta:
+            saldo -= mov.monto
+            tipo = 'Salida'
+        elif mov.cuenta_destino == cuenta:
+            saldo += mov.monto
+            tipo = 'Entrada'
+        else:
+            tipo = 'Otro'
+
+        historial.append({
+            'fecha': mov.fecha,
+            'descripcion': mov.descripcion,
+            'monto': mov.monto,
+            'tipo': tipo,
+            'saldo_despues': saldo,
+        })
+
+    template = get_template('informe_cuenta_pdf.html')
+    html = template.render({
+        'cuenta': cuenta,
+        'historial': historial,
+        'rango': f"{fi.strftime('%d/%m/%Y')} – {ff.strftime('%d/%m/%Y')}",
+    })
+
+    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_{cuenta.nombre}.pdf'
+        return response

+ 1 - 1
src/misfinanzas/settings.py

@@ -16,7 +16,7 @@ import logging
 
 
 APP_NAME = "Finanzas"
-APP_VERSION = os.environ.get("APP_VERSION", "17.0.2")  # Valor por defecto si no está definido
+APP_VERSION = os.environ.get("APP_VERSION", "17.0.3")  # Valor por defecto si no está definido
 IMG_VERSION = os.environ.get("IMG_VERSION", "0.0.0-dev")  # Valor por defecto si no está definido
 
 

+ 3 - 0
src/requirements.txt

@@ -20,3 +20,6 @@ six==1.17.0
 sqlparse==0.5.1
 typing_extensions==4.12.2
 tzdata==2025.1
+weasyprint==65.1
+webencodings==0.5.1
+zopfli==0.2.3.post1

+ 42 - 0
src/templates/informe_cuenta.html

@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+{% block interface %}
+
+<section id="interface">
+    {% include "_navigation.html" %}
+    
+    <h3 class="i-name">
+        Informe de cuenta: {{ cuenta.nombre }}    
+    </h3>
+    
+    <div class="board"> 
+        <table class="table table-striped">
+            <thead>
+                <tr>
+                    <th>Fecha</th>
+                    <th>Descripción</th>
+                    <th>Tipo</th>
+                    <th class="text-end">Monto (€)</th>
+                    <th class="text-end">Saldo después (€)</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for item in historial %}
+                <tr>
+                    <td>{{ item.fecha|date:"Y-m-d H:i" }}</td>
+                    <td>{{ item.descripcion }}</td>
+                    <td>{{ item.tipo }}</td>
+                    <td class="text-end">{{ item.monto|floatformat:2 }}</td>
+                    <td class="text-end">{{ item.saldo_despues|floatformat:2 }}</td>
+                </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+        <div class="botones">
+            <a href="{% url 'listar_cuentas' %}" class="btn btn-secondary">Volver</a>
+            <a href="{% url 'informe_cuenta_pdf' cuenta.id %}" class="btn btn-outline-primary" target="_blank">
+            Descargar PDF
+            </a>
+       </div>
+    </div>
+</section>
+{% endblock %}

+ 22 - 0
src/templates/informe_cuenta_form.html

@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+
+{% block interface %}
+
+<section id="interface">
+    <div class="board">
+    {% include "_navigation.html" %}
+        <div class="div-form">
+            <h3>Generar informe de cuenta</h3>
+            <div class="form-group">
+                <form method="post">
+                    {% csrf_token %}
+                    {{ form.as_p }}
+                    <button type="submit" class="btn btn-primary">Generar PDF</button>
+                </form>
+        </div>
+        <div class="botones">
+            <a href="{% url 'listar_cuentas' %}"><button class="btn btn-secondary" type="button">Volver a cuentas</button></a>
+        </div>
+    </div>
+</section>
+{% endblock %}

+ 38 - 0
src/templates/informe_cuenta_pdf.html

@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <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>Informe de cuenta: {{ cuenta.nombre }}</h2>
+    <table>
+        <thead>
+            <tr>
+                <th class="text-left">Fecha</th>
+                <th class="text-left">Descripción</th>
+                <th class="text-left">Tipo</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.tipo }}</td>
+                <td>{{ item.monto|floatformat:2 }}</td>
+                <td>{{ item.saldo_despues|floatformat:2 }}</td>
+            </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+</body>
+</html>

+ 3 - 0
src/templates/listar_cuentas.html

@@ -44,6 +44,9 @@
                         <a href="{% url 'eliminar_cuenta' cuenta.id %}">
                             <i class="bi bi-trash3"></i>
                         </a>
+                        <a href="{% url 'informe_cuenta' cuenta.id %}">
+                            <i class="bi bi-file-earmark"></i>
+                        </a>
                     </td>   
                     <td>{% if cuenta.favorita %}⭐{% else %}-{% endif %}</td>