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

Empiezo a añadir gestión de categorías

Celestino Rey пре 7 месеци
родитељ
комит
bdfc81564f
4 измењених фајлова са 208 додато и 0 уклоњено
  1. 6 0
      src/finanzas/urls.py
  2. 139 0
      src/finanzas/views.py
  3. 14 0
      src/templates/base.html
  4. 49 0
      src/templates/listar_categorias.html

+ 6 - 0
src/finanzas/urls.py

@@ -17,6 +17,12 @@ urlpatterns = [
     path('cuentas/informe/pdf/', views.informe_cuenta_pdf, name='informe_cuenta_pdf'),
     path('informe-cuenta/', views.informe_cuenta, name='informe_cuenta'),
 
+    # Categorías
+    path('categorias/', views.listar_categorias, name='listar_categorias'),
+    path('categorias/<int:categoria_id>/informe/', views.informe_categoria, name='informe_categoria'),
+    path('categorias/informe/pdf/', views.informe_categoria_pdf, name='informe_categoria_pdf'),
+    path('informe-categoria/', views.informe_categoria, name='informe_categoria'),
+    
     # API
     path('api/datos/', views.api_datos_completos, name='api_datos_completos'),
 

+ 139 - 0
src/finanzas/views.py

@@ -322,6 +322,145 @@ def listar_cuentas(request):
                    'fecha_fin': fecha_fin
                    })
 
+@login_required
+def listar_categorias(request):
+    categorias = Categoria.objects.all().order_by('nombre')
+
+    form_filtro = FiltroForm(request.GET or None)
+
+    if form_filtro.is_valid():
+        busqueda = form_filtro.cleaned_data.get('buscar')
+        if busqueda:
+            categorias = categorias.filter(
+                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)
 
 # API
 

+ 14 - 0
src/templates/base.html

@@ -150,6 +150,20 @@
                              <span class="nav-link-text">Ver cuentas</span>
                         </a><!--//nav-link-->
                     </li><!--//nav-item-->
+
+                    <li class="nav-item">
+                        <!--//Bootstrap Icons: https://icons.getbootstrap.com/ -->
+                        <a class="nav-link" href="{% url 'listar_categorias' %}">
+                            <span class="nav-icon">
+                            <!--//Bootstrap Icons: https://icons.getbootstrap.com/ -->
+                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tags" viewBox="0 0 16 16">
+                                <path d="M3 2a1 1 0 0 1 .707.293l9 9a1 1 0 0 1 0 1.414l-2.5 2.5A1 1 0 0 1 10.5 15h-7a1 1 0 0 1-1-1V3a1 1 0 0 1 .293-.707L2.293.293A1 1 0 0 1 3 .001zM4.5 4a.5.5 0 1 0-1.001-.001A.5.5 0 0 0 4.5 4zm6.364-.364L4.707 2H3v2h7V3h-2.707l5.657-5.657a.5.5 0 0 1 .707.707z"/>
+                                <path d="M11.354 8.646a.5.5 0 0 1 .707-.707l2.5-2.5a1.00101 1.00101 0 0 1 .293-.207V10h-7V9h6v-.707a1.00101 1.00101 0 0 1 .293-.207l-2.5-2.5z"/>
+                            </svg>
+                             </span>
+                             <span class="nav-link-text">Ver categorías</span>
+                        </a><!--//nav-link-->
+                    </li><!--//nav-item-->
                     
                     <li class="nav-item">
                         <!--//Bootstrap Icons: https://icons.getbootstrap.com/ -->

+ 49 - 0
src/templates/listar_categorias.html

@@ -0,0 +1,49 @@
+{% 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">
+            Mis Categorías
+        </h3>
+        
+        <div class="row g-4 mb-4">
+            <table width="100%" class="table app-table-hover mb-0 text-left">
+                <thead>
+                    <tr>
+                        <td>Nombre</td>
+                        <td>Tipo</td>
+                        <td>Acciones</td>
+                    </tr>
+                </thead>
+
+                <tbody>
+                {% for categoria in categorias %}
+                    <tr class="{% cycle 'fila-impar' 'fila-par' %}">
+                        <td class="movimiento">
+                            <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>
+                            </a>
+                        </td>   
+                    </tr>
+                {% empty %}
+                    <tr><td colspan="3">No tienes categorías aún.</td></tr>
+                {% endfor %}
+                </tbody>
+            </table>
+            <div class="row g-4 mb-4">
+                <a href="{% url 'dashboard' %}"><button class="btn btn-secondary" type="button">Volver al Dashboard</button></a>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}