views.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. from django.contrib import messages
  2. from django.db.models.deletion import ProtectedError
  3. from django.shortcuts import redirect, render
  4. from django.urls import reverse_lazy
  5. from django.views.generic import CreateView, DeleteView, ListView, UpdateView
  6. from .forms import DividendoForm, DivisaForm, OperacionForm, ValorForm
  7. from .models import Dividendo, Divisa, Operacion, Valor
  8. from .services import (
  9. actualizar_cambios,
  10. actualizar_precios,
  11. desglose_fifo,
  12. posiciones,
  13. resumen_fiscal,
  14. totales,
  15. )
  16. class CancelarUrlMixin:
  17. """Aporta la URL de cancelación al formulario (vuelve atrás si hay referer)."""
  18. def get_context_data(self, **kwargs):
  19. ctx = super().get_context_data(**kwargs)
  20. ctx['cancelar_url'] = self.request.META.get('HTTP_REFERER') or str(self.success_url)
  21. return ctx
  22. def dashboard(request):
  23. pos = posiciones()
  24. context = {
  25. 'posiciones': pos,
  26. 'abiertas': [p for p in pos if p['acciones'] > 0],
  27. 'cerradas': [p for p in pos if p['acciones'] == 0],
  28. 'totales': totales(pos),
  29. }
  30. return render(request, 'cartera/dashboard.html', context)
  31. def actualizar(request):
  32. if request.method == 'POST':
  33. cambios, errores_cambios = actualizar_cambios()
  34. if cambios:
  35. messages.success(request, f'Cambios de moneda actualizados: {cambios}')
  36. for par, motivo in errores_cambios:
  37. messages.warning(request, f'{par}: {motivo}')
  38. correctos, errores = actualizar_precios()
  39. if correctos:
  40. messages.success(request, f'Precios actualizados: {correctos}')
  41. for ticker, motivo in errores:
  42. messages.warning(request, f'{ticker}: {motivo}')
  43. return redirect('cartera:dashboard')
  44. def fifo(request):
  45. filas = desglose_fifo()
  46. context = {
  47. 'filas': filas,
  48. 'fiscal': resumen_fiscal(filas),
  49. 'total': sum((f['resultado_eur'] or 0 for f in filas), start=0),
  50. }
  51. return render(request, 'cartera/fifo.html', context)
  52. class OperacionListView(ListView):
  53. model = Operacion
  54. template_name = 'cartera/operacion_list.html'
  55. context_object_name = 'operaciones'
  56. def get_queryset(self):
  57. qs = Operacion.objects.select_related('valor', 'valor__divisa').order_by('-fecha', '-pk')
  58. valor = self.request.GET.get('valor')
  59. if valor:
  60. qs = qs.filter(valor_id=valor)
  61. return qs
  62. def get_context_data(self, **kwargs):
  63. ctx = super().get_context_data(**kwargs)
  64. ctx['valores'] = Valor.objects.order_by('ticker')
  65. ctx['valor_filtro'] = self.request.GET.get('valor', '')
  66. return ctx
  67. class OperacionCreateView(CancelarUrlMixin, CreateView):
  68. model = Operacion
  69. form_class = OperacionForm
  70. template_name = 'cartera/form.html'
  71. success_url = reverse_lazy('cartera:operaciones')
  72. titulo = 'Nueva operación'
  73. def form_valid(self, form):
  74. messages.success(self.request, 'Operación registrada')
  75. return super().form_valid(form)
  76. class OperacionUpdateView(CancelarUrlMixin, UpdateView):
  77. model = Operacion
  78. form_class = OperacionForm
  79. template_name = 'cartera/form.html'
  80. success_url = reverse_lazy('cartera:operaciones')
  81. titulo = 'Editar operación'
  82. def form_valid(self, form):
  83. messages.success(self.request, 'Operación actualizada')
  84. return super().form_valid(form)
  85. class OperacionDeleteView(CancelarUrlMixin, DeleteView):
  86. model = Operacion
  87. template_name = 'cartera/confirm_delete.html'
  88. success_url = reverse_lazy('cartera:operaciones')
  89. titulo = 'Eliminar operación'
  90. def form_valid(self, form):
  91. messages.success(self.request, 'Operación eliminada')
  92. return super().form_valid(form)
  93. class DividendoListView(ListView):
  94. model = Dividendo
  95. template_name = 'cartera/dividendo_list.html'
  96. context_object_name = 'dividendos'
  97. queryset = Dividendo.objects.select_related('valor')
  98. class DividendoCreateView(CancelarUrlMixin, CreateView):
  99. model = Dividendo
  100. form_class = DividendoForm
  101. template_name = 'cartera/form.html'
  102. success_url = reverse_lazy('cartera:dividendos')
  103. titulo = 'Nuevo dividendo'
  104. def form_valid(self, form):
  105. messages.success(self.request, 'Dividendo registrado')
  106. return super().form_valid(form)
  107. class DividendoUpdateView(CancelarUrlMixin, UpdateView):
  108. model = Dividendo
  109. form_class = DividendoForm
  110. template_name = 'cartera/form.html'
  111. success_url = reverse_lazy('cartera:dividendos')
  112. titulo = 'Editar dividendo'
  113. def form_valid(self, form):
  114. messages.success(self.request, 'Dividendo actualizado')
  115. return super().form_valid(form)
  116. class DividendoDeleteView(CancelarUrlMixin, DeleteView):
  117. model = Dividendo
  118. template_name = 'cartera/confirm_delete.html'
  119. success_url = reverse_lazy('cartera:dividendos')
  120. titulo = 'Eliminar dividendo'
  121. def form_valid(self, form):
  122. messages.success(self.request, 'Dividendo eliminado')
  123. return super().form_valid(form)
  124. class ValorListView(ListView):
  125. model = Valor
  126. template_name = 'cartera/valor_list.html'
  127. context_object_name = 'valores'
  128. class ValorCreateView(CancelarUrlMixin, CreateView):
  129. model = Valor
  130. form_class = ValorForm
  131. template_name = 'cartera/form.html'
  132. success_url = reverse_lazy('cartera:valores')
  133. titulo = 'Nuevo valor'
  134. def form_valid(self, form):
  135. messages.success(self.request, 'Valor creado')
  136. return super().form_valid(form)
  137. class ValorUpdateView(CancelarUrlMixin, UpdateView):
  138. model = Valor
  139. form_class = ValorForm
  140. template_name = 'cartera/form.html'
  141. success_url = reverse_lazy('cartera:valores')
  142. titulo = 'Editar valor'
  143. def form_valid(self, form):
  144. messages.success(self.request, 'Valor actualizado')
  145. return super().form_valid(form)
  146. class ValorDeleteView(CancelarUrlMixin, DeleteView):
  147. model = Valor
  148. template_name = 'cartera/confirm_delete.html'
  149. success_url = reverse_lazy('cartera:valores')
  150. titulo = 'Eliminar valor'
  151. def form_valid(self, form):
  152. messages.success(self.request, 'Valor eliminado (con todas sus operaciones)')
  153. return super().form_valid(form)
  154. class DivisaListView(ListView):
  155. model = Divisa
  156. template_name = 'cartera/divisa_list.html'
  157. context_object_name = 'divisas'
  158. class DivisaCreateView(CancelarUrlMixin, CreateView):
  159. model = Divisa
  160. form_class = DivisaForm
  161. template_name = 'cartera/form.html'
  162. success_url = reverse_lazy('cartera:divisas')
  163. titulo = 'Nueva divisa'
  164. def form_valid(self, form):
  165. messages.success(self.request, 'Divisa creada')
  166. return super().form_valid(form)
  167. class DivisaUpdateView(CancelarUrlMixin, UpdateView):
  168. model = Divisa
  169. form_class = DivisaForm
  170. template_name = 'cartera/form.html'
  171. success_url = reverse_lazy('cartera:divisas')
  172. titulo = 'Editar divisa'
  173. def form_valid(self, form):
  174. messages.success(self.request, 'Divisa actualizada')
  175. return super().form_valid(form)
  176. class DivisaDeleteView(CancelarUrlMixin, DeleteView):
  177. model = Divisa
  178. template_name = 'cartera/confirm_delete.html'
  179. success_url = reverse_lazy('cartera:divisas')
  180. titulo = 'Eliminar divisa'
  181. def post(self, request, *args, **kwargs):
  182. try:
  183. return super().post(request, *args, **kwargs)
  184. except ProtectedError:
  185. messages.error(
  186. request,
  187. 'No se puede eliminar: hay valores que usan esta divisa.',
  188. )
  189. return redirect('cartera:divisas')