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 from config.models import SingletonModel from shop.settings import CURRENCY_CODES 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 ProductCategory(models.Model): name = models.CharField(max_length=32) parent = models.ForeignKey( "shop.ProductCategory", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("categoría padre"), ) promoted = models.BooleanField(default=False, verbose_name=_("promocionada")) hidden = models.BooleanField(default=False, verbose_name=_("oculta")) show_in_navbar = models.BooleanField( default=False, verbose_name=_("mostrar en cabecera") ) slug = models.SlugField( max_length=48, blank=True, default="", verbose_name=_("slug"), unique=True, db_index=True, ) def __str__(self): return self.name def save(self, *args, **kwargs): self.slug = slugify(self.name) super().save() class Meta: verbose_name = _("categoría") verbose_name_plural = _("categorías") 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")) categories = models.ManyToManyField( "shop.ProductCategory", blank=True, verbose_name=_("categorías") ) 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")) is_shipping_method = models.BooleanField( default=False, verbose_name=_("es forma de envío") ) 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") def create_order_code(*args, **kwargs): return ( timezone.now() .isoformat() .replace("-", "") .replace("T", "") .replace(":", "") .replace(".", "")[:-5] ) class Order(TimestampedModel): class Statuses(models.TextChoices): STATUS_PENDING = "PEN", _("pendiente de pago") STATUS_PAID = "PAI", _("pagado") STATUS_RETURNED = "RTN", _("devuelto") STATUS_ERROR = "ERR", _("Error") class ShippingStatuses(models.TextChoices): STATUS_NOT_READY = "NOT", _("no preparado") 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_REQUESTED_RETURN = "RQT", _("devolución solicitada") STATUS_RETURNED = "RTN", _("devuelto") uuid = models.UUIDField(default=uuid4, verbose_name=_("UUID"), db_index=True) code = models.CharField(default=create_order_code, max_length=20, unique=True) status = models.CharField( max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_("estado") ) shipping_status = models.CharField( max_length=3, default=ShippingStatuses.STATUS_NOT_READY, verbose_name=_("estado de envío"), ) 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, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_("cliente") ) email = models.EmailField(blank=True, verbose_name=_("e-mail")) # Datos de facturación 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") ) # Datos de envío 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") ) shipping_method = models.ForeignKey( "shop.ShippingMethod", blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("método de envío"), ) 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") ordering = ("creation_date",) 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"), ) email = models.EmailField(blank=False, null=False, verbose_name=_("e-mail")) default = models.BooleanField(default=True, verbose_name=_("por defecto")) hidden = models.BooleanField(default=False, db_index=True, verbose_name=_("oculto")) def __str__(self): return f"{self.full_name} - {self.address}, {self.address_zip}, {self.address_state} - {self.address_country}" 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") class WishlistedProduct(TimestampedModel): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("usuario")) product = models.ForeignKey( "shop.Product", on_delete=models.CASCADE, verbose_name=_("producto") ) class Meta: verbose_name = _("productos deseados") verbose_name_plural = _("productos deseados") unique_together = ( "user", "product", ) class Payment(models.Model): class MethodChoices(models.TextChoices): REDSYS = "REDSYS" hash = models.UUIDField(verbose_name=_("Hash"), default=uuid4, primary_key=True) amount = models.DecimalField( max_digits=8, decimal_places=2, default=Decimal("0"), verbose_name=_("Cantidad"), ) order = models.ForeignKey( "shop.Order", on_delete=models.PROTECT, verbose_name=_("pedido"), related_name="payments", ) user = models.ForeignKey( User, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_("cliente") ) creation_date = models.DateTimeField( auto_now_add=True, verbose_name=_("Fecha de creación") ) metadata = models.JSONField(default=dict, verbose_name=_("Metadata")) method = models.CharField( max_length=6, choices=MethodChoices.choices, default=MethodChoices.REDSYS, verbose_name=_("método de pago"), ) class Meta: verbose_name = _("Pago") verbose_name_plural = _("Pagos") class ShopSettings(SingletonModel): merchant_code = models.CharField( max_length=9, blank=False, null=False, verbose_name=_("Identificación de comercio"), ) currency_code = models.CharField( max_length=4, default="978", choices=CURRENCY_CODES, verbose_name=_("Código de moneda"), ) # 978 == EURO terminal = models.CharField(max_length=8, verbose_name=_("Terminal")) shared_secret = models.CharField(max_length=100, verbose_name=_("Clave de Redsys")) tpv_domain = models.URLField( max_length=128, blank=True, null=True, verbose_name=_("dominio del tpv") ) class Meta: verbose_name = _("ajustes de la tienda") verbose_name_plural = _("ajustes de la tienda")