Просмотр исходного кода

Informes en pdf y cambio de versión

Celestino Rey 1 год назад
Родитель
Сommit
20235b60c5

+ 1 - 1
Dockerfile

@@ -2,7 +2,7 @@
 FROM python:3.11-slim
 FROM python:3.11-slim
 
 
 # install nginx y otras cosas
 # 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
 # copy our nginx configuration to overwrite nginx defaults
 RUN rm /etc/nginx/sites-enabled/default
 RUN rm /etc/nginx/sites-enabled/default

+ 1 - 1
Dockerfile.k8s

@@ -40,7 +40,7 @@ RUN mkdir -p $APP_HOME
 WORKDIR $APP_HOME
 WORKDIR $APP_HOME
 
 
 # install system dependencies
 # 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/wheels /wheels
 COPY --from=builder /app/requirements.txt .
 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 ARQUITECTURA := $(shell lscpu |grep itectur | tr -d ' '| cut -f2 -d':')
 export REGISTRY=registry.reymota.es
 export REGISTRY=registry.reymota.es
-export IMG_VERSION = 0.36
+export IMG_VERSION = 0.37
 export IMG_NGINX_VERSION = 1.0
 export IMG_NGINX_VERSION = 1.0
 
 
 # limpia todo
 # limpia todo

+ 23 - 0
src/finanzas/forms.py

@@ -140,3 +140,26 @@ class FiltroCuentaForm(forms.Form):
         required=False,
         required=False,
         widget=forms.Select(attrs={'class': 'form-control'})
         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>/editar/', views.editar_cuenta, name='editar_cuenta'),
     path('cuentas/<int:cuenta_id>/eliminar/', views.eliminar_cuenta, name='eliminar_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('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
     # API
     path('api/datos/', views.api_datos_completos, name='api_datos_completos'),
     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 django.utils.timezone import localtime, now, make_aware, is_naive
 from calendar import monthrange
 from calendar import monthrange
 from django.utils import timezone
 from django.utils import timezone
-from django.db.models import Sum
+from django.db.models import Q
 from collections import defaultdict
 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 logging
 import re
 import re
 
 
 from accounts import models
 from accounts import models
 from .models import Movimiento, Cuenta, MovimientoFrecuente, Categoria, TransaccionRecurrente
 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
 from .recurrentes import procesar_transacciones_recurrentes
 
 
 
 
@@ -452,3 +456,148 @@ def resumen_saldos_mensuales(request):
         'meses': meses_ordenados,
         'meses': meses_ordenados,
         'saldos_mensuales': saldos_mensuales,
         '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_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
 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
 sqlparse==0.5.1
 typing_extensions==4.12.2
 typing_extensions==4.12.2
 tzdata==2025.1
 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 %}">
                         <a href="{% url 'eliminar_cuenta' cuenta.id %}">
                             <i class="bi bi-trash3"></i>
                             <i class="bi bi-trash3"></i>
                         </a>
                         </a>
+                        <a href="{% url 'informe_cuenta' cuenta.id %}">
+                            <i class="bi bi-file-earmark"></i>
+                        </a>
                     </td>   
                     </td>   
                     <td>{% if cuenta.favorita %}⭐{% else %}-{% endif %}</td>
                     <td>{% if cuenta.favorita %}⭐{% else %}-{% endif %}</td>