| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703 |
- from django.shortcuts import render, redirect, get_object_or_404
- from django.http import JsonResponse
- from django.views.decorators.csrf import csrf_exempt
- from decimal import Decimal, InvalidOperation
- from django.db import models as db_models
- from django.contrib.auth.decorators import login_required
- from django.contrib import messages
- from django.core.serializers import serialize
- from datetime import date, timedelta, datetime, time
- 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 Q
- from collections import defaultdict
- from django.template.loader import get_template
- from django.http import HttpResponse
- from weasyprint import HTML
- import tempfile
- from django.utils.dateparse import parse_date
- from django.db.models import Sum
- import logging
- import re
- from accounts import models
- from .models import Movimiento, Cuenta, MovimientoFrecuente, Categoria, TransaccionRecurrente
- from .forms import MovimientoForm, FiltroForm, FiltroMesAnioForm, FiltroCuentaForm, InformeCuentaForm, FiltroInformeCuentaForm
- from .recurrentes import procesar_transacciones_recurrentes
- logger = logging.getLogger(__name__)
- @login_required
- def dashboard(request):
- # Copiamos GET para poder modificarlo
- request.GET = request.GET.copy()
- hoy = now()
- form_fecha = FiltroMesAnioForm(request.GET or None)
- mes = hoy.month
- anio = hoy.year
- if form_fecha.is_valid():
- mes = int(form_fecha.cleaned_data['mes'])
- anio = int(form_fecha.cleaned_data['anio'])
- # logger.warning(f"dashboard: mes: {mes} / año: {anio}")
- inicio_mes = timezone.make_aware(datetime(anio, mes, 1))
- ultimo_dia = monthrange(anio, mes)[1]
- fin_mes = timezone.make_aware(datetime(anio, mes, ultimo_dia, 23, 59, 59))
- movimientos = Movimiento.objects.filter(usuario=request.user, fecha__range=(inicio_mes, fin_mes)).order_by('-fecha')
- # logger.warning(f"dashboard: movimientos: {movimientos}")
- favoritas = Cuenta.objects.filter(favorita='True').order_by('nombre')
- frecuentes = MovimientoFrecuente.objects.filter(usuario=request.user).order_by('cuenta_destino')
- form_filtro = FiltroForm(request.GET or None)
- if form_filtro.is_valid():
- busqueda = form_filtro.cleaned_data.get('buscar')
- if busqueda:
- # logger.warning(f"dashboard: busqueda=true {busqueda}")
- movimientos = movimientos.filter(
- db_models.Q(descripcion__icontains=busqueda) | db_models.Q(cuenta_origen__nombre__icontains=busqueda) | db_models.Q(cuenta_destino__nombre__icontains=busqueda)
- )
- return render(request, 'dashboard.html', {
- 'movimientos': movimientos,
- 'user': request.user,
- 'form_filtro': form_filtro,
- 'form_fecha': form_fecha,
- 'favoritas': favoritas,
- 'frecuentes': frecuentes,
- 'mes': mes,
- 'anio': anio,
- })
- @login_required
- def buscatotal(request):
- # Copiamos GET para poder modificarlo
- request.GET = request.GET.copy()
- movimientos = Movimiento.objects.filter(usuario=request.user).order_by('-fecha')
- 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:
- # logger.warning(f"dashboard: busqueda=true {busqueda}")
- movimientos = movimientos.filter(
- db_models.Q(descripcion__icontains=busqueda) | db_models.Q(cuenta_origen__nombre__icontains=busqueda) | db_models.Q(cuenta_destino__nombre__icontains=busqueda)
- )
- return render(request, 'buscatotal.html', {
- 'movimientos': movimientos,
- 'user': request.user,
- 'form_filtro': form_filtro,
- 'fecha_inicio': fecha_inicio,
- 'fecha_fin': fecha_fin,
- })
- # Movimientos
- @login_required
- def agregar_movimiento(request):
- if request.method == 'POST':
- form = MovimientoForm(request.POST)
- cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
- form.fields['cuenta_origen'].queryset = cuentas
- form.fields['cuenta_destino'].queryset = cuentas
- if form.is_valid():
- movimiento = form.save(commit=False)
- movimiento.usuario = request.user
- movimiento.save()
- # Ajustar saldos de cuentas
- movimiento.cuenta_origen.saldo -= movimiento.monto
- movimiento.cuenta_origen.save()
- movimiento.cuenta_destino.saldo += movimiento.monto
- movimiento.cuenta_destino.save()
- messages.success(request, 'Movimiento registrado con éxito.')
- return redirect('dashboard')
- else:
- datos_iniciales = {}
- origen = request.GET.get('origen')
- destino = request.GET.get('destino')
- categoria = request.GET.get('categoria')
- descripcion = request.GET.get('descripcion')
- monto_raw = request.GET.get('monto')
- # logger.warning(f"agregar_movimiento: origen: {origen}")
- # logger.warning(f"agregar_movimiento: destino: {destino}")
- # logger.warning(f"agregar_movimiento: categoria: {categoria}")
- # logger.warning(f"agregar_movimiento: descripcion: {descripcion}")
- # logger.warning(f"agregar_movimiento: monto_raw: {monto_raw}")
- try:
- if origen:
- datos_iniciales['cuenta_origen'] = Cuenta.objects.get(nombre=request.GET['origen'], usuario=request.user)
- if destino:
- datos_iniciales['cuenta_destino'] = Cuenta.objects.get(nombre=request.GET['destino'], usuario=request.user)
- if categoria:
- datos_iniciales['categoria'] = Categoria.objects.get(nombre=request.GET['categoria'])
- if descripcion:
- datos_iniciales['descripcion'] = descripcion
- if monto_raw:
- # Reemplaza coma por punto, elimina símbolos y espacios
- monto_normalizado = re.sub(r'[^\d,.-]', '', monto_raw).replace(',', '.')
- try:
- datos_iniciales['monto'] = str(Decimal(monto_normalizado).quantize(Decimal('0.01')))
- except InvalidOperation:
- pass # Ignorar monto malformado
- logger.warning(f"monto: mal formado {datos_iniciales['monto']}")
- except (Cuenta.DoesNotExist, InvalidOperation):
- pass
- # logger.warning(f"agregar_movimiento: datos iniciales: {datos_iniciales}")
- cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
- form = MovimientoForm(initial=datos_iniciales)
- form.fields['cuenta_origen'].queryset = cuentas
- form.fields['cuenta_destino'].queryset = cuentas
- return render(request, 'agregar_movimiento.html', {'form': form})
- @login_required
- def editar_movimiento(request, movimiento_id):
- movimiento = get_object_or_404(Movimiento, id=movimiento_id, usuario=request.user)
- cuenta_original_origen = movimiento.cuenta_origen
- cuenta_original_destino = movimiento.cuenta_destino
- monto_original = movimiento.monto
- if request.method == 'POST':
- form = MovimientoForm(request.POST, instance=movimiento)
- cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
- form.fields['cuenta_origen'].queryset = cuentas
- form.fields['cuenta_destino'].queryset = cuentas
- # Revertir efecto anterior
- # Cuenta origen
- cuenta_original_origen.saldo += monto_original
- cuenta_original_origen.save()
- # Cuenta destino
- cuenta_original_destino.saldo -= monto_original
- cuenta_original_destino.save()
- if form.is_valid():
- movimiento_nuevo = form.save(commit=False)
- # Aplicar nuevo efecto
- # con esta modificación, que lo deja como estaba originalmente, parece que está arreglado.
- # Si cambio el importe, se ajustan los saldos origen y destino apropiadamente.
- # Si cambia una de las cuentas, también queda bien reflelado.
- # Si intercambio las cuentas, es decir, la origen pasa a ser la destino y viceversa, también queda bien reflejado.
- movimiento_nuevo.usuario = request.user
- movimiento_nuevo.save()
- movimiento_nuevo.cuenta_origen.saldo -= movimiento_nuevo.monto # ...tomar como valor del saldo el de la nueva cuenta.
- movimiento_nuevo.cuenta_origen.save()
- movimiento_nuevo.cuenta_destino.saldo += movimiento_nuevo.monto # ...tomar como valor del saldo el de la nueva cuenta.
- movimiento_nuevo.cuenta_destino.save()
- return redirect('dashboard')
- else:
- form = MovimientoForm(instance=movimiento)
- cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
- form.fields['cuenta_origen'].queryset = cuentas
- form.fields['cuenta_destino'].queryset = cuentas
- return render(request, 'editar_movimiento.html', {'form': form})
- @login_required
- def eliminar_movimiento(request, movimiento_id):
- movimiento = get_object_or_404(Movimiento, id=movimiento_id, usuario=request.user)
- if request.method == 'POST':
- # Revertir efecto del movimiento antes de borrarlo
- movimiento.cuenta_origen.saldo += movimiento.monto
- movimiento.cuenta_origen.save()
- movimiento.cuenta_destino.saldo -= movimiento.monto
- movimiento.cuenta_destino.save()
- movimiento.delete()
- return redirect('dashboard')
- return render(request, 'eliminar_movimiento.html', {'movimiento': movimiento})
- @login_required
- def duplicar_movimiento(request, movimiento_id):
- original = get_object_or_404(Movimiento, id=movimiento_id, usuario=request.user)
- # Crear una copia del movimiento sin guardar
- movimiento_nuevo = Movimiento(
- cuenta_origen=original.cuenta_origen,
- cuenta_destino=original.cuenta_destino,
- monto=original.monto,
- fecha=now(),
- descripcion=original.descripcion,
- usuario=request.user,
- categoria=original.categoria,
- )
- if request.method == 'POST':
- form = MovimientoForm(request.POST, instance=movimiento_nuevo)
- cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
- form.fields['cuenta_origen'].queryset = cuentas
- form.fields['cuenta_destino'].queryset = cuentas
- if form.is_valid():
- nuevo = form.save(commit=False)
- nuevo.usuario = request.user
- nuevo.save()
- # Ajustar saldos
- nuevo.cuenta_origen.saldo -= nuevo.monto
- nuevo.cuenta_origen.save()
- nuevo.cuenta_destino.saldo += nuevo.monto
- nuevo.cuenta_destino.save()
- messages.success(request, 'Movimiento duplicado con éxito.')
- return redirect('dashboard')
- else:
- form = MovimientoForm(instance=movimiento_nuevo)
- cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
- form.fields['cuenta_origen'].queryset = cuentas
- form.fields['cuenta_destino'].queryset = cuentas
- return render(request, 'duplicar_movimiento.html', {'form': form})
- # Cuentas
- @login_required
- def listar_cuentas(request):
- cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
- # desde aquí....
- 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:
- cuentas = cuentas.filter(
- db_models.Q(nombre__icontains=busqueda)
- )
- # hasta aquí, hay que quitarlo
- # en el render hay que pasar solo cuentas.
- return render(request, 'listar_cuentas.html',
- {'cuentas': cuentas,
- 'form_filtro': form_filtro,
- 'fecha_inicio': fecha_inicio,
- 'fecha_fin': fecha_fin
- })
- # API
- def api_datos_completos(request):
- cuentas = list(Cuenta.objects.values('id', 'nombre', 'tipo', 'saldo', 'usuario__nombre', 'favorita', 'saldo_inicial'))
- categorias = list(Categoria.objects.values('id', 'nombre'))
- movimientos = [
- {
- 'id': m.id,
- 'usuario': m.usuario.nombre,
- 'cuenta_origen': m.cuenta_origen.nombre,
- 'cuenta_destino': m.cuenta_destino.nombre,
- 'monto': float(m.monto),
- 'fecha': m.fecha.isoformat(),
- 'descripcion': m.descripcion,
- 'categoria': m.categoria.nombre if m.categoria else None,
- }
- for m in Movimiento.objects.select_related('cuenta_origen', 'cuenta_destino', 'usuario', 'categoria')
- ]
- movs_frecuentes = [
- {
- 'id': m.id,
- 'nombre': m.nombre,
- 'cuenta_origen': m.cuenta_origen.nombre,
- 'cuenta_destino': m.cuenta_destino.nombre,
- 'monto': float(m.monto),
- 'categoria': m.categoria.nombre if m.categoria else None,
- 'descripcion': m.descripcion,
- 'usuario': m.usuario.nombre,
- }
- for m in MovimientoFrecuente.objects.select_related('cuenta_origen', 'cuenta_destino', 'usuario', 'categoria')
- ]
- movs_recurrentes = [
- {
- 'id': m.id,
- 'usuario': m.usuario.nombre,
- 'cuenta_origen': m.cuenta_origen.nombre,
- 'cuenta_destino': m.cuenta_destino.nombre,
- 'monto': float(m.monto),
- 'categoria': m.categoria.nombre if m.categoria else None,
- 'descripcion': m.descripcion,
- 'frecuencia': m.frecuencia,
- 'dia_del_mes': m.dia_del_mes,
- 'dia_semana': m.dia_semana,
- 'semana': m.semana,
- 'activa': m.activa,
- }
- for m in TransaccionRecurrente.objects.select_related('cuenta_origen', 'cuenta_destino', 'usuario', 'categoria')
- ]
- return JsonResponse({
- 'cuentas': cuentas,
- 'categorias': categorias,
- 'movimientos': movimientos,
- 'movimientos_frecuentes': movs_frecuentes,
- 'transacciones_recurrentes': movs_recurrentes,
- }, safe=False)
- @login_required
- def ejecutar_recurrentes(request):
- procesar_transacciones_recurrentes()
- return redirect('dashboard')
- @csrf_exempt
- def api_ejecutar_recurrentes(request):
- token_enviado = request.headers.get('X-API-KEY') or request.GET.get('token')
- # Usar token dedicado para la API de informes
- if token_enviado != settings.REPORTS_API_TOKEN:
- logger.warning('api_informe_cuenta_pdf: intento no autorizado', extra={'ip': request.META.get('REMOTE_ADDR'), 'cuenta_id': request.GET.get('cuenta_id')})
- return JsonResponse({'error': 'No autorizado'}, status=403)
- procesar_transacciones_recurrentes(hoy=now().date())
- return JsonResponse({'ok': True, 'mensaje': 'Transacciones recurrentes ejecutadas'})
- @login_required
- def resumen_saldos_mensuales(request):
- cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
- movimientos = Movimiento.objects.filter(usuario=request.user).order_by('fecha')
- # Diccionario: {(cuenta_id, año, mes): saldo}
- saldos_mensuales = defaultdict(lambda: 0)
- # Diccionario para llevar el saldo acumulado
- saldos_acumulados = {cuenta.id: float(cuenta.saldo_inicial) for cuenta in cuentas}
- # Obtener todos los meses presentes en movimientos
- meses_presentes = set()
- for mov in movimientos:
- anio = mov.fecha.year
- mes = mov.fecha.month
- clave = (mov.cuenta_origen.id, anio, mes)
- meses_presentes.add((anio, mes))
- # Resta al origen
- saldos_acumulados[mov.cuenta_origen.id] -= float(mov.monto)
- saldos_mensuales[clave] = saldos_acumulados[mov.cuenta_origen.id]
- # Suma al destino
- clave_dest = (mov.cuenta_destino.id, anio, mes)
- saldos_acumulados[mov.cuenta_destino.id] += float(mov.monto)
- saldos_mensuales[clave_dest] = saldos_acumulados[mov.cuenta_destino.id]
- # Ordenar meses
- meses_ordenados = sorted(meses_presentes)
- return render(request, 'resumen_saldos_mensuales.html', {
- 'cuentas': cuentas,
- 'meses': meses_ordenados,
- 'saldos_mensuales': saldos_mensuales,
- })
- # Informes
- @login_required
- def informe_cuenta(request):
- cuenta_id = request.GET.get('cuenta_id')
- cuenta = get_object_or_404(Cuenta, id=cuenta_id)
- logger.info('api_informe_cuenta_pdf: petición aceptada', extra={'ip': request.META.get('REMOTE_ADDR'), 'cuenta_id': cuenta_id, 'usuario': getattr(request, 'user', None)})
- 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))
- # Saldo antes del periodo
- movimientos_previos = Movimiento.objects.filter(
- fecha__lt=fecha_inicio_dt,
- ).filter(
- cuenta_origen=cuenta
- ).aggregate(total_origen=Sum('monto')) or {'total_origen': 0}
- entradas_previas = Movimiento.objects.filter(
- fecha__lt=fecha_inicio_dt,
- ).filter(
- cuenta_destino=cuenta
- ).aggregate(total_destino=Sum('monto')) or {'total_destino': 0}
- saldo_inicial = (entradas_previas['total_destino'] or 0) - (movimientos_previos['total_origen'] or 0)
- saldo_inicial = saldo_inicial + cuenta.saldo_inicial
- movimientos = Movimiento.objects.filter(
- Q(cuenta_origen=cuenta) | Q(cuenta_destino=cuenta),
- fecha__range=(fecha_inicio_dt, fecha_fin_dt)
- ).order_by('fecha')
- # Cálculo de saldos después de cada movimiento
- saldo = saldo_inicial
- movimientos_con_saldo = []
- for mov in movimientos:
- if mov.cuenta_origen == cuenta:
- saldo -= mov.monto
- tipo = 'Gasto'
- elif mov.cuenta_destino == cuenta:
- saldo += mov.monto
- tipo = 'Ingreso'
- movimientos_con_saldo.append({
- 'id': mov.id,
- 'fecha': mov.fecha,
- 'descripcion': mov.descripcion,
- 'monto': mov.monto,
- 'tipo': tipo,
- 'desde': mov.cuenta_origen.nombre if mov.cuenta_origen else 'N/A',
- 'hasta': mov.cuenta_destino.nombre if mov.cuenta_destino else 'N/A',
- 'saldo_despues': saldo,
- })
- # Renderizado del PDF o del HTML como prefieras
- context = {
- 'cuenta': cuenta,
- 'movimientos_con_saldo': movimientos_con_saldo,
- 'fecha_inicio': fecha_inicio_str,
- 'fecha_fin': fecha_fin_str,
- 'saldo_inicial': saldo_inicial,
- }
- return render(request, 'informe_cuenta.html', context)
- @login_required
- def informe_cuenta_pdf(request):
- cuenta_id = request.GET.get('cuenta_id')
- cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
- # 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))
- # Saldo antes del periodo
- movimientos_previos = Movimiento.objects.filter(
- fecha__lt=fecha_inicio_dt,
- ).filter(
- cuenta_origen=cuenta
- ).aggregate(total_origen=Sum('monto')) or {'total_origen': 0}
- entradas_previas = Movimiento.objects.filter(
- fecha__lt=fecha_inicio_dt,
- ).filter(
- cuenta_destino=cuenta
- ).aggregate(total_destino=Sum('monto')) or {'total_destino': 0}
- saldo_inicial = (entradas_previas['total_destino'] or 0) - (movimientos_previos['total_origen'] or 0)
- saldo_inicial = saldo_inicial + cuenta.saldo_inicial
- movimientos = Movimiento.objects.filter(
- Q(cuenta_origen=cuenta) | Q(cuenta_destino=cuenta),
- fecha__range=(fecha_inicio_dt, fecha_fin_dt)
- ).order_by('fecha')
- saldo = saldo_inicial
- historial = []
- for mov in movimientos:
- if mov.cuenta_origen == cuenta:
- saldo -= mov.monto
- tipo = 'Gasto'
- elif mov.cuenta_destino == cuenta:
- saldo += mov.monto
- tipo = 'Ingreso'
- else:
- tipo = 'Otro'
- historial.append({
- 'fecha': mov.fecha,
- 'desde': mov.cuenta_origen.nombre if mov.cuenta_origen else 'N/A',
- 'hasta': mov.cuenta_destino.nombre if mov.cuenta_destino else 'N/A',
- 'descripcion': mov.descripcion,
- 'monto': mov.monto,
- 'tipo': tipo,
- 'saldo_despues': saldo,
- })
- template = get_template('informe_cuenta_pdf.html')
- html = template.render({'cuenta': cuenta, 'saldo_inicial': saldo_inicial, 'fecha_inicio': fecha_inicio_str, 'fecha_fin': fecha_fin_str, 'historial': historial})
- with tempfile.NamedTemporaryFile(delete=True) as output:
- HTML(string=html).write_pdf(output.name)
- output.seek(0)
- data = output.read()
- response = HttpResponse(data, content_type='application/pdf')
- filename = f'informe_{cuenta.nombre}.pdf'
- response['Content-Disposition'] = f'filename={filename}'
- logger.info('api_informe_cuenta_pdf: PDF generado', extra={'cuenta_id': cuenta_id, 'filename': filename, 'size_bytes': len(data)})
- return response
- @csrf_exempt
- def api_informe_cuenta_pdf(request):
- """API endpoint para generar PDF del informe de una cuenta.
- Parámetros GET esperados:
- - cuenta_id: id de la cuenta
- - fecha_inicio: YYYY-MM-DD (opcional)
- - fecha_fin: YYYY-MM-DD (opcional)
- También acepta header `X-API-KEY` o parámetro `token` para autenticación.
- """
- token_enviado = request.headers.get('X-API-KEY') or request.GET.get('token')
- if token_enviado != settings.RECURRENTES_API_TOKEN:
- return JsonResponse({'error': 'No autorizado'}, status=403)
- cuenta_id = request.GET.get('cuenta_id')
- if not cuenta_id:
- return JsonResponse({'error': 'Falta parámetro cuenta_id'}, status=400)
- cuenta = get_object_or_404(Cuenta, id=cuenta_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_previos = Movimiento.objects.filter(
- fecha__lt=fecha_inicio_dt,
- ).filter(
- cuenta_origen=cuenta
- ).aggregate(total_origen=Sum('monto')) or {'total_origen': 0}
- entradas_previas = Movimiento.objects.filter(
- fecha__lt=fecha_inicio_dt,
- ).filter(
- cuenta_destino=cuenta
- ).aggregate(total_destino=Sum('monto')) or {'total_destino': 0}
- saldo_inicial = (entradas_previas['total_destino'] or 0) - (movimientos_previos['total_origen'] or 0)
- saldo_inicial = saldo_inicial + cuenta.saldo_inicial
- movimientos = Movimiento.objects.filter(
- Q(cuenta_origen=cuenta) | Q(cuenta_destino=cuenta),
- fecha__range=(fecha_inicio_dt, fecha_fin_dt)
- ).order_by('fecha')
- saldo = saldo_inicial
- historial = []
- for mov in movimientos:
- if mov.cuenta_origen == cuenta:
- saldo -= mov.monto
- tipo = 'Gasto'
- elif mov.cuenta_destino == cuenta:
- saldo += mov.monto
- tipo = 'Ingreso'
- else:
- tipo = 'Otro'
- historial.append({
- 'fecha': mov.fecha,
- 'desde': mov.cuenta_origen.nombre if mov.cuenta_origen else 'N/A',
- 'hasta': mov.cuenta_destino.nombre if mov.cuenta_destino else 'N/A',
- 'descripcion': mov.descripcion,
- 'monto': mov.monto,
- 'tipo': tipo,
- 'saldo_despues': saldo,
- })
- template = get_template('informe_cuenta_pdf.html')
- html = template.render({'cuenta': cuenta, 'saldo_inicial': saldo_inicial, 'fecha_inicio': fecha_inicio_str, 'fecha_fin': fecha_fin_str, '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_{cuenta.nombre}.pdf'
- return response
|