class ProductVariation(Priced): """ A combination of selected options from ``SHOP_OPTION_TYPE_CHOICES`` for a ``Product`` instance. """ product = models.ForeignKey("Product", related_name="variations") default = models.BooleanField(_("Default")) image = models.ForeignKey("ProductImage", verbose_name=_("Image"), null=True, blank=True) objects = managers.ProductVariationManager() __metaclass__ = ProductVariationMetaclass class Meta: ordering = ("-default", ) def __unicode__(self): """ Display the option names and values for the variation. """ options = [] for field in self.option_fields(): if getattr(self, field.name) is not None: options.append( "%s: %s" % (unicode(field.verbose_name), getattr(self, field.name))) return ("%s %s" % (unicode(self.product), ", ".join(options))).strip() def save(self, *args, **kwargs): """ Use the variation's ID as the SKU when the variation is first created. """ super(ProductVariation, self).save(*args, **kwargs) if not self.sku: self.sku = self.id self.save() def get_absolute_url(self): return self.product.get_absolute_url() @classmethod def option_fields(cls): """ Returns each of the model fields that are dynamically created from ``SHOP_OPTION_TYPE_CHOICES`` in ``ProductVariationMetaclass``. """ all_fields = cls._meta.fields return [f for f in all_fields if isinstance(f, fields.OptionField)] def options(self): """ Returns the field values of each of the model fields that are dynamically created from ``SHOP_OPTION_TYPE_CHOICES`` in ``ProductVariationMetaclass``. """ return [getattr(self, field.name) for field in self.option_fields()] def live_num_in_stock(self): """ Returns the live number in stock, which is ``self.num_in_stock - num in carts``. Also caches the value for subsequent lookups. """ if self.num_in_stock is None: return None if not hasattr(self, "_cached_num_in_stock"): num_in_stock = self.num_in_stock items = CartItem.objects.filter(sku=self.sku) aggregate = items.aggregate(quantity_sum=models.Sum("quantity")) num_in_carts = aggregate["quantity_sum"] if num_in_carts is not None: num_in_stock = num_in_stock - num_in_carts self._cached_num_in_stock = num_in_stock return self._cached_num_in_stock def has_stock(self, quantity=1): """ Returns ``True`` if the given quantity is in stock, by checking against ``live_num_in_stock``. ``True`` is returned when ``num_in_stock`` is ``None`` which is how stock control is disabled. """ live = self.live_num_in_stock() return live is None or quantity == 0 or live >= quantity def update_stock(self, quantity): """ Update the stock amount - called when an order is complete. Also update the denormalised stock amount of the product if this is the default variation. """ if self.num_in_stock is not None: self.num_in_stock += quantity self.save() if self.default: self.product.num_in_stock = self.num_in_stock self.product.save()
class ProductVariation(with_metaclass(ProductVariationMetaclass, Priced)): """ A combination of selected options from ``SHOP_OPTION_TYPE_CHOICES`` for a ``Product`` instance. """ product = models.ForeignKey("Product", on_delete=models.DO_NOTHING, related_name="variations") default = models.BooleanField(_("Default"), default=False) image = models.ForeignKey("ProductImage", verbose_name=_("Image"), null=True, blank=True, on_delete=models.SET_NULL) objects = managers.ProductVariationManager() class Meta: ordering = ("-default", ) def __str__(self): """ Display the option names and values for the variation. """ options = [] for field in self.option_fields(): name = getattr(self, field.name) if name is not None: option = u"%s: %s" % (field.verbose_name, name) options.append(option) result = u"%s %s" % (str(self.product), u", ".join(options)) return result.strip() def save(self, *args, **kwargs): """ Use the variation's ID as the SKU when the variation is first created. """ super(ProductVariation, self).save(*args, **kwargs) if not self.sku: self.sku = self.id self.save() def get_absolute_url(self): return self.product.get_absolute_url() def validate_unique(self, *args, **kwargs): """ Overridden to ensure SKU is unique per site, which can't be defined by ``Meta.unique_together`` since it can't span relationships. """ super(ProductVariation, self).validate_unique(*args, **kwargs) if self.__class__.objects.exclude(id=self.id).filter( product__site_id=self.product.site_id, sku=self.sku).exists(): raise ValidationError({"sku": _("SKU is not unique")}) @classmethod def option_fields(cls): """ Returns each of the model fields that are dynamically created from ``SHOP_OPTION_TYPE_CHOICES`` in ``ProductVariationMetaclass``. """ all_fields = cls._meta.fields return [ f for f in all_fields if isinstance(f, fields.OptionField) and not hasattr(f, "translated_field") ] def options(self): """ Returns the field values of each of the model fields that are dynamically created from ``SHOP_OPTION_TYPE_CHOICES`` in ``ProductVariationMetaclass``. """ return [getattr(self, field.name) for field in self.option_fields()] def live_num_in_stock(self): """ Returns the live number in stock, which is ``self.num_in_stock - num in carts``. Also caches the value for subsequent lookups. """ if self.num_in_stock is None: return None if not hasattr(self, "_cached_num_in_stock"): num_in_stock = self.num_in_stock carts = Cart.objects.current() items = CartItem.objects.filter(sku=self.sku, cart__in=carts) aggregate = items.aggregate(quantity_sum=models.Sum("quantity")) num_in_carts = aggregate["quantity_sum"] if num_in_carts is not None: num_in_stock = num_in_stock - num_in_carts self._cached_num_in_stock = num_in_stock return self._cached_num_in_stock def has_stock(self, quantity=1): """ Returns ``True`` if the given quantity is in stock, by checking against ``live_num_in_stock``. ``True`` is returned when ``num_in_stock`` is ``None`` which is how stock control is disabled. """ live = self.live_num_in_stock() return live is None or quantity == 0 or live >= quantity def update_stock(self, quantity): """ Update the stock amount - called when an order is complete. Also update the denormalised stock amount of the product if this is the default variation. """ if self.num_in_stock is not None: self.num_in_stock += quantity self.save() if self.default: self.product.num_in_stock = self.num_in_stock self.product.save()