| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- 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 Sum
- from collections import defaultdict
- 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 .recurrentes import procesar_transacciones_recurrentes
- from .forms import FiltroCuentaForm
- 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')
- frecuentes = MovimientoFrecuente.objects.filter(usuario=request.user)
- 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,
- })
- # Movimientos
- @login_required
- def agregar_movimiento(request):
- if request.method == 'POST':
- form = MovimientoForm(request.POST)
- form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
- form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
- 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}")
- form = MovimientoForm(initial=datos_iniciales)
- form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
- form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
- 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)
- saldo_original_origen = movimiento.cuenta_origen
- saldo_original_destino = movimiento.cuenta_destino
- monto_original = movimiento.monto
- # logger.warning(f"editar_movimiento: fecha: {movimiento.fecha}")
- if request.method == 'POST':
- form = MovimientoForm(request.POST, instance=movimiento)
- form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
- form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
- if form.is_valid():
- movimiento_nuevo = form.save(commit=False)
- # Revertir efecto anterior
- saldo_original_origen.saldo += monto_original
- saldo_original_origen.save()
- saldo_original_destino.saldo -= monto_original
- saldo_original_destino.save()
- # Aplicar nuevo efecto
- movimiento_nuevo.usuario = request.user
- movimiento_nuevo.save()
- movimiento_nuevo.cuenta_origen.saldo -= movimiento_nuevo.monto
- movimiento_nuevo.cuenta_origen.save()
- movimiento_nuevo.cuenta_destino.saldo += movimiento_nuevo.monto
- movimiento_nuevo.cuenta_destino.save()
- return redirect('dashboard')
- else:
- form = MovimientoForm(instance=movimiento)
- form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
- form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
- 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,
- )
- if request.method == 'POST':
- form = MovimientoForm(request.POST, instance=movimiento_nuevo)
- form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
- form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
- 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)
- form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
- form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
- return render(request, 'duplicar_movimiento.html', {'form': form})
- # Cuentas
- @login_required
- def agregar_cuenta(request):
- if request.method == 'POST':
- form = CuentaForm(request.POST)
- if form.is_valid():
- cuenta = form.save(commit=False)
- cuenta.usuario = request.user
- cuenta.save()
- return redirect('dashboard')
- else:
- form = CuentaForm()
- return render(request, 'agregar_cuenta.html', {'form': form})
- @login_required
- def listar_cuentas(request):
- cuentas = Cuenta.objects.filter(usuario=request.user)
- form_filtro = FiltroForm(request.GET or None)
- if form_filtro.is_valid():
- busqueda = form_filtro.cleaned_data.get('buscar')
- if busqueda:
- cuentas = cuentas.filter(
- db_models.Q(nombre__icontains=busqueda)
- )
- return render(request, 'listar_cuentas.html',
- {'cuentas': cuentas, 'form_filtro': form_filtro})
- @login_required
- def editar_cuenta(request, cuenta_id):
- cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
- if request.method == 'POST':
- form = CuentaForm(request.POST, instance=cuenta)
- if form.is_valid():
- form.save()
- return redirect('listar_cuentas')
- else:
- form = CuentaForm(instance=cuenta)
- return render(request, 'editar_cuenta.html', {'form': form})
- @login_required
- def eliminar_cuenta(request, cuenta_id):
- cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
- tiene_movimientos = cuenta.movimientos_salida.exists() or cuenta.movimientos_entrada.exists()
- if request.method == 'POST' and not tiene_movimientos:
- cuenta.delete()
- return redirect('listar_cuentas')
- return render(request, 'eliminar_cuenta.html', {
- 'cuenta': cuenta,
- 'tiene_movimientos': tiene_movimientos,
- })
- @login_required
- def ver_cuenta(request, cuenta_id):
- cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
- movimientos = Movimiento.objects.filter(
- db_models.Q(cuenta_origen=cuenta) | db_models.Q(cuenta_destino=cuenta)
- )
- form = FiltroCuentaForm(request.GET or None)
- if form.is_valid():
- mes = int(form.cleaned_data['mes'] or date.today().month)
- anio = int(form.cleaned_data['anio'] or date.today().year)
- categoria = form.cleaned_data.get('categoria')
- movimientos = movimientos.filter(fecha__month=mes, fecha__year=anio)
- if categoria:
- movimientos = movimientos.filter(categoria=categoria)
- movimientos = movimientos.order_by('-fecha')
- return render(request, 'ver_cuenta.html', {
- 'cuenta': cuenta,
- 'movimientos': movimientos,
- 'form': form
- })
- # API
- def api_datos_completos(request):
- cuentas = list(Cuenta.objects.values('id', 'nombre', 'tipo', 'saldo', 'usuario__nombre', 'favorita'))
- 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,
- }
- 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')
- if token_enviado != settings.RECURRENTES_API_TOKEN:
- 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)
- 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: 0 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,
- })
|