from decimal import Decimal from uuid import uuid4 from django.conf import settings from django.contrib.auth import get_user_model from django.db import models from django.utils import timezone from django.utils.text import gettext_lazy as _ from django.utils.text import slugify User = get_user_model() class TimestampedModel(models.Model): creation_date = models.DateTimeField( auto_now_add=True, verbose_name=_("fecha de creación") ) last_modification_date = models.DateTimeField( auto_now=True, verbose_name=_("fecha de última modificación") ) class Meta: abstract = True class Tag(models.Model): name = models.CharField(max_length=16) def __str__(self): return self.name class Meta: verbose_name = _("etiqueta") verbose_name_plural = _("etiquetas") ordering = ("name",) class Product(TimestampedModel): sku = models.CharField( max_length=64, blank=False, null=False, unique=True, verbose_name=_("código de referencia"), ) name = models.CharField( max_length=96, blank=False, null=False, verbose_name=_("nombre"), ) description = models.TextField( blank=True, default="", verbose_name=_("descripción"), ) stock = models.DecimalField( max_digits=13, decimal_places=4, default=Decimal("0"), verbose_name=_("stock"), ) is_digital_asset = models.BooleanField( default=False, verbose_name=_("es un activo digital") ) url = models.URLField(blank=True, verbose_name=_("URL de descarga")) images = models.ManyToManyField( "files.FileUpload", blank=True, verbose_name=_("imágenes") ) tags = models.ManyToManyField("shop.Tag", blank=True, verbose_name=_("etiquetas")) brand = models.ForeignKey( "shop.Brand", blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("marca"), ) slug = models.SlugField( default="", blank=True, null=True, max_length=128, verbose_name=_("Slug") ) hidden = models.BooleanField(default=False, db_index=True, verbose_name=_("oculto")) def __str__(self): return self.name def save( self, *args, **kwargs, ): if not self.pk: self.slug = slugify(self.name)[:128] super().save(*args, **kwargs) @property def price(self): return self.prices.filter(current=True).first() class Meta: verbose_name = _("producto") verbose_name_plural = _("productos") ordering = ("id",) class ProductPrice(TimestampedModel): price = models.DecimalField( max_digits=11, decimal_places=2, verbose_name=_("precio") ) date = models.DateTimeField(default=timezone.now, verbose_name=_("fecha")) product = models.ForeignKey( "shop.Product", on_delete=models.CASCADE, verbose_name=_("producto"), related_name="prices", ) tax = models.ForeignKey( "shop.Tax", on_delete=models.PROTECT, verbose_name=_("impuesto aplicable"), ) price_with_tax = models.DecimalField( max_digits=11, decimal_places=2, blank=True, null=True, verbose_name=_("precio con impuestos"), ) current = models.BooleanField(default=False, verbose_name=_("es el precio actual")) def __str__(self): return f"{self.price} - {self.tax.code}" def save(self, *args, **kwargs): tax_value = self.price * Decimal(self.tax.value / 100) self.price_with_tax = round(self.price + tax_value, 2) super().save() class Meta: verbose_name = _("precio de producto") verbose_name_plural = _("precio de producto") ordering = ("-date",) class ProductBatch(TimestampedModel): code = models.CharField( max_length=32, unique=True, db_index=True, verbose_name=_("código") ) product = models.ForeignKey( "shop.Product", on_delete=models.PROTECT, verbose_name=_("producto") ) quantity = models.DecimalField( max_digits=13, decimal_places=4, default=Decimal("1"), verbose_name=_("cantidad"), ) expiration_date = models.DateField( blank=True, null=True, verbose_name=_("fecha de caducidad / consumo preferente") ) provider = models.ForeignKey( "shop.Provider", on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_("proveedor"), ) def __str__(self): return f"{self.code} - {self.product.name} - {self.quantity}" class Meta: verbose_name = _("remesa de producto") verbose_name_plural = _("remesas de productos") class Tax(TimestampedModel): code = models.CharField( max_length=8, blank=False, unique=True, verbose_name=_("código de impuesto") ) value = models.PositiveIntegerField( null=False, blank=False, verbose_name=_("valor entero (porcentaje)") ) def __str__(self): return f"{self.code} - {self.value}%" class Meta: verbose_name = _("impuesto") verbose_name_plural = _("impuestos") ordering = ("id",) class Provider(TimestampedModel): vat_id = models.CharField( max_length=32, blank=False, unique=True, verbose_name=_("documento de identidad"), ) name = models.CharField(max_length=64, blank=False, verbose_name=_("nombre")) email = models.EmailField(blank=False, verbose_name=_("e-mail de contacto")) phone = models.CharField( max_length=16, blank=True, null=True, default="", verbose_name=_("Teléfono de contacto"), ) address = models.CharField(max_length=255, blank=False, verbose_name=_("dirección")) city = models.CharField(max_length=64, blank=False, verbose_name=_("ciudad")) state = models.CharField(max_length=64, blank=False, verbose_name=_("región")) country = models.CharField(max_length=64, blank=False, verbose_name=_("país")) zip = models.CharField(max_length=32, blank=False, verbose_name=_("código postal")) def __str__(self): return f"{self.vat_id} - {self.name}" class Meta: verbose_name = _("proveedor") verbose_name_plural = _("proveedores") class Brand(TimestampedModel): name = models.CharField( max_length=100, unique=True, blank=False, null=False, verbose_name=_("nombre") ) class Meta: verbose_name = _("marca") verbose_name_plural = _("marcas") class OrderLine(TimestampedModel): order = models.ForeignKey( "shop.Order", on_delete=models.CASCADE, related_name="lines", verbose_name=_("pedido"), ) product = models.ForeignKey( "shop.Product", on_delete=models.CASCADE, null=False, blank=False, verbose_name=_("producto"), ) quantity = models.DecimalField( max_digits=13, decimal_places=4, default=Decimal("1"), null=False, blank=False, verbose_name=_("cantidad"), ) # Precio del producto en el momento en el que se crea el pedido price = models.DecimalField( max_digits=13, decimal_places=4, verbose_name=_("precio") ) # base_total = quantity * price base_total = models.DecimalField( max_digits=13, decimal_places=4, default=Decimal("0"), null=False, blank=False, verbose_name=_("total sin impuestos"), ) # Valor entero del impuesto en el momento en el que se crea el pedido tax_value = models.PositiveIntegerField( null=False, blank=False, verbose_name=_("valor de impuestos") ) # taxes = price * quantity * (tax_value / 100) taxes = models.DecimalField( max_digits=13, decimal_places=4, verbose_name=_("impuestos") ) # total = base_total + taxes total = models.DecimalField( max_digits=13, decimal_places=4, verbose_name=_("total") ) def __str__(self): return f"{self.product.name} - {self.quantity} - {self.price}" class Meta: verbose_name = _("línea de pedido") verbose_name_plural = _("líneas de pedido") class Order(TimestampedModel): class Statuses(models.TextChoices): STATUS_PENDING = "PEN", _("pendiente de pago") STATUS_PAID = "PAI", _("pagado") STATUS_READY = "RDY", _("preparado") STATUS_TO_BE_SENT = "TBS", _("listo para ser enviado") STATUS_SENT = "SNT", _("enviado") STATUS_DELIVERED = "DLV", _("entregado") STATUS_FINISHED = "FIN", _("finalizado") STATUS_CANCELED = "CAN", _("cancelado") STATUS_RETURNED = "RTN", _("devuelto") uuid = models.UUIDField(default=uuid4, verbose_name=_("UUID"), db_index=True) status = models.CharField( max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_("estado") ) base_total = models.DecimalField( default=Decimal("0"), max_digits=13, decimal_places=2, verbose_name=_("base imponible"), ) total = models.DecimalField( default=Decimal("0"), max_digits=13, decimal_places=2, verbose_name=_("total") ) user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name=_("cliente")) # Billing information billing_address = models.CharField( max_length=255, blank=False, verbose_name=_("dirección de facturación") ) billing_city = models.CharField( max_length=64, blank=False, verbose_name=_("ciudad de facturación") ) billing_state = models.CharField( max_length=64, blank=False, verbose_name=_("región de facturación") ) billing_country = models.CharField( max_length=64, blank=False, verbose_name=_("país de facturación") ) billing_zip = models.CharField( max_length=32, blank=False, verbose_name=_("código postal de facturación") ) # Shipping information shipping_address = models.CharField( max_length=255, blank=False, verbose_name=_("dirección") ) shipping_city = models.CharField( max_length=64, blank=False, verbose_name=_("ciudad") ) shipping_state = models.CharField( max_length=64, blank=False, verbose_name=_("región") ) shipping_country = models.CharField( max_length=64, blank=False, verbose_name=_("país") ) shipping_zip = models.CharField( max_length=32, blank=False, verbose_name=_("código postal") ) contact_phone = models.CharField( max_length=32, blank=True, verbose_name=_("teléfono de contacto") ) def calculate_total_from_lines(self): self.base_total = self.lines.aggregate(base_total=models.Sum("base_total")).get( "base_total" ) self.total = self.lines.aggregate(base_total=models.Sum("total")).get( "base_total" ) self.save() class Meta: verbose_name = _("pedido") verbose_name_plural = _("pedidos") class Cart(models.Model): uuid = models.UUIDField(default=uuid4, primary_key=True, verbose_name=_("uuid")) user = models.ForeignKey( User, on_delete=models.CASCADE, verbose_name=_("usuario"), blank=True, null=True, ) creation_date = models.DateTimeField( auto_now_add=True, verbose_name=_("fecha de creación") ) class Meta: verbose_name = _("carrito") verbose_name_plural = _("carritos") class CartItem(models.Model): cart = models.ForeignKey( "shop.Cart", on_delete=models.CASCADE, related_name="items", verbose_name=_("carrito"), ) product = models.ForeignKey( "shop.Product", on_delete=models.CASCADE, verbose_name=_("producto") ) quantity = models.PositiveIntegerField(default=1, verbose_name=_("cantidad")) class Meta: verbose_name = _("línea de carrito") verbose_name_plural = _("líneas de carrito") class CustomerAddress(models.Model): class Types(models.TextChoices): BILLING = "BILL", _("facturación") SHIPPING = "SHIP", _("envío") user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, verbose_name=_("usuario")) vat_id = models.CharField( max_length=16, blank=True, null=True, default="", verbose_name=_("número de identificación fiscal"), ) full_name = models.CharField( max_length=128, blank=False, null=False, verbose_name=_("dirección") ) address = models.CharField( max_length=128, blank=False, null=False, verbose_name=_("dirección") ) address_town = models.CharField( max_length=64, blank=False, null=False, verbose_name=_("localidad") ) address_zip = models.CharField( max_length=16, blank=False, null=False, verbose_name=_("código postal") ) address_state = models.CharField( max_length=64, blank=False, null=False, verbose_name=_("provincia") ) address_country = models.CharField( max_length=64, default=settings.SHOP_COUNTRY, blank=False, null=False, verbose_name=_("país"), ) address_phone = models.CharField( max_length=16, blank=True, null=True, default="", verbose_name=_("teléfono") ) address_type = models.CharField( max_length=4, choices=Types.choices, default=Types.SHIPPING, verbose_name=_("tipo de dirección"), ) default = models.BooleanField(default=True, verbose_name=_("por defecto")) hidden = models.BooleanField(default=False, db_index=True, verbose_name=_("oculto")) class Meta: verbose_name = _("dirección de cliente") verbose_name_plural = _("direcciones de cliente") class ShippingMethod(models.Model): name = models.CharField(max_length=64, verbose_name=_("nombre")) description = models.CharField( max_length=256, default="", blank=True, verbose_name=_("description") ) url = models.URLField(default="", blank=True, verbose_name=_("url")) shipping_product = models.ForeignKey( "shop.Product", on_delete=models.CASCADE, verbose_name=_("producto de envío") ) enabled = models.BooleanField(default=True, verbose_name=_("habilitado")) class Meta: verbose_name = _("método de envío") verbose_name_plural = _("métodos de envío")