class SelectedProduct(models.Model): """ Abstract model representing a "selected" product in a cart or order. """ sku = fields.SKUField() description = CharField(_("Description"), max_length=200) quantity = models.IntegerField(_("Quantity"), default=0) unit_price = fields.MoneyField(_("Unit price"), default=Decimal("0")) total_price = fields.MoneyField(_("Total price"), default=Decimal("0")) class Meta: abstract = True def __unicode__(self): return "" def save(self, *args, **kwargs): """ Set the total price based on the given quantity. If the quantity is zero, which may occur via the cart page, just delete it. """ if not self.id or self.quantity > 0: self.total_price = self.unit_price * self.quantity super(SelectedProduct, self).save(*args, **kwargs) else: self.delete()
class Priced(models.Model): """ Abstract model with unit and sale price fields. Inherited by ``Product`` and ``ProductVariation`` models. """ unit_price = fields.MoneyField(_("Unit price")) sale_id = models.IntegerField(null=True) sale_price = fields.MoneyField(_("Sale price")) sale_from = models.DateTimeField(_("Sale start"), blank=True, null=True) sale_to = models.DateTimeField(_("Sale end"), blank=True, null=True) sku = fields.SKUField(unique=True, blank=True, null=True) num_in_stock = models.IntegerField(_("Number in stock"), blank=True, null=True) class Meta: abstract = True def on_sale(self): """ Returns True if the sale price is applicable. """ n = now() valid_from = self.sale_from is None or self.sale_from < n valid_to = self.sale_to is None or self.sale_to > n return self.sale_price is not None and valid_from and valid_to def has_price(self): """ Returns True if there is a valid price. """ return self.on_sale() or self.unit_price is not None def price(self): """ Returns the actual price - sale price if applicable otherwise the unit price. """ if self.on_sale(): return self.sale_price elif self.has_price(): return self.unit_price return Decimal("0") def copy_price_fields_to(self, obj_to): """ Copies each of the fields for the ``Priced`` model from one instance to another. Used for synchronising the denormalised fields on ``Product`` instances with their default variation. """ for field in Priced._meta.fields: if not isinstance(field, models.AutoField): setattr(obj_to, field.name, getattr(self, field.name)) obj_to.save()
class SelectedProduct(models.Model): """ Abstract model representing a "selected" product in a cart or order. """ sku = fields.SKUField() description = CharField(_("Description"), max_length=200) quantity = models.IntegerField(_("Quantity"), default=0) unit_price = fields.MoneyField(_("Unit price"), default=Decimal("0")) total_price = fields.MoneyField(_("Total price"), default=Decimal("0")) class Meta: abstract = True def __unicode__(self): return "" def save(self, *args, **kwargs): self.total_price = self.unit_price * self.quantity super(SelectedProduct, self).save(*args, **kwargs)
class Priced(models.Model): """ Abstract model with unit and sale price fields. Inherited by ``Product`` and ``ProductVariation`` models. """ CURRENCY = (('E', _('Евро')), ('R', _('Рубль')), ('U', _('Доллар'))) unit_price = fields.MoneyField(_("Цена")) currency = fields.CharField(_("Валюта"), blank=False, max_length=1, default='E', choices=CURRENCY) sale_id = models.IntegerField(null=True) sale_price = fields.MoneyField(_("Sale price")) sale_from = models.DateTimeField(_("Sale start"), blank=True, null=True) sale_to = models.DateTimeField(_("Sale end"), blank=True, null=True) sku = fields.SKUField(unique=True, blank=True, null=True) num_in_stock = models.IntegerField(_("Number in stock"), blank=True, null=True) class Meta: abstract = True def on_sale(self): """ Returns True if the sale price is applicable. """ n = now() valid_from = self.sale_from is None or self.sale_from < n valid_to = self.sale_to is None or self.sale_to > n return self.sale_price is not None and valid_from and valid_to def has_price(self): """ Returns True if there is a valid price. """ return self.on_sale() or self.unit_price is not None def price(self): """ Returns the actual price - sale price if applicable otherwise the unit price. """ settings.use_editable() rate = { 'R': 1, 'E': Decimal(settings.SHOP_EURO_EXCHANGE_RATE) * Decimal(1.02), 'U': Decimal(settings.SHOP_USD_EXCHANGE_RATE) * Decimal(1.02), } round_50 = lambda x: x if self.currency == 'R' else (int(x / 50) + ( x % 50 > 24)) * 50 if self.on_sale(): return round_50(self.sale_price * rate[self.currency]) elif self.has_price(): price = self.unit_price if isinstance(self, Product): price = self.variations.aggregate( Min('unit_price'))['unit_price__min'] return round_50(price * rate[self.currency]) return Decimal("0") def copy_price_fields_to(self, obj_to): """ Copies each of the fields for the ``Priced`` model from one instance to another. Used for synchronising the denormalised fields on ``Product`` instances with their default variation. """ for field in Priced._meta.fields: if not isinstance(field, models.AutoField): setattr(obj_to, field.name, getattr(self, field.name)) obj_to.save()
class ProductVariation(Priced): """ A combination of selected options from ``SHOP_OPTION_TYPE_CHOICES`` for a ``Product`` instance. """ product = models.ForeignKey("Product", related_name="variations") sku = fields.SKUField(unique=True) num_in_stock = models.IntegerField(_("Number in stock"), blank=True, null=True) default = models.BooleanField(_("Default")) image = models.ForeignKey("ProductImage", 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 and set the variation's image to be the first image of the product if no image is chosen for the variation. """ super(ProductVariation, self).save(*args, **kwargs) save = False if not self.sku: self.sku = self.id save = True if not self.image: image = self.product.images.all()[:1] if len(image) == 1: self.image = image[0] save = True if save: 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