459 lines
13 KiB
Python
459 lines
13 KiB
Python
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")
|
|
)
|
|
|
|
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, verbose_name=_("usuario"))
|
|
vat_id = models.CharField(
|
|
max_length=16,
|
|
blank=True,
|
|
null=True,
|
|
default="",
|
|
verbose_name=_("número de identificación fiscal"),
|
|
)
|
|
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)
|
|
|
|
class Meta:
|
|
verbose_name = _("dirección de cliente")
|
|
verbose_name_plural = _("direcciones de cliente")
|