from decimal import Decimal from django.db import models class Divisa(models.Model): """Moneda de cotización y su tipo de cambio a euros.""" codigo = models.CharField( max_length=3, unique=True, help_text='Código ISO 4217; p. ej. EUR, USD, CHF', ) nombre = models.CharField(max_length=50) cambio_eur = models.DecimalField( max_digits=12, decimal_places=6, default=Decimal('1'), help_text='Euros que vale 1 unidad de esta divisa', ) actualizado_en = models.DateTimeField(null=True, blank=True, editable=False) class Meta: verbose_name = 'divisa' verbose_name_plural = 'divisas' ordering = ['codigo'] def __str__(self): return self.codigo class Valor(models.Model): """Un valor bursátil (acción, ETF...) de la cartera.""" ticker = models.CharField( max_length=20, unique=True, help_text='Ticker según Yahoo Finance; en Bolsa española con sufijo .MC', ) nombre = models.CharField(max_length=100) mercado = models.CharField(max_length=30, blank=True, help_text='p. ej. SIBE, NASDAQ') isin = models.CharField(max_length=12, blank=True) divisa = models.ForeignKey( Divisa, on_delete=models.PROTECT, related_name='valores', help_text='Moneda en la que cotiza y se opera este valor', ) precio_actual = models.DecimalField( max_digits=12, decimal_places=4, null=True, blank=True, editable=False, ) actualizado_en = models.DateTimeField(null=True, blank=True, editable=False) class Meta: verbose_name = 'valor' verbose_name_plural = 'valores' ordering = ['ticker'] def __str__(self): return f'{self.ticker} - {self.nombre}' class Operacion(models.Model): COMPRA = 'C' VENTA = 'V' TIPOS = [ (COMPRA, 'Compra'), (VENTA, 'Venta'), ] valor = models.ForeignKey(Valor, on_delete=models.CASCADE, related_name='operaciones') tipo = models.CharField('tipo de operación', max_length=1, choices=TIPOS) fecha = models.DateField() cantidad = models.DecimalField(max_digits=14, decimal_places=4) precio = models.DecimalField( max_digits=12, decimal_places=4, help_text='Precio unitario por acción', ) comisiones = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0')) cambio_eur = models.DecimalField( max_digits=12, decimal_places=6, null=True, blank=True, editable=False, help_text='Cambio a euros congelado al registrar la operación', ) class Meta: ordering = ['-fecha', '-pk'] verbose_name = 'operación' def __str__(self): return f'{self.get_tipo_display()} {self.cantidad} {self.valor.ticker} ({self.fecha})' def save(self, *args, **kwargs): # congela el tipo de cambio vigente si aún no tiene uno: # así las posiciones cerradas no se alteran al actualizar cotizaciones if self.cambio_eur is None and self.valor_id: self.cambio_eur = self.valor.divisa.cambio_eur super().save(*args, **kwargs) @property def importe(self): """Importe total de la operación, comisiones incluidas.""" return (self.cantidad * self.precio).quantize(Decimal('0.01')) + self.comisiones class Dividendo(models.Model): valor = models.ForeignKey(Valor, on_delete=models.CASCADE, related_name='dividendos') fecha = models.DateField() bruto = models.DecimalField(max_digits=10, decimal_places=2) retencion = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0')) cambio_eur = models.DecimalField( max_digits=12, decimal_places=6, null=True, blank=True, editable=False, help_text='Cambio a euros congelado al registrar el dividendo', ) class Meta: ordering = ['-fecha', '-pk'] def __str__(self): return f'Dividendo {self.bruto} € {self.valor.ticker} ({self.fecha})' def save(self, *args, **kwargs): if self.cambio_eur is None and self.valor_id: self.cambio_eur = self.valor.divisa.cambio_eur super().save(*args, **kwargs) @property def neto(self): return self.bruto - self.retencion