class Condition(models.Model): COUNT, VALUE, COVERAGE = ("Count", "Value", "Coverage") TYPE_CHOICES = (( COUNT, _("Depends on number of items in basket that are in condition range") ), ( VALUE, _("Depends on value of items in basket that are in condition range") ), (COVERAGE, _("Needs to contain a set number of DISTINCT items from the condition range" ))) range = models.ForeignKey('offer.Range') type = models.CharField(max_length=128, choices=TYPE_CHOICES) value = PositiveDecimalField(decimal_places=2, max_digits=12) def __unicode__(self): if self.type == self.COUNT: return u"Basket includes %d item(s) from %s" % ( self.value, unicode(self.range).lower()) elif self.type == self.COVERAGE: return u"Basket includes %d distinct products from %s" % ( self.value, unicode(self.range).lower()) return u"Basket includes %d value from %s" % ( self.value, unicode(self.range).lower()) def consume_items(self, basket, lines=None): return () def is_satisfied(self, basket): """ Determines whether a given basket meets this condition. This is stubbed in this top-class object. The subclassing proxies are responsible for implementing it correctly. """ return False
class Benefit(models.Model): PERCENTAGE, FIXED, MULTIBUY, FIXED_PRICE = ("Percentage", "Absolute", "Multibuy", "Fixed price") TYPE_CHOICES = ( (PERCENTAGE, _("Discount is a % of the product's value")), (FIXED, _("Discount is a fixed amount off the product's value")), (MULTIBUY, _("Discount is to give the cheapest product for free")), (FIXED_PRICE, _("Get the products that meet the condition for a fixed price")), ) range = models.ForeignKey('offer.Range', null=True, blank=True) type = models.CharField(max_length=128, choices=TYPE_CHOICES) value = PositiveDecimalField(decimal_places=2, max_digits=12) price_field = 'price_incl_tax' # If this is not set, then there is no upper limit on how many products # can be discounted by this benefit. max_affected_items = models.PositiveIntegerField(blank=True, null=True, help_text="""Set this to prevent the discount consuming all items within the range that are in the basket.""" ) def __unicode__(self): if self.type == self.PERCENTAGE: desc = u"%s%% discount on %s" % (self.value, str( self.range).lower()) elif self.type == self.MULTIBUY: desc = u"Cheapest product is free from %s" % str(self.range) elif self.type == self.FIXED_PRICE: desc = u"The products that meet the condition are sold for %s" % self.value else: desc = u"%.2f discount on %s" % (self.value, str( self.range).lower()) if self.max_affected_items == 1: desc += u" (max 1 item)" elif self.max_affected_items > 1: desc += u" (max %d items)" % self.max_affected_items return desc def apply(self, basket, condition=None): return Decimal('0.00') def clean(self): # All benefits need a range apart from FIXED_PRICE if self.type and self.type != self.FIXED_PRICE and not self.range: raise ValidationError("Benefits of type %s need a range" % self.type) def _effective_max_affected_items(self): if not self.max_affected_items: max_affected_items = 10000 else: max_affected_items = self.max_affected_items return max_affected_items
class Benefit(models.Model): range = models.ForeignKey('offer.Range', null=True, blank=True, verbose_name=_("Range")) # Benefit types PERCENTAGE, FIXED, MULTIBUY, FIXED_PRICE = ("Percentage", "Absolute", "Multibuy", "Fixed price") SHIPPING_PERCENTAGE, SHIPPING_ABSOLUTE, SHIPPING_FIXED_PRICE = ( 'Shipping percentage', 'Shipping absolute', 'Shipping fixed price') TYPE_CHOICES = ( (PERCENTAGE, _("Discount is a % of the product's value")), (FIXED, _("Discount is a fixed amount off the product's value")), (MULTIBUY, _("Discount is to give the cheapest product for free")), (FIXED_PRICE, _("Get the products that meet the condition for a fixed price")), (SHIPPING_ABSOLUTE, _("Discount is a fixed amount off the shipping cost")), (SHIPPING_FIXED_PRICE, _("Get shipping for a fixed price")), (SHIPPING_PERCENTAGE, _("Discount is a % off the shipping cost")), ) type = models.CharField(_("Type"), max_length=128, choices=TYPE_CHOICES) value = PositiveDecimalField(_("Value"), decimal_places=2, max_digits=12, null=True, blank=True) # If this is not set, then there is no upper limit on how many products # can be discounted by this benefit. max_affected_items = models.PositiveIntegerField( _("Max Affected Items"), blank=True, null=True, help_text=_("Set this to prevent the discount consuming all items " "within the range that are in the basket.")) class Meta: verbose_name = _("Benefit") verbose_name_plural = _("Benefits") def proxy(self): field_dict = dict(self.__dict__) for field in field_dict.keys(): if field.startswith('_'): del field_dict[field] klassmap = { self.PERCENTAGE: PercentageDiscountBenefit, self.FIXED: AbsoluteDiscountBenefit, self.MULTIBUY: MultibuyDiscountBenefit, self.FIXED_PRICE: FixedPriceBenefit, self.SHIPPING_ABSOLUTE: ShippingAbsoluteDiscountBenefit, self.SHIPPING_FIXED_PRICE: ShippingFixedPriceBenefit, self.SHIPPING_PERCENTAGE: ShippingPercentageDiscountBenefit } if self.type in klassmap: return klassmap[self.type](**field_dict) return self def __unicode__(self): desc = self.proxy().__unicode__() if self.max_affected_items: desc += ungettext( " (max %d item)", " (max %d items)", self.max_affected_items) % self.max_affected_items return desc @property def description(self): return self.proxy().description def apply(self, basket, condition, offer=None): return D('0.00') def clean(self): if not self.type: raise ValidationError(_("Benefit requires a value")) method_name = 'clean_%s' % self.type.lower().replace(' ', '_') if hasattr(self, method_name): getattr(self, method_name)() def clean_multibuy(self): if not self.range: raise ValidationError( _("Multibuy benefits require a product range")) if self.value: raise ValidationError(_("Multibuy benefits don't require a value")) if self.max_affected_items: raise ValidationError( _("Multibuy benefits don't require a 'max affected items' " "attribute")) def clean_percentage(self): if not self.range: raise ValidationError( _("Percentage benefits require a product range")) if self.value > 100: raise ValidationError( _("Percentage discount cannot be greater than 100")) def clean_shipping_absolute(self): if not self.value: raise ValidationError(_("A discount value is required")) if self.range: raise ValidationError( _("No range should be selected as this benefit does not " "apply to products")) if self.max_affected_items: raise ValidationError( _("Shipping discounts don't require a 'max affected items' " "attribute")) def clean_shipping_percentage(self): if self.value > 100: raise ValidationError( _("Percentage discount cannot be greater than 100")) if self.range: raise ValidationError( _("No range should be selected as this benefit does not " "apply to products")) if self.max_affected_items: raise ValidationError( _("Shipping discounts don't require a 'max affected items' " "attribute")) def clean_shipping_fixed_price(self): if self.range: raise ValidationError( _("No range should be selected as this benefit does not " "apply to products")) if self.max_affected_items: raise ValidationError( _("Shipping discounts don't require a 'max affected items' " "attribute")) def clean_fixed_price(self): if self.range: raise ValidationError( _("No range should be selected as the condition range will " "be used instead.")) def clean_absolute(self): if not self.range: raise ValidationError( _("Percentage benefits require a product range")) def round(self, amount): """ Apply rounding to discount amount """ if hasattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION'): return settings.OSCAR_OFFER_ROUNDING_FUNCTION(amount) return amount.quantize(D('.01'), ROUND_DOWN) def _effective_max_affected_items(self): """ Return the maximum number of items that can have a discount applied during the application of this benefit """ return self.max_affected_items if self.max_affected_items else 10000 def can_apply_benefit(self, product): """ Determines whether the benefit can be applied to a given product """ return product.has_stockrecord and product.is_discountable def get_applicable_lines(self, basket, range=None): """ Return the basket lines that are available to be discounted :basket: The basket :range: The range of products to use for filtering. The fixed-price benefit ignores its range and uses the condition range """ if range is None: range = self.range line_tuples = [] for line in basket.all_lines(): product = line.product if (not range.contains(product) or not self.can_apply_benefit(product)): continue price = line.unit_price_incl_tax if not price: # Avoid zero price products continue if line.quantity_without_discount == 0: continue line_tuples.append((price, line)) # We sort lines to be cheapest first to ensure consistent applications return sorted(line_tuples) def shipping_discount(self, charge): return D('0.00')
class Condition(models.Model): COUNT, VALUE, COVERAGE = ("Count", "Value", "Coverage") TYPE_CHOICES = ((COUNT, _("Depends on number of items in basket that are in " "condition range")), (VALUE, _("Depends on value of items in basket that are in " "condition range")), (COVERAGE, _("Needs to contain a set number of DISTINCT items " "from the condition range"))) range = models.ForeignKey('offer.Range', verbose_name=_("Range"), null=True, blank=True) type = models.CharField(_('Type'), max_length=128, choices=TYPE_CHOICES, null=True, blank=True) value = PositiveDecimalField(_('Value'), decimal_places=2, max_digits=12, null=True, blank=True) proxy_class = models.CharField(_("Custom class"), null=True, blank=True, max_length=255, unique=True, default=None) class Meta: verbose_name = _("Condition") verbose_name_plural = _("Conditions") def proxy(self): """ Return the proxy model """ field_dict = dict(self.__dict__) for field in field_dict.keys(): if field.startswith('_'): del field_dict[field] if self.proxy_class: klass = load_proxy(self.proxy_class) return klass(**field_dict) klassmap = { self.COUNT: CountCondition, self.VALUE: ValueCondition, self.COVERAGE: CoverageCondition } if self.type in klassmap: return klassmap[self.type](**field_dict) return self def __unicode__(self): return self.proxy().__unicode__() @property def description(self): return self.proxy().description def consume_items(self, basket, affected_lines): pass def is_satisfied(self, basket): """ Determines whether a given basket meets this condition. This is stubbed in this top-class object. The subclassing proxies are responsible for implementing it correctly. """ return False def is_partially_satisfied(self, basket): """ Determine if the basket partially meets the condition. This is useful for up-selling messages to entice customers to buy something more in order to qualify for an offer. """ return False def get_upsell_message(self, basket): return None def can_apply_condition(self, product): """ Determines whether the condition can be applied to a given product """ return (self.range.contains_product(product) and product.is_discountable and product.has_stockrecord) def get_applicable_lines(self, basket, most_expensive_first=True): """ Return line data for the lines that can be consumed by this condition """ line_tuples = [] for line in basket.all_lines(): product = line.product if not self.can_apply_condition(product): continue price = line.unit_price_incl_tax if not price: continue line_tuples.append((price, line)) if most_expensive_first: return sorted(line_tuples, reverse=True) return sorted(line_tuples)
class Benefit(models.Model): PERCENTAGE, FIXED, MULTIBUY, FIXED_PRICE = ("Percentage", "Absolute", "Multibuy", "Fixed price") TYPE_CHOICES = ( (PERCENTAGE, _("Discount is a % of the product's value")), (FIXED, _("Discount is a fixed amount off the product's value")), (MULTIBUY, _("Discount is to give the cheapest product for free")), (FIXED_PRICE, _("Get the products that meet the condition for a fixed price")), ) range = models.ForeignKey('offer.Range', null=True, blank=True, verbose_name=_("Range")) type = models.CharField(_("Type"), max_length=128, choices=TYPE_CHOICES) value = PositiveDecimalField(_("Value"), decimal_places=2, max_digits=12, null=True, blank=True) # If this is not set, then there is no upper limit on how many products # can be discounted by this benefit. max_affected_items = models.PositiveIntegerField(_("Max Affected Items"), blank=True, null=True, help_text=_("Set this to prevent the discount consuming all items within the range that are in the basket.")) class Meta: verbose_name = _("Benefit") verbose_name_plural = _("Benefits") def __unicode__(self): if self.type == self.PERCENTAGE: desc = _("%(value)s%% discount on %(range)s") % {'value': self.value, 'range': unicode(self.range).lower()} elif self.type == self.MULTIBUY: desc = _("Cheapest product is free from %s") % unicode(self.range).lower() elif self.type == self.FIXED_PRICE: desc = _("The products that meet the condition are sold for %s") % self.value else: desc = _("%(value).2f discount on %(range)s") % {'value': float(self.value), 'range': unicode(self.range).lower()} if self.max_affected_items: desc += ungettext(" (max %d item)", " (max %d items)", self.max_affected_items) % self.max_affected_items return desc description = __unicode__ def apply(self, basket, condition): return D('0.00') def clean(self): if self.value is None: if not self.type: raise ValidationError(_("Benefit requires a value")) elif self.type != self.MULTIBUY: raise ValidationError(_("Benefits of type %s need a value") % self.type) elif self.value > 100 and self.type == 'Percentage': raise ValidationError(_("Percentage benefit value can't be greater than 100")) # All benefits need a range apart from FIXED_PRICE if self.type and self.type != self.FIXED_PRICE and not self.range: raise ValidationError(_("Benefits of type %s need a range") % self.type) def round(self, amount): """ Apply rounding to discount amount """ if hasattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION'): return settings.OSCAR_OFFER_ROUNDING_FUNCTION(amount) return amount.quantize(D('.01'), ROUND_DOWN) def _effective_max_affected_items(self): """ Return the maximum number of items that can have a discount applied during the application of this benefit """ return self.max_affected_items if self.max_affected_items else 10000 def can_apply_benefit(self, product): """ Determines whether the benefit can be applied to a given product """ return product.has_stockrecord and product.is_discountable def get_applicable_lines(self, basket, range=None): """ Return the basket lines that are available to be discounted :basket: The basket :range: The range of products to use for filtering. The fixed-price benefit ignores its range and uses the condition range """ if range is None: range = self.range line_tuples = [] for line in basket.all_lines(): product = line.product if (not range.contains(product) or not self.can_apply_benefit(product)): continue price = line.unit_price_incl_tax if not price: # Avoid zero price products continue if line.quantity_without_discount == 0: continue line_tuples.append((price, line)) # We sort lines to be cheapest first to ensure consistent applications return sorted(line_tuples)
class Condition(models.Model): COUNT, VALUE, COVERAGE = ("Count", "Value", "Coverage") TYPE_CHOICES = ( (COUNT, _("Depends on number of items in basket that are in condition range")), (VALUE, _("Depends on value of items in basket that are in condition range")), (COVERAGE, _("Needs to contain a set number of DISTINCT items from the condition range")) ) range = models.ForeignKey('offer.Range', verbose_name=_("Range")) type = models.CharField(_('Type'), max_length=128, choices=TYPE_CHOICES) value = PositiveDecimalField(_('Value'), decimal_places=2, max_digits=12) class Meta: verbose_name = _("Condition") verbose_name_plural = _("Conditions") def __unicode__(self): if self.type == self.COUNT: return _("Basket includes %(count)d item(s) from %(range)s") % { 'count': self.value, 'range': unicode(self.range).lower()} elif self.type == self.COVERAGE: return _("Basket includes %(count)d distinct products from %(range)s") % { 'count': self.value, 'range': unicode(self.range).lower()} return _("Basket includes %(count)d value from %(range)s") % { 'count': self.value, 'range': unicode(self.range).lower()} description = __unicode__ def consume_items(self, basket, lines): raise NotImplementedError("This method should never be called - " "ensure you are using the correct proxy model") def is_satisfied(self, basket): """ Determines whether a given basket meets this condition. This is stubbed in this top-class object. The subclassing proxies are responsible for implementing it correctly. """ return False def is_partially_satisfied(self, basket): """ Determine if the basket partially meets the condition. This is useful for up-selling messages to entice customers to buy something more in order to qualify for an offer. """ return False def get_upsell_message(self, basket): return None def can_apply_condition(self, product): """ Determines whether the condition can be applied to a given product """ return (self.range.contains_product(product) and product.is_discountable and product.has_stockrecord) def get_applicable_lines(self, basket): """ Return line data for the lines that can be consumed by this condition """ line_tuples = [] for line in basket.all_lines(): product = line.product if not self.can_apply_condition(product): continue price = line.unit_price_incl_tax if not price: continue line_tuples.append((price, line)) return sorted(line_tuples)