Beispiel #1
0
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
Beispiel #2
0
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
Beispiel #3
0
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')
Beispiel #4
0
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)
Beispiel #5
0
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)
Beispiel #6
0
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)