models.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. from decimal import Decimal
  2. from django.db import models
  3. class Divisa(models.Model):
  4. """Moneda de cotización y su tipo de cambio a euros."""
  5. codigo = models.CharField(
  6. max_length=3, unique=True,
  7. help_text='Código ISO 4217; p. ej. EUR, USD, CHF',
  8. )
  9. nombre = models.CharField(max_length=50)
  10. cambio_eur = models.DecimalField(
  11. max_digits=12, decimal_places=6, default=Decimal('1'),
  12. help_text='Euros que vale 1 unidad de esta divisa',
  13. )
  14. actualizado_en = models.DateTimeField(null=True, blank=True, editable=False)
  15. class Meta:
  16. verbose_name = 'divisa'
  17. verbose_name_plural = 'divisas'
  18. ordering = ['codigo']
  19. def __str__(self):
  20. return self.codigo
  21. class Valor(models.Model):
  22. """Un valor bursátil (acción, ETF...) de la cartera."""
  23. ticker = models.CharField(
  24. max_length=20, unique=True,
  25. help_text='Ticker según Yahoo Finance; en Bolsa española con sufijo .MC',
  26. )
  27. nombre = models.CharField(max_length=100)
  28. mercado = models.CharField(max_length=30, blank=True, help_text='p. ej. SIBE, NASDAQ')
  29. isin = models.CharField(max_length=12, blank=True)
  30. divisa = models.ForeignKey(
  31. Divisa, on_delete=models.PROTECT, related_name='valores',
  32. help_text='Moneda en la que cotiza y se opera este valor',
  33. )
  34. precio_actual = models.DecimalField(
  35. max_digits=12, decimal_places=4, null=True, blank=True,
  36. editable=False,
  37. )
  38. actualizado_en = models.DateTimeField(null=True, blank=True, editable=False)
  39. class Meta:
  40. verbose_name = 'valor'
  41. verbose_name_plural = 'valores'
  42. ordering = ['ticker']
  43. def __str__(self):
  44. return f'{self.ticker} - {self.nombre}'
  45. class Operacion(models.Model):
  46. COMPRA = 'C'
  47. VENTA = 'V'
  48. TIPOS = [
  49. (COMPRA, 'Compra'),
  50. (VENTA, 'Venta'),
  51. ]
  52. valor = models.ForeignKey(Valor, on_delete=models.CASCADE, related_name='operaciones')
  53. tipo = models.CharField('tipo de operación', max_length=1, choices=TIPOS)
  54. fecha = models.DateField()
  55. cantidad = models.DecimalField(max_digits=14, decimal_places=4)
  56. precio = models.DecimalField(
  57. max_digits=12, decimal_places=4,
  58. help_text='Precio unitario por acción',
  59. )
  60. comisiones = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0'))
  61. cambio_eur = models.DecimalField(
  62. max_digits=12, decimal_places=6, null=True, blank=True,
  63. editable=False,
  64. help_text='Cambio a euros congelado al registrar la operación',
  65. )
  66. class Meta:
  67. ordering = ['-fecha', '-pk']
  68. verbose_name = 'operación'
  69. def __str__(self):
  70. return f'{self.get_tipo_display()} {self.cantidad} {self.valor.ticker} ({self.fecha})'
  71. def save(self, *args, **kwargs):
  72. # congela el tipo de cambio vigente si aún no tiene uno:
  73. # así las posiciones cerradas no se alteran al actualizar cotizaciones
  74. if self.cambio_eur is None and self.valor_id:
  75. self.cambio_eur = self.valor.divisa.cambio_eur
  76. super().save(*args, **kwargs)
  77. @property
  78. def importe(self):
  79. """Importe total de la operación, comisiones incluidas."""
  80. return (self.cantidad * self.precio).quantize(Decimal('0.01')) + self.comisiones
  81. class Dividendo(models.Model):
  82. valor = models.ForeignKey(Valor, on_delete=models.CASCADE, related_name='dividendos')
  83. fecha = models.DateField()
  84. bruto = models.DecimalField(max_digits=10, decimal_places=2)
  85. retencion = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0'))
  86. cambio_eur = models.DecimalField(
  87. max_digits=12, decimal_places=6, null=True, blank=True,
  88. editable=False,
  89. help_text='Cambio a euros congelado al registrar el dividendo',
  90. )
  91. class Meta:
  92. ordering = ['-fecha', '-pk']
  93. def __str__(self):
  94. return f'Dividendo {self.bruto} € {self.valor.ticker} ({self.fecha})'
  95. def save(self, *args, **kwargs):
  96. if self.cambio_eur is None and self.valor_id:
  97. self.cambio_eur = self.valor.divisa.cambio_eur
  98. super().save(*args, **kwargs)
  99. @property
  100. def neto(self):
  101. return self.bruto - self.retencion