views.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  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 Q
  15. from collections import defaultdict
  16. from django.template.loader import get_template
  17. from django.http import HttpResponse
  18. from weasyprint import HTML
  19. import tempfile
  20. from django.utils.dateparse import parse_date
  21. from django.db.models import Sum
  22. import logging
  23. import re
  24. from accounts import models
  25. from .models import Movimiento, Cuenta, MovimientoFrecuente, Categoria, TransaccionRecurrente
  26. from .forms import MovimientoForm, FiltroForm, CuentaForm, FiltroMesAnioForm, FiltroCuentaForm, InformeCuentaForm, FiltroInformeCuentaForm
  27. from .recurrentes import procesar_transacciones_recurrentes
  28. logger = logging.getLogger(__name__)
  29. @login_required
  30. def dashboard(request):
  31. # Copiamos GET para poder modificarlo
  32. request.GET = request.GET.copy()
  33. hoy = now()
  34. form_fecha = FiltroMesAnioForm(request.GET or None)
  35. mes = hoy.month
  36. anio = hoy.year
  37. if form_fecha.is_valid():
  38. mes = int(form_fecha.cleaned_data['mes'])
  39. anio = int(form_fecha.cleaned_data['anio'])
  40. # logger.warning(f"dashboard: mes: {mes} / año: {anio}")
  41. inicio_mes = timezone.make_aware(datetime(anio, mes, 1))
  42. ultimo_dia = monthrange(anio, mes)[1]
  43. fin_mes = timezone.make_aware(datetime(anio, mes, ultimo_dia, 23, 59, 59))
  44. movimientos = Movimiento.objects.filter(usuario=request.user, fecha__range=(inicio_mes, fin_mes)).order_by('-fecha')
  45. # logger.warning(f"dashboard: movimientos: {movimientos}")
  46. favoritas = Cuenta.objects.filter(favorita='True').order_by('nombre')
  47. frecuentes = MovimientoFrecuente.objects.filter(usuario=request.user).order_by('cuenta_destino')
  48. form_filtro = FiltroForm(request.GET or None)
  49. if form_filtro.is_valid():
  50. busqueda = form_filtro.cleaned_data.get('buscar')
  51. if busqueda:
  52. # logger.warning(f"dashboard: busqueda=true {busqueda}")
  53. movimientos = movimientos.filter(
  54. db_models.Q(descripcion__icontains=busqueda) | db_models.Q(cuenta_origen__nombre__icontains=busqueda) | db_models.Q(cuenta_destino__nombre__icontains=busqueda)
  55. )
  56. return render(request, 'dashboard.html', {
  57. 'movimientos': movimientos,
  58. 'user': request.user,
  59. 'form_filtro': form_filtro,
  60. 'form_fecha': form_fecha,
  61. 'favoritas': favoritas,
  62. 'frecuentes': frecuentes,
  63. 'mes': mes,
  64. 'anio': anio,
  65. })
  66. # Movimientos
  67. @login_required
  68. def agregar_movimiento(request):
  69. if request.method == 'POST':
  70. form = MovimientoForm(request.POST)
  71. cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
  72. form.fields['cuenta_origen'].queryset = cuentas
  73. form.fields['cuenta_destino'].queryset = cuentas
  74. if form.is_valid():
  75. movimiento = form.save(commit=False)
  76. movimiento.usuario = request.user
  77. movimiento.save()
  78. # Ajustar saldos de cuentas
  79. movimiento.cuenta_origen.saldo -= movimiento.monto
  80. movimiento.cuenta_origen.save()
  81. movimiento.cuenta_destino.saldo += movimiento.monto
  82. movimiento.cuenta_destino.save()
  83. messages.success(request, 'Movimiento registrado con éxito.')
  84. return redirect('dashboard')
  85. else:
  86. datos_iniciales = {}
  87. origen = request.GET.get('origen')
  88. destino = request.GET.get('destino')
  89. categoria = request.GET.get('categoria')
  90. descripcion = request.GET.get('descripcion')
  91. monto_raw = request.GET.get('monto')
  92. # logger.warning(f"agregar_movimiento: origen: {origen}")
  93. # logger.warning(f"agregar_movimiento: destino: {destino}")
  94. # logger.warning(f"agregar_movimiento: categoria: {categoria}")
  95. # logger.warning(f"agregar_movimiento: descripcion: {descripcion}")
  96. # logger.warning(f"agregar_movimiento: monto_raw: {monto_raw}")
  97. try:
  98. if origen:
  99. datos_iniciales['cuenta_origen'] = Cuenta.objects.get(nombre=request.GET['origen'], usuario=request.user)
  100. if destino:
  101. datos_iniciales['cuenta_destino'] = Cuenta.objects.get(nombre=request.GET['destino'], usuario=request.user)
  102. if categoria:
  103. datos_iniciales['categoria'] = Categoria.objects.get(nombre=request.GET['categoria'])
  104. if descripcion:
  105. datos_iniciales['descripcion'] = descripcion
  106. if monto_raw:
  107. # Reemplaza coma por punto, elimina símbolos y espacios
  108. monto_normalizado = re.sub(r'[^\d,.-]', '', monto_raw).replace(',', '.')
  109. try:
  110. datos_iniciales['monto'] = str(Decimal(monto_normalizado).quantize(Decimal('0.01')))
  111. except InvalidOperation:
  112. pass # Ignorar monto malformado
  113. logger.warning(f"monto: mal formado {datos_iniciales['monto']}")
  114. except (Cuenta.DoesNotExist, InvalidOperation):
  115. pass
  116. # logger.warning(f"agregar_movimiento: datos iniciales: {datos_iniciales}")
  117. cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
  118. form = MovimientoForm(initial=datos_iniciales)
  119. form.fields['cuenta_origen'].queryset = cuentas
  120. form.fields['cuenta_destino'].queryset = cuentas
  121. return render(request, 'agregar_movimiento.html', {'form': form})
  122. @login_required
  123. def editar_movimiento(request, movimiento_id):
  124. movimiento = get_object_or_404(Movimiento, id=movimiento_id, usuario=request.user)
  125. saldo_original_origen = movimiento.cuenta_origen
  126. saldo_original_destino = movimiento.cuenta_destino
  127. monto_original = movimiento.monto
  128. if request.method == 'POST':
  129. form = MovimientoForm(request.POST, instance=movimiento)
  130. cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
  131. form.fields['cuenta_origen'].queryset = cuentas
  132. form.fields['cuenta_destino'].queryset = cuentas
  133. if form.is_valid():
  134. movimiento_nuevo = form.save(commit=False)
  135. # Revertir efecto anterior
  136. # Cuenta origen
  137. saldo_original_origen.saldo += monto_original
  138. saldo_original_origen.save()
  139. # Cuenta destino
  140. saldo_original_destino.saldo -= monto_original
  141. saldo_original_destino.save()
  142. # Aplicar nuevo efecto
  143. movimiento_nuevo.usuario = request.user
  144. movimiento_nuevo.save()
  145. if movimiento_nuevo.cuenta_origen.nombre == saldo_original_origen.nombre: # si se trata de la misma cuenta...
  146. saldo_original_origen.saldo -= movimiento_nuevo.monto # ...tomo como valor del saldo el ya revertido
  147. saldo_original_origen.save()
  148. else: # ...de lo contrario
  149. movimiento_nuevo.cuenta_origen.saldo -= movimiento_nuevo.monto # ...tomar como valor del saldo el de la nueva cuenta.
  150. movimiento_nuevo.cuenta_origen.save()
  151. if movimiento_nuevo.cuenta_destino.nombre == saldo_original_destino.nombre: # si se trata de la misma cuenta...
  152. saldo_original_destino.saldo += movimiento_nuevo.monto # ...tomo como valor del saldo el ya revertido
  153. saldo_original_destino.save()
  154. else: # ...de lo contrario
  155. movimiento_nuevo.cuenta_destino.saldo += movimiento_nuevo.monto # ...tomar como valor del saldo el de la nueva cuenta.
  156. movimiento_nuevo.cuenta_destino.save()
  157. return redirect('dashboard')
  158. else:
  159. form = MovimientoForm(instance=movimiento)
  160. cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
  161. form.fields['cuenta_origen'].queryset = cuentas
  162. form.fields['cuenta_destino'].queryset = cuentas
  163. return render(request, 'editar_movimiento.html', {'form': form})
  164. @login_required
  165. def eliminar_movimiento(request, movimiento_id):
  166. movimiento = get_object_or_404(Movimiento, id=movimiento_id, usuario=request.user)
  167. if request.method == 'POST':
  168. # Revertir efecto del movimiento antes de borrarlo
  169. movimiento.cuenta_origen.saldo += movimiento.monto
  170. movimiento.cuenta_origen.save()
  171. movimiento.cuenta_destino.saldo -= movimiento.monto
  172. movimiento.cuenta_destino.save()
  173. movimiento.delete()
  174. return redirect('dashboard')
  175. return render(request, 'eliminar_movimiento.html', {'movimiento': movimiento})
  176. @login_required
  177. def duplicar_movimiento(request, movimiento_id):
  178. original = get_object_or_404(Movimiento, id=movimiento_id, usuario=request.user)
  179. # Crear una copia del movimiento sin guardar
  180. movimiento_nuevo = Movimiento(
  181. cuenta_origen=original.cuenta_origen,
  182. cuenta_destino=original.cuenta_destino,
  183. monto=original.monto,
  184. fecha=now(),
  185. descripcion=original.descripcion,
  186. usuario=request.user,
  187. categoria=original.categoria,
  188. )
  189. if request.method == 'POST':
  190. form = MovimientoForm(request.POST, instance=movimiento_nuevo)
  191. cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
  192. form.fields['cuenta_origen'].queryset = cuentas
  193. form.fields['cuenta_destino'].queryset = cuentas
  194. if form.is_valid():
  195. nuevo = form.save(commit=False)
  196. nuevo.usuario = request.user
  197. nuevo.save()
  198. # Ajustar saldos
  199. nuevo.cuenta_origen.saldo -= nuevo.monto
  200. nuevo.cuenta_origen.save()
  201. nuevo.cuenta_destino.saldo += nuevo.monto
  202. nuevo.cuenta_destino.save()
  203. messages.success(request, 'Movimiento duplicado con éxito.')
  204. return redirect('dashboard')
  205. else:
  206. form = MovimientoForm(instance=movimiento_nuevo)
  207. cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
  208. form.fields['cuenta_origen'].queryset = cuentas
  209. form.fields['cuenta_destino'].queryset = cuentas
  210. return render(request, 'duplicar_movimiento.html', {'form': form})
  211. # Cuentas
  212. @login_required
  213. def agregar_cuenta(request):
  214. if request.method == 'POST':
  215. form = CuentaForm(request.POST)
  216. if form.is_valid():
  217. cuenta = form.save(commit=False)
  218. cuenta.usuario = request.user
  219. cuenta.save()
  220. return redirect('dashboard')
  221. else:
  222. form = CuentaForm()
  223. return render(request, 'agregar_cuenta.html', {'form': form})
  224. @login_required
  225. def listar_cuentas(request):
  226. cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
  227. form_filtro = FiltroForm(request.GET or None)
  228. fecha_inicio = timezone.localtime()
  229. fecha_fin = fecha_inicio.replace(day=1)
  230. if form_filtro.is_valid():
  231. busqueda = form_filtro.cleaned_data.get('buscar')
  232. if busqueda:
  233. cuentas = cuentas.filter(
  234. db_models.Q(nombre__icontains=busqueda)
  235. )
  236. return render(request, 'listar_cuentas.html',
  237. {'cuentas': cuentas,
  238. 'form_filtro': form_filtro,
  239. 'fecha_inicio:': fecha_inicio,
  240. 'fecha_fin': fecha_fin})
  241. @login_required
  242. def editar_cuenta(request, cuenta_id):
  243. cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
  244. if request.method == 'POST':
  245. form = CuentaForm(request.POST, instance=cuenta)
  246. if form.is_valid():
  247. form.save()
  248. return redirect('listar_cuentas')
  249. else:
  250. form = CuentaForm(instance=cuenta)
  251. return render(request, 'editar_cuenta.html', {'form': form})
  252. @login_required
  253. def eliminar_cuenta(request, cuenta_id):
  254. cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
  255. tiene_movimientos = cuenta.movimientos_salida.exists() or cuenta.movimientos_entrada.exists()
  256. if request.method == 'POST' and not tiene_movimientos:
  257. cuenta.delete()
  258. return redirect('listar_cuentas')
  259. return render(request, 'eliminar_cuenta.html', {
  260. 'cuenta': cuenta,
  261. 'tiene_movimientos': tiene_movimientos,
  262. })
  263. @login_required
  264. def ver_cuenta(request, cuenta_id):
  265. hoy = date.today()
  266. logger.warning(f"ver_cuenta: mes: {hoy.month} de {hoy.year}")
  267. cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
  268. movimientos = Movimiento.objects.filter(
  269. db_models.Q(cuenta_origen=cuenta) | db_models.Q(cuenta_destino=cuenta)
  270. )
  271. movimientos = movimientos.filter(fecha__year=hoy.year, fecha__month=hoy.month)
  272. form = FiltroCuentaForm(request.GET or None)
  273. if form.is_valid():
  274. mes = int(form.cleaned_data['mes'] or date.today().month)
  275. anio = int(form.cleaned_data['anio'] or date.today().year)
  276. categoria = form.cleaned_data.get('categoria')
  277. movimientos = movimientos.filter(fecha__month=mes, fecha__year=anio)
  278. if categoria:
  279. movimientos = movimientos.filter(categoria=categoria)
  280. movimientos = movimientos.order_by('-fecha')
  281. return render(request, 'ver_cuenta.html', {
  282. 'cuenta': cuenta,
  283. 'movimientos': movimientos,
  284. 'form': form
  285. })
  286. # API
  287. def api_datos_completos(request):
  288. cuentas = list(Cuenta.objects.values('id', 'nombre', 'tipo', 'saldo', 'usuario__nombre', 'favorita', 'saldo_inicial'))
  289. categorias = list(Categoria.objects.values('id', 'nombre'))
  290. movimientos = [
  291. {
  292. 'id': m.id,
  293. 'usuario': m.usuario.nombre,
  294. 'cuenta_origen': m.cuenta_origen.nombre,
  295. 'cuenta_destino': m.cuenta_destino.nombre,
  296. 'monto': float(m.monto),
  297. 'fecha': m.fecha.isoformat(),
  298. 'descripcion': m.descripcion,
  299. 'categoria': m.categoria.nombre if m.categoria else None,
  300. }
  301. for m in Movimiento.objects.select_related('cuenta_origen', 'cuenta_destino', 'usuario', 'categoria')
  302. ]
  303. movs_frecuentes = [
  304. {
  305. 'id': m.id,
  306. 'nombre': m.nombre,
  307. 'cuenta_origen': m.cuenta_origen.nombre,
  308. 'cuenta_destino': m.cuenta_destino.nombre,
  309. 'monto': float(m.monto),
  310. 'categoria': m.categoria.nombre if m.categoria else None,
  311. 'descripcion': m.descripcion,
  312. 'usuario': m.usuario.nombre,
  313. }
  314. for m in MovimientoFrecuente.objects.select_related('cuenta_origen', 'cuenta_destino', 'usuario', 'categoria')
  315. ]
  316. movs_recurrentes = [
  317. {
  318. 'id': m.id,
  319. 'usuario': m.usuario.nombre,
  320. 'cuenta_origen': m.cuenta_origen.nombre,
  321. 'cuenta_destino': m.cuenta_destino.nombre,
  322. 'monto': float(m.monto),
  323. 'categoria': m.categoria.nombre if m.categoria else None,
  324. 'descripcion': m.descripcion,
  325. 'frecuencia': m.frecuencia,
  326. }
  327. for m in TransaccionRecurrente.objects.select_related('cuenta_origen', 'cuenta_destino', 'usuario', 'categoria')
  328. ]
  329. return JsonResponse({
  330. 'cuentas': cuentas,
  331. 'categorias': categorias,
  332. 'movimientos': movimientos,
  333. 'movimientos_frecuentes': movs_frecuentes,
  334. 'transacciones_recurrentes': movs_recurrentes,
  335. }, safe=False)
  336. @login_required
  337. def ejecutar_recurrentes(request):
  338. procesar_transacciones_recurrentes()
  339. return redirect('dashboard')
  340. @csrf_exempt
  341. def api_ejecutar_recurrentes(request):
  342. token_enviado = request.headers.get('X-API-KEY') or request.GET.get('token')
  343. if token_enviado != settings.RECURRENTES_API_TOKEN:
  344. return JsonResponse({'error': 'No autorizado'}, status=403)
  345. procesar_transacciones_recurrentes(hoy=now().date())
  346. return JsonResponse({'ok': True, 'mensaje': 'Transacciones recurrentes ejecutadas'})
  347. @login_required
  348. def resumen_saldos_mensuales(request):
  349. cuentas = Cuenta.objects.filter(usuario=request.user).order_by('nombre')
  350. movimientos = Movimiento.objects.filter(usuario=request.user).order_by('fecha')
  351. # Diccionario: {(cuenta_id, año, mes): saldo}
  352. saldos_mensuales = defaultdict(lambda: 0)
  353. # Diccionario para llevar el saldo acumulado
  354. saldos_acumulados = {cuenta.id: float(cuenta.saldo_inicial) for cuenta in cuentas}
  355. # Obtener todos los meses presentes en movimientos
  356. meses_presentes = set()
  357. for mov in movimientos:
  358. anio = mov.fecha.year
  359. mes = mov.fecha.month
  360. clave = (mov.cuenta_origen.id, anio, mes)
  361. meses_presentes.add((anio, mes))
  362. # Resta al origen
  363. saldos_acumulados[mov.cuenta_origen.id] -= float(mov.monto)
  364. saldos_mensuales[clave] = saldos_acumulados[mov.cuenta_origen.id]
  365. # Suma al destino
  366. clave_dest = (mov.cuenta_destino.id, anio, mes)
  367. saldos_acumulados[mov.cuenta_destino.id] += float(mov.monto)
  368. saldos_mensuales[clave_dest] = saldos_acumulados[mov.cuenta_destino.id]
  369. # Ordenar meses
  370. meses_ordenados = sorted(meses_presentes)
  371. return render(request, 'resumen_saldos_mensuales.html', {
  372. 'cuentas': cuentas,
  373. 'meses': meses_ordenados,
  374. 'saldos_mensuales': saldos_mensuales,
  375. })
  376. # Informes
  377. @login_required
  378. def informe_cuenta(request):
  379. cuenta_id = request.GET.get('cuenta_id')
  380. cuenta = get_object_or_404(Cuenta, id=cuenta_id)
  381. form = FiltroInformeCuentaForm(request.GET or None)
  382. if form.is_valid():
  383. # obtener fechas desde GET o establecer por defecto (mes actual)
  384. hoy = timezone.localtime()
  385. primer_dia = hoy.replace(day=1)
  386. fecha_inicio_str = request.GET.get('fecha_inicio')
  387. fecha_fin_str = request.GET.get('fecha_fin')
  388. if fecha_inicio_str and fecha_fin_str:
  389. fecha_inicio = datetime.strptime(fecha_inicio_str, '%Y-%m-%d').date()
  390. fecha_fin = datetime.strptime(fecha_fin_str, '%Y-%m-%d').date()
  391. else:
  392. fecha_inicio = primer_dia
  393. fecha_fin = hoy
  394. # Convertir a datetime aware para evitar warnings
  395. fecha_inicio_dt = timezone.make_aware(datetime.combine(fecha_inicio, time.min))
  396. fecha_fin_dt = timezone.make_aware(datetime.combine(fecha_fin, time.max))
  397. # Saldo antes del periodo
  398. movimientos_previos = Movimiento.objects.filter(
  399. fecha__lt=fecha_inicio_dt,
  400. ).filter(
  401. cuenta_origen=cuenta
  402. ).aggregate(total_origen=Sum('monto')) or {'total_origen': 0}
  403. entradas_previas = Movimiento.objects.filter(
  404. fecha__lt=fecha_inicio_dt,
  405. ).filter(
  406. cuenta_destino=cuenta
  407. ).aggregate(total_destino=Sum('monto')) or {'total_destino': 0}
  408. saldo_inicial = (entradas_previas['total_destino'] or 0) - (movimientos_previos['total_origen'] or 0)
  409. saldo_inicial = saldo_inicial + cuenta.saldo_inicial
  410. movimientos = Movimiento.objects.filter(
  411. Q(cuenta_origen=cuenta) | Q(cuenta_destino=cuenta),
  412. fecha__range=(fecha_inicio_dt, fecha_fin_dt)
  413. ).order_by('fecha')
  414. # Cálculo de saldos después de cada movimiento
  415. saldo = saldo_inicial
  416. movimientos_con_saldo = []
  417. for mov in movimientos:
  418. if mov.cuenta_origen == cuenta:
  419. saldo -= mov.monto
  420. tipo = 'Gasto'
  421. elif mov.cuenta_destino == cuenta:
  422. saldo += mov.monto
  423. tipo = 'Ingreso'
  424. movimientos_con_saldo.append({
  425. 'fecha': mov.fecha,
  426. 'descripcion': mov.descripcion,
  427. 'monto': mov.monto,
  428. 'tipo': tipo,
  429. 'desde': mov.cuenta_origen.nombre if mov.cuenta_origen else 'N/A',
  430. 'hasta': mov.cuenta_destino.nombre if mov.cuenta_destino else 'N/A',
  431. 'saldo_despues': saldo,
  432. })
  433. # Renderizado del PDF o del HTML como prefieras
  434. context = {
  435. 'cuenta': cuenta,
  436. 'movimientos_con_saldo': movimientos_con_saldo,
  437. 'fecha_inicio': fecha_inicio_str,
  438. 'fecha_fin': fecha_fin_str,
  439. 'saldo_inicial': saldo_inicial,
  440. }
  441. return render(request, 'informe_cuenta.html', context)
  442. @login_required
  443. def informe_cuenta_pdf(request):
  444. cuenta_id = request.GET.get('cuenta_id')
  445. cuenta = get_object_or_404(Cuenta, id=cuenta_id, usuario=request.user)
  446. # obtener fechas desde GET o establecer por defecto (mes actual)
  447. hoy = timezone.localtime()
  448. primer_dia = hoy.replace(day=1)
  449. fecha_inicio_str = request.GET.get('fecha_inicio')
  450. fecha_fin_str = request.GET.get('fecha_fin')
  451. if fecha_inicio_str and fecha_fin_str:
  452. fecha_inicio = datetime.strptime(fecha_inicio_str, '%Y-%m-%d').date()
  453. fecha_fin = datetime.strptime(fecha_fin_str, '%Y-%m-%d').date()
  454. else:
  455. fecha_inicio = primer_dia
  456. fecha_fin = hoy
  457. # Convertir a datetime aware para evitar warnings
  458. fecha_inicio_dt = timezone.make_aware(datetime.combine(fecha_inicio, time.min))
  459. fecha_fin_dt = timezone.make_aware(datetime.combine(fecha_fin, time.max))
  460. # Saldo antes del periodo
  461. movimientos_previos = Movimiento.objects.filter(
  462. fecha__lt=fecha_inicio_dt,
  463. ).filter(
  464. cuenta_origen=cuenta
  465. ).aggregate(total_origen=Sum('monto')) or {'total_origen': 0}
  466. entradas_previas = Movimiento.objects.filter(
  467. fecha__lt=fecha_inicio_dt,
  468. ).filter(
  469. cuenta_destino=cuenta
  470. ).aggregate(total_destino=Sum('monto')) or {'total_destino': 0}
  471. saldo_inicial = (entradas_previas['total_destino'] or 0) - (movimientos_previos['total_origen'] or 0)
  472. saldo_inicial = saldo_inicial + cuenta.saldo_inicial
  473. movimientos = Movimiento.objects.filter(
  474. Q(cuenta_origen=cuenta) | Q(cuenta_destino=cuenta),
  475. fecha__range=(fecha_inicio_dt, fecha_fin_dt)
  476. ).order_by('fecha')
  477. saldo = saldo_inicial
  478. historial = []
  479. for mov in movimientos:
  480. if mov.cuenta_origen == cuenta:
  481. saldo -= mov.monto
  482. tipo = 'Gasto'
  483. elif mov.cuenta_destino == cuenta:
  484. saldo += mov.monto
  485. tipo = 'Ingreso'
  486. else:
  487. tipo = 'Otro'
  488. historial.append({
  489. 'fecha': mov.fecha,
  490. 'desde': mov.cuenta_origen.nombre if mov.cuenta_origen else 'N/A',
  491. 'hasta': mov.cuenta_destino.nombre if mov.cuenta_destino else 'N/A',
  492. 'descripcion': mov.descripcion,
  493. 'monto': mov.monto,
  494. 'tipo': tipo,
  495. 'saldo_despues': saldo,
  496. })
  497. template = get_template('informe_cuenta_pdf.html')
  498. html = template.render({'cuenta': cuenta, 'saldo_inicial': saldo_inicial, 'fecha_inicio': fecha_inicio_str, 'fecha_fin': fecha_fin_str, 'historial': historial})
  499. with tempfile.NamedTemporaryFile(delete=True) as output:
  500. HTML(string=html).write_pdf(output.name)
  501. output.seek(0)
  502. response = HttpResponse(output.read(), content_type='application/pdf')
  503. response['Content-Disposition'] = f'filename=informe_{cuenta.nombre}.pdf'
  504. return response