feat: added product variants
This commit is contained in:
+110
-2
@@ -4,6 +4,7 @@ from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
@@ -91,17 +92,100 @@ class Product(TimestampedModel):
|
||||
def price(self):
|
||||
return self.prices.filter(current=True).first()
|
||||
|
||||
@property
|
||||
def has_variants(self):
|
||||
return self.variants.exists()
|
||||
|
||||
@property
|
||||
def min_variant_price(self):
|
||||
return ProductPrice.objects.filter(variant__product=self, current=True).order_by('price').first()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('producto')
|
||||
verbose_name_plural = _('productos')
|
||||
ordering = ('id',)
|
||||
|
||||
|
||||
class ProductAttribute(models.Model):
|
||||
name = models.CharField(max_length=32, unique=True, verbose_name=_('nombre'))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('atributo de producto')
|
||||
verbose_name_plural = _('atributos de producto')
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class ProductAttributeValue(models.Model):
|
||||
attribute = models.ForeignKey(
|
||||
'shop.ProductAttribute', on_delete=models.CASCADE, related_name='values', verbose_name=_('atributo')
|
||||
)
|
||||
value = models.CharField(max_length=32, verbose_name=_('valor'))
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.attribute.name}: {self.value}'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('valor de atributo')
|
||||
verbose_name_plural = _('valores de atributo')
|
||||
unique_together = ('attribute', 'value')
|
||||
ordering = ('attribute__name', 'value')
|
||||
|
||||
|
||||
class ProductVariant(TimestampedModel):
|
||||
product = models.ForeignKey(
|
||||
'shop.Product', on_delete=models.CASCADE, related_name='variants', verbose_name=_('producto')
|
||||
)
|
||||
sku = models.CharField(max_length=64, blank=False, null=False, unique=True, verbose_name=_('código de referencia'))
|
||||
stock = models.DecimalField(max_digits=13, decimal_places=4, default=Decimal('0'), verbose_name=_('stock'))
|
||||
attribute_values = models.ManyToManyField(
|
||||
'shop.ProductAttributeValue', related_name='variants', blank=True, verbose_name=_('valores de atributo')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
attrs = ', '.join(str(value) for value in self.attribute_values.all())
|
||||
return f'{self.product.name} ({attrs})' if attrs else self.product.name
|
||||
|
||||
@property
|
||||
def price(self):
|
||||
return self.prices.filter(current=True).first()
|
||||
|
||||
def clean(self):
|
||||
if not self.pk:
|
||||
return
|
||||
|
||||
combination = set(self.attribute_values.values_list('pk', flat=True))
|
||||
|
||||
for other in ProductVariant.objects.filter(product=self.product).exclude(pk=self.pk):
|
||||
if set(other.attribute_values.values_list('pk', flat=True)) == combination:
|
||||
raise ValidationError(_('Ya existe una variante de este producto con la misma combinación de atributos.'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('variante de producto')
|
||||
verbose_name_plural = _('variantes de producto')
|
||||
ordering = ('product', 'sku')
|
||||
|
||||
|
||||
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'
|
||||
'shop.Product',
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('producto'),
|
||||
related_name='prices',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
variant = models.ForeignKey(
|
||||
'shop.ProductVariant',
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('variante'),
|
||||
related_name='prices',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
tax = models.ForeignKey('shop.Tax', on_delete=models.PROTECT, verbose_name=_('impuesto aplicable'))
|
||||
price_with_tax = models.DecimalField(
|
||||
@@ -116,6 +200,10 @@ class ProductPrice(TimestampedModel):
|
||||
def __str__(self):
|
||||
return f'{self.price} - {self.tax.code}'
|
||||
|
||||
def clean(self):
|
||||
if bool(self.product_id) == bool(self.variant_id):
|
||||
raise ValidationError(_('El precio debe pertenecer a un producto o a una variante, pero no a ambos.'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
tax_value = self.price * Decimal(self.tax.value / 100)
|
||||
self.price_with_tax = round(self.price + tax_value, 2)
|
||||
@@ -275,6 +363,14 @@ class OrderLine(TimestampedModel):
|
||||
product = models.ForeignKey(
|
||||
'shop.Product', on_delete=models.CASCADE, null=False, blank=False, verbose_name=_('producto')
|
||||
)
|
||||
variant = models.ForeignKey(
|
||||
'shop.ProductVariant',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='order_lines',
|
||||
verbose_name=_('variante'),
|
||||
)
|
||||
quantity = models.DecimalField(
|
||||
max_digits=13, decimal_places=4, default=Decimal('1'), null=False, blank=False, verbose_name=_('cantidad')
|
||||
)
|
||||
@@ -336,7 +432,7 @@ class Order(TimestampedModel):
|
||||
STATUS_REQUESTED_RETURN = 'RQT', _('devolución solicitada')
|
||||
STATUS_RETURNED = 'RTN', _('devuelto')
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, verbose_name=_('UUID'), db_index=True)
|
||||
uuid = models.UUIDField(default=uuid4, verbose_name=_('UUID'), unique=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'))
|
||||
@@ -418,8 +514,20 @@ class Cart(models.Model):
|
||||
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'))
|
||||
variant = models.ForeignKey(
|
||||
'shop.ProductVariant',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='cart_items',
|
||||
verbose_name=_('variante'),
|
||||
)
|
||||
quantity = models.PositiveIntegerField(default=1, verbose_name=_('cantidad'))
|
||||
|
||||
@property
|
||||
def price(self):
|
||||
return self.variant.price if self.variant else self.product.price
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('línea de carrito')
|
||||
verbose_name_plural = _('líneas de carrito')
|
||||
|
||||
Reference in New Issue
Block a user