views.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. from django.shortcuts import render, redirect, get_object_or_404
  2. from django.http import JsonResponse
  3. from django.views.decorators.csrf import csrf_exempt
  4. from decimal import Decimal, InvalidOperation
  5. from django.db import models as db_models
  6. from django.contrib.auth.decorators import login_required
  7. from django.contrib import messages
  8. from django.core.serializers import serialize
  9. from datetime import date, timedelta, datetime, time
  10. from django.conf import settings
  11. from django.utils.timezone import localtime, now, make_aware, is_naive
  12. from calendar import monthrange
  13. from django.utils import timezone
  14. from django.db.models import Sum
  15. from collections import defaultdict
  16. import logging
  17. import re
  18. from accounts import models
  19. from .models import Movimiento, Cuenta, MovimientoFrecuente, Categoria, TransaccionRecurrente
  20. from .forms import MovimientoForm, FiltroForm, CuentaForm, FiltroMesAnioForm, FiltroCuentaForm
  21. from .recurrentes import procesar_transacciones_recurrentes
  22. from .forms import FiltroCuentaForm
  23. logger = logging.getLogger(__name__)
  24. @login_required
  25. def dashboard(request):
  26. # Copiamos GET para poder modificarlo
  27. request.GET = request.GET.copy()
  28. hoy = now()
  29. form_fecha = FiltroMesAnioForm(request.GET or None)
  30. mes = hoy.month
  31. anio = hoy.year
  32. if form_fecha.is_valid():
  33. mes = int(form_fecha.cleaned_data['mes'])
  34. anio = int(form_fecha.cleaned_data['anio'])
  35. # logger.warning(f"dashboard: mes: {mes} / año: {anio}")
  36. inicio_mes = timezone.make_aware(datetime(anio, mes, 1))
  37. ultimo_dia = monthrange(anio, mes)[1]
  38. fin_mes = timezone.make_aware(datetime(anio, mes, ultimo_dia, 23, 59, 59))
  39. movimientos = Movimiento.objects.filter(usuario=request.user, fecha__range=(inicio_mes, fin_mes)).order_by('-fecha')
  40. # logger.warning(f"dashboard: movimientos: {movimientos}")
  41. favoritas = Cuenta.objects.filter(favorita='True')
  42. frecuentes = MovimientoFrecuente.objects.filter(usuario=request.user)
  43. form_filtro = FiltroForm(request.GET or None)
  44. if form_filtro.is_valid():
  45. busqueda = form_filtro.cleaned_data.get('buscar')
  46. if busqueda:
  47. # logger.warning(f"dashboard: busqueda=true {busqueda}")
  48. movimientos = movimientos.filter(
  49. db_models.Q(descripcion__icontains=busqueda) | db_models.Q(cuenta_origen__nombre__icontains=busqueda) | db_models.Q(cuenta_destino__nombre__icontains=busqueda)
  50. )
  51. return render(request, 'dashboard.html', {
  52. 'movimientos': movimientos,
  53. 'user': request.user,
  54. 'form_filtro': form_filtro,
  55. 'form_fecha': form_fecha,
  56. 'favoritas': favoritas,
  57. 'frecuentes': frecuentes,
  58. 'mes': mes,
  59. 'anio': anio,
  60. })
  61. # Movimientos
  62. @login_required
  63. def agregar_movimiento(request):
  64. if request.method == 'POST':
  65. form = MovimientoForm(request.POST)
  66. form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
  67. form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
  68. if form.is_valid():
  69. movimiento = form.save(commit=False)
  70. movimiento.usuario = request.user
  71. movimiento.save()
  72. # Ajustar saldos de cuentas
  73. movimiento.cuenta_origen.saldo -= movimiento.monto
  74. movimiento.cuenta_origen.save()
  75. movimiento.cuenta_destino.saldo += movimiento.monto
  76. movimiento.cuenta_destino.save()
  77. messages.success(request, 'Movimiento registrado con éxito.')
  78. return redirect('dashboard')
  79. else:
  80. datos_iniciales = {}
  81. origen = request.GET.get('origen')
  82. destino = request.GET.get('destino')
  83. categoria = request.GET.get('categoria')
  84. descripcion = request.GET.get('descripcion')
  85. monto_raw = request.GET.get('monto')
  86. # logger.warning(f"agregar_movimiento: origen: {origen}")
  87. # logger.warning(f"agregar_movimiento: destino: {destino}")
  88. # logger.warning(f"agregar_movimiento: categoria: {categoria}")
  89. # logger.warning(f"agregar_movimiento: descripcion: {descripcion}")
  90. # logger.warning(f"agregar_movimiento: monto_raw: {monto_raw}")
  91. try:
  92. if origen:
  93. datos_iniciales['cuenta_origen'] = Cuenta.objects.get(nombre=request.GET['origen'], usuario=request.user)
  94. if destino:
  95. datos_iniciales['cuenta_destino'] = Cuenta.objects.get(nombre=request.GET['destino'], usuario=request.user)
  96. if categoria:
  97. datos_iniciales['categoria'] = Categoria.objects.get(nombre=request.GET['categoria'])
  98. if descripcion:
  99. datos_iniciales['descripcion'] = descripcion
  100. if monto_raw:
  101. # Reemplaza coma por punto, elimina símbolos y espacios
  102. monto_normalizado = re.sub(r'[^\d,.-]', '', monto_raw).replace(',', '.')
  103. try:
  104. datos_iniciales['monto'] = str(Decimal(monto_normalizado).quantize(Decimal('0.01')))
  105. except InvalidOperation:
  106. pass # Ignorar monto malformado
  107. logger.warning(f"monto: mal formado {datos_iniciales['monto']}")
  108. except (Cuenta.DoesNotExist, InvalidOperation):
  109. pass
  110. # logger.warning(f"agregar_movimiento: datos iniciales: {datos_iniciales}")
  111. form = MovimientoForm(initial=datos_iniciales)
  112. form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
  113. form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
  114. return render(request, 'agregar_movimiento.html', {'form': form})
  115. @login_required
  116. def editar_movimiento(request, movimiento_id):
  117. movimiento = get_object_or_404(Movimiento, id=movimiento_id, usuario=request.user)
  118. saldo_original_origen = movimiento.cuenta_origen
  119. saldo_original_destino = movimiento.cuenta_destino
  120. monto_original = movimiento.monto
  121. # logger.warning(f"editar_movimiento: fecha: {movimiento.fecha}")
  122. if request.method == 'POST':
  123. form = MovimientoForm(request.POST, instance=movimiento)
  124. form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
  125. form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
  126. if form.is_valid():
  127. movimiento_nuevo = form.save(commit=False)
  128. # Revertir efecto anterior
  129. saldo_original_origen.saldo += monto_original
  130. saldo_original_origen.save()
  131. saldo_original_destino.saldo -= monto_original
  132. saldo_original_destino.save()
  133. # Aplicar nuevo efecto
  134. movimiento_nuevo.usuario = request.user
  135. movimiento_nuevo.save()
  136. movimiento_nuevo.cuenta_origen.saldo -= movimiento_nuevo.monto
  137. movimiento_nuevo.cuenta_origen.save()
  138. movimiento_nuevo.cuenta_destino.saldo += movimiento_nuevo.monto
  139. movimiento_nuevo.cuenta_destino.save()
  140. return redirect('dashboard')
  141. else:
  142. form = MovimientoForm(instance=movimiento)
  143. form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
  144. form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
  145. return render(request, 'editar_movimiento.html', {'form': form})
  146. @login_required
  147. def eliminar_movimiento(request, movimiento_id):
  148. movimiento = get_object_or_404(Movimiento, id=movimiento_id, usuario=request.user)
  149. if request.method == 'POST':
  150. # Revertir efecto del movimiento antes de borrarlo
  151. movimiento.cuenta_origen.saldo += movimiento.monto
  152. movimiento.cuenta_origen.save()
  153. movimiento.cuenta_destino.saldo -= movimiento.monto
  154. movimiento.cuenta_destino.save()
  155. movimiento.delete()
  156. return redirect('dashboard')
  157. return render(request, 'eliminar_movimiento.html', {'movimiento': movimiento})
  158. @login_required
  159. def duplicar_movimiento(request, movimiento_id):
  160. original = get_object_or_404(Movimiento, id=movimiento_id, usuario=request.user)
  161. # Crear una copia del movimiento sin guardar
  162. movimiento_nuevo = Movimiento(
  163. cuenta_origen=original.cuenta_origen,
  164. cuenta_destino=original.cuenta_destino,
  165. monto=original.monto,
  166. fecha=now(),
  167. descripcion=original.descripcion,
  168. usuario=request.user,
  169. )
  170. if request.method == 'POST':
  171. form = MovimientoForm(request.POST, instance=movimiento_nuevo)
  172. form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
  173. form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
  174. if form.is_valid():
  175. nuevo = form.save(commit=False)
  176. nuevo.usuario = request.user
  177. nuevo.save()
  178. # Ajustar saldos
  179. nuevo.cuenta_origen.saldo -= nuevo.monto
  180. nuevo.cuenta_origen.save()
  181. nuevo.cuenta_destino.saldo += nuevo.monto
  182. nuevo.cuenta_destino.save()
  183. messages.success(request, 'Movimiento duplicado con éxito.')
  184. return redirect('dashboard')
  185. else:
  186. form = MovimientoForm(instance=movimiento_nuevo)
  187. form.fields['cuenta_origen'].queryset = Cuenta.objects.filter(usuario=request.user)
  188. form.fields['cuenta_destino'].queryset = Cuenta.objects.filter(usuario=request.user)
  189. return render(request, 'duplicar_movimiento.html', {'form': form})
  190. # Cuentas
  191. @login_required
  192. def agregar_cuenta(request):
  193. if request.method == 'POST':
  194. form = CuentaForm(request.POST)
  195. if form.is_valid():
  196. cuenta = form.save(commit=False)
  197. cuenta.usuario = request.user
  198. cuenta.save()
  199. return redirect('dashboard')
  200. else:
  201. form = CuentaForm()
  202. return render(request, 'agregar_cuenta.html', {'form': form})
  203. @login_required
  204. def listar_cuentas(request):
  205. cuentas = Cuenta.objects.filter(usuario=request.user)
  206. form_filtro = FiltroForm(request.GET or None)
  207. if form_filtro.is_valid():
  208. busqueda = form_filtro.cleaned_data.get('buscar')
  209. if busqueda:
  210. cuentas = cuentas.filter(
  211. db_models.Q(nombre__icontains=busqueda)
  212. )
  213. return render(request, 'listar_cuentas.html',
  214. {'cuentas': cuentas, 'form_filtro': form_filtro})
  215. @login_required
  216. def editar_cuenta(request, cuenta_id):
  217. cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
  218. if request.method == 'POST':
  219. form = CuentaForm(request.POST, instance=cuenta)
  220. if form.is_valid():
  221. form.save()
  222. return redirect('listar_cuentas')
  223. else:
  224. form = CuentaForm(instance=cuenta)
  225. return render(request, 'editar_cuenta.html', {'form': form})
  226. @login_required
  227. def eliminar_cuenta(request, cuenta_id):
  228. cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
  229. tiene_movimientos = cuenta.movimientos_salida.exists() or cuenta.movimientos_entrada.exists()
  230. if request.method == 'POST' and not tiene_movimientos:
  231. cuenta.delete()
  232. return redirect('listar_cuentas')
  233. return render(request, 'eliminar_cuenta.html', {
  234. 'cuenta': cuenta,
  235. 'tiene_movimientos': tiene_movimientos,
  236. })
  237. @login_required
  238. def ver_cuenta(request, cuenta_id):
  239. cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
  240. movimientos = Movimiento.objects.filter(
  241. db_models.Q(cuenta_origen=cuenta) | db_models.Q(cuenta_destino=cuenta)
  242. )
  243. form = FiltroCuentaForm(request.GET or None)
  244. if form.is_valid():
  245. mes = int(form.cleaned_data['mes'] or date.today().month)
  246. anio = int(form.cleaned_data['anio'] or date.today().year)
  247. categoria = form.cleaned_data.get('categoria')
  248. movimientos = movimientos.filter(fecha__month=mes, fecha__year=anio)
  249. if categoria:
  250. movimientos = movimientos.filter(categoria=categoria)
  251. movimientos = movimientos.order_by('-fecha')
  252. return render(request, 'ver_cuenta.html', {
  253. 'cuenta': cuenta,
  254. 'movimientos': movimientos,
  255. 'form': form
  256. })
  257. # API
  258. def api_datos_completos(request):
  259. cuentas = list(Cuenta.objects.values('id', 'nombre', 'tipo', 'saldo', 'usuario__nombre', 'favorita'))
  260. categorias = list(Categoria.objects.values('id', 'nombre'))
  261. movimientos = [
  262. {
  263. 'id': m.id,
  264. 'usuario': m.usuario.nombre,
  265. 'cuenta_origen': m.cuenta_origen.nombre,
  266. 'cuenta_destino': m.cuenta_destino.nombre,
  267. 'monto': float(m.monto),
  268. 'fecha': m.fecha.isoformat(),
  269. 'descripcion': m.descripcion,
  270. 'categoria': m.categoria.nombre if m.categoria else None,
  271. }
  272. for m in Movimiento.objects.select_related('cuenta_origen', 'cuenta_destino', 'usuario', 'categoria')
  273. ]
  274. movs_frecuentes = [
  275. {
  276. 'id': m.id,
  277. 'nombre': m.nombre,
  278. 'cuenta_origen': m.cuenta_origen.nombre,
  279. 'cuenta_destino': m.cuenta_destino.nombre,
  280. 'monto': float(m.monto),
  281. 'categoria': m.categoria.nombre if m.categoria else None,
  282. 'descripcion': m.descripcion,
  283. 'usuario': m.usuario.nombre,
  284. }
  285. for m in MovimientoFrecuente.objects.select_related('cuenta_origen', 'cuenta_destino', 'usuario', 'categoria')
  286. ]
  287. movs_recurrentes = [
  288. {
  289. 'id': m.id,
  290. 'usuario': m.usuario.nombre,
  291. 'cuenta_origen': m.cuenta_origen.nombre,
  292. 'cuenta_destino': m.cuenta_destino.nombre,
  293. 'monto': float(m.monto),
  294. 'categoria': m.categoria.nombre if m.categoria else None,
  295. 'descripcion': m.descripcion,
  296. 'frecuencia': m.frecuencia,
  297. }
  298. for m in TransaccionRecurrente.objects.select_related('cuenta_origen', 'cuenta_destino', 'usuario', 'categoria')
  299. ]
  300. return JsonResponse({
  301. 'cuentas': cuentas,
  302. 'categorias': categorias,
  303. 'movimientos': movimientos,
  304. 'movimientos_frecuentes': movs_frecuentes,
  305. 'transacciones_recurrentes': movs_recurrentes,
  306. }, safe=False)
  307. @login_required
  308. def ejecutar_recurrentes(request):
  309. procesar_transacciones_recurrentes()
  310. return redirect('dashboard')
  311. @csrf_exempt
  312. def api_ejecutar_recurrentes(request):
  313. token_enviado = request.headers.get('X-API-KEY') or request.GET.get('token')
  314. if token_enviado != settings.RECURRENTES_API_TOKEN:
  315. return JsonResponse({'error': 'No autorizado'}, status=403)
  316. procesar_transacciones_recurrentes(hoy=now().date())
  317. return JsonResponse({'ok': True, 'mensaje': 'Transacciones recurrentes ejecutadas'})
  318. @login_required
  319. def resumen_saldos_mensuales(request):
  320. cuentas = Cuenta.objects.filter(usuario=request.user)
  321. movimientos = Movimiento.objects.filter(usuario=request.user).order_by('fecha')
  322. # Diccionario: {(cuenta_id, año, mes): saldo}
  323. saldos_mensuales = defaultdict(lambda: 0)
  324. # Diccionario para llevar el saldo acumulado
  325. saldos_acumulados = {cuenta.id: 0 for cuenta in cuentas}
  326. # Obtener todos los meses presentes en movimientos
  327. meses_presentes = set()
  328. for mov in movimientos:
  329. anio = mov.fecha.year
  330. mes = mov.fecha.month
  331. clave = (mov.cuenta_origen.id, anio, mes)
  332. meses_presentes.add((anio, mes))
  333. # Resta al origen
  334. saldos_acumulados[mov.cuenta_origen.id] -= float(mov.monto)
  335. saldos_mensuales[clave] = saldos_acumulados[mov.cuenta_origen.id]
  336. # Suma al destino
  337. clave_dest = (mov.cuenta_destino.id, anio, mes)
  338. saldos_acumulados[mov.cuenta_destino.id] += float(mov.monto)
  339. saldos_mensuales[clave_dest] = saldos_acumulados[mov.cuenta_destino.id]
  340. # Ordenar meses
  341. meses_ordenados = sorted(meses_presentes)
  342. return render(request, 'resumen_saldos_mensuales.html', {
  343. 'cuentas': cuentas,
  344. 'meses': meses_ordenados,
  345. 'saldos_mensuales': saldos_mensuales,
  346. })