Пример #1
0
class Image(AbstractPromotion):
    """
    An image promotion is simply a named image which has an optional
    link to another part of the site (or another site).

    This can be used to model both banners and pods.
    """
    _type = 'Image'
    name = models.CharField(_("Name"), max_length=128)
    link_url = ExtendedURLField(
        _('Link URL'),
        blank=True,
        help_text=_('This is where this promotion links to'))
    image = models.ImageField(_('Image'),
                              upload_to=settings.OSCAR_PROMOTION_FOLDER,
                              max_length=255)
    date_created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

    class Meta:
        app_label = 'promotions'
        verbose_name = _("Image")
        verbose_name_plural = _("Image")
Пример #2
0
class AbstractProductList(AbstractPromotion):
    """
    Abstract superclass for promotions which are essentially a list
    of products.
    促销的抽象超类本质上是产品列表。
    """
    name = models.CharField(pgettext_lazy("Promotion product list title",
                                          "Title"),
                            max_length=255)
    description = models.TextField(_("Description"), blank=True)
    link_url = ExtendedURLField(_('Link URL'), blank=True)
    link_text = models.CharField(_("Link text"), max_length=255, blank=True)
    date_created = models.DateTimeField(auto_now_add=True)

    class Meta:
        abstract = True
        app_label = 'promotions'
        verbose_name = _("Product list")
        verbose_name_plural = _("Product lists")

    def __str__(self):
        return self.name

    def template_context(self, request):
        return {'products': self.get_products()}
Пример #3
0
class PagePromotion(LinkedPromotion):
    """
    A promotion embedded on a particular page.
    """
    page_url = ExtendedURLField(max_length=128, db_index=True)

    def __unicode__(self):
        return u"%s on %s" % (self.content_object, self.page_url)

    def get_link(self):
        return reverse('promotions:page-click',
                       kwargs={'page_promotion_id': self.id})
Пример #4
0
class AbstractProductList(AbstractPromotion):
    """
    Abstract superclass for promotions which are essentially a list
    of products.
    """
    name = models.CharField(_("Title"), max_length=255)
    description = models.TextField(null=True, blank=True)
    link_url = ExtendedURLField(blank=True, null=True)
    date_created = models.DateTimeField(auto_now_add=True)

    class Meta:
        abstract = True

    def __unicode__(self):
        return self.name
Пример #5
0
class PagePromotion(LinkedPromotion):
    """
    A promotion embedded on a particular page.
    """
    page_url = ExtendedURLField(_('Page URL'), max_length=128, db_index=True)

    def __str__(self):
        return u"%s on %s" % (self.content_object, self.page_url)

    def get_link(self):
        return reverse('promotions:page-click',
                       kwargs={'page_promotion_id': self.id})

    class Meta(LinkedPromotion.Meta):
        verbose_name = _("Page Promotion")
        verbose_name_plural = _("Page Promotions")
Пример #6
0
class Image(AbstractPromotion):
    """
    An image promotion is simply a named image which has an optional 
    link to another part of the site (or another site).
    
    This can be used to model both banners and pods.
    """
    _type = 'Image'
    name = models.CharField(_("Name"), max_length=128)
    link_url = ExtendedURLField(blank=True,
                                null=True,
                                help_text="""This is 
        where this promotion links to""")
    image = models.ImageField(upload_to=settings.OSCAR_PROMOTION_FOLDER)
    date_created = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return self.name
Пример #7
0
class CarouselImage(AbstractPromotion):
    """
    
    """
    _type = 'CarouselImage'
    name = models.CharField(_("Name"), max_length=128)
    url = ExtendedURLField(_('url'),
                           max_length=128,
                           db_index=True,
                           verify_exists=True)
    image = models.ImageField(_('Image'),
                              upload_to=settings.OSCAR_PROMOTION_FOLDER,
                              max_length=255)
    date_created = models.DateTimeField(auto_now_add=True)
    message = models.TextField(_("HTML message"))

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = _("Carousel Image")
        verbose_name_plural = _("Carousel Images")
Пример #8
0
class Slide(AbstractPromotion):
    """
    Slide for bxslider
    """
    _type = 'Slide'
    name = models.CharField(_("Name"), max_length=128)
    image = models.ImageField(_('Image'),
                              upload_to=settings.OSCAR_PROMOTION_FOLDER,
                              max_length=255)
    link_url = ExtendedURLField(
        _('Link URL'),
        blank=True,
        help_text=_('This is where this promotion links to'))
    body = models.TextField(_("BxSlider text block in HTML"))
    date_created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

    class Meta:
        app_label = 'promotions'
        verbose_name = _("Slide")
        verbose_name_plural = _("Slide")
Пример #9
0
class ConditionalOffer(models.Model):
    """
    A conditional offer (eg buy 1, get 10% off)
    """
    name = models.CharField(
        max_length=128,
        unique=True,
        help_text="""This is displayed within the customer's
                            basket""")
    slug = models.SlugField(max_length=128, unique=True, null=True)
    description = models.TextField(blank=True, null=True)

    # Offers come in a few different types:
    # (a) Offers that are available to all customers on the site.  Eg a
    #     3-for-2 offer.
    # (b) Offers that are linked to a voucher, and only become available once
    #     that voucher has been applied to the basket
    # (c) Offers that are linked to a user.  Eg, all students get 10% off.  The code
    #     to apply this offer needs to be coded
    # (d) Session offers - these are temporarily available to a user after some trigger
    #     event.  Eg, users coming from some affiliate site get 10% off.
    SITE, VOUCHER, USER, SESSION = ("Site", "Voucher", "User", "Session")
    TYPE_CHOICES = (
        (SITE, "Site offer - available to all users"),
        (VOUCHER,
         "Voucher offer - only available after entering the appropriate voucher code"
         ),
        (USER, "User offer - available to certain types of user"),
        (SESSION,
         "Session offer - temporary offer, available for a user for the duration of their session"
         ),
    )
    offer_type = models.CharField(_("Type"),
                                  choices=TYPE_CHOICES,
                                  default=SITE,
                                  max_length=128)

    condition = models.ForeignKey('offer.Condition')
    benefit = models.ForeignKey('offer.Benefit')

    # Range of availability.  Note that if this is a voucher offer, then these
    # dates are ignored and only the dates from the voucher are used to determine
    # availability.
    start_date = models.DateField(blank=True, null=True)
    end_date = models.DateField(blank=True,
                                null=True,
                                help_text="""Offers are not active on their end
                                date, only the days preceding""")

    # Some complicated situations require offers to be applied in a set order.
    priority = models.IntegerField(
        default=0, help_text="The highest priority offers are applied first")

    # We track some information on usage
    total_discount = models.DecimalField(decimal_places=2,
                                         max_digits=12,
                                         default=Decimal('0.00'))
    num_orders = models.PositiveIntegerField(default=0)

    date_created = models.DateTimeField(auto_now_add=True)

    objects = models.Manager()
    active = ActiveOfferManager()

    redirect_url = ExtendedURLField(_('URL redirect (optional)'), blank=True)

    # We need to track the voucher that this offer came from (if it is a voucher offer)
    _voucher = None

    class Meta:
        ordering = ['-priority']

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        return super(ConditionalOffer, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('offer:detail', kwargs={'slug': self.slug})

    def __unicode__(self):
        return self.name

    def clean(self):
        if self.start_date and self.end_date and self.start_date > self.end_date:
            raise exceptions.ValidationError(
                'End date should be later than start date')

    def is_active(self, test_date=None):
        if not test_date:
            test_date = datetime.date.today()
        return self.start_date <= test_date and test_date < self.end_date

    def is_condition_satisfied(self, basket):
        return self._proxy_condition().is_satisfied(basket)

    def is_condition_partially_satisfied(self, basket):
        return self._proxy_condition().is_partially_satisfied(basket)

    def get_upsell_message(self, basket):
        return self._proxy_condition().get_upsell_message(basket)

    def apply_benefit(self, basket):
        """
        Applies the benefit to the given basket and returns the discount.
        """
        if not self.is_condition_satisfied(basket):
            return Decimal('0.00')
        return self._proxy_benefit().apply(basket, self._proxy_condition())

    def set_voucher(self, voucher):
        self._voucher = voucher

    def get_voucher(self):
        return self._voucher

    def _proxy_condition(self):
        """
        Returns the appropriate proxy model for the condition
        """
        field_dict = dict(self.condition.__dict__)
        if '_state' in field_dict:
            del field_dict['_state']
        if '_range_cache' in field_dict:
            del field_dict['_range_cache']
        if self.condition.type == self.condition.COUNT:
            return CountCondition(**field_dict)
        elif self.condition.type == self.condition.VALUE:
            return ValueCondition(**field_dict)
        elif self.condition.type == self.condition.COVERAGE:
            return CoverageCondition(**field_dict)
        return self.condition

    def _proxy_benefit(self):
        """
        Returns the appropriate proxy model for the condition
        """
        field_dict = dict(self.benefit.__dict__)
        if '_state' in field_dict:
            del field_dict['_state']
        if self.benefit.type == self.benefit.PERCENTAGE:
            return PercentageDiscountBenefit(**field_dict)
        elif self.benefit.type == self.benefit.FIXED:
            return AbsoluteDiscountBenefit(**field_dict)
        elif self.benefit.type == self.benefit.MULTIBUY:
            return MultibuyDiscountBenefit(**field_dict)
        elif self.benefit.type == self.benefit.FIXED_PRICE:
            return FixedPriceBenefit(**field_dict)
        return self.benefit

    def record_usage(self, discount):
        self.num_orders += 1
        self.total_discount += discount
        self.save()
Пример #10
0
class ConditionalOffer(models.Model):
    """
    A conditional offer (eg buy 1, get 10% off)
    """
    name = models.CharField(
        _("Name"),
        max_length=128,
        unique=True,
        help_text=_("This is displayed within the customer's basket"))
    slug = models.SlugField(_("Slug"), max_length=128, unique=True, null=True)
    description = models.TextField(
        _("Description"),
        blank=True,
        null=True,
        help_text=_("This is displayed on the offer browsing page"))

    # Offers come in a few different types:
    # (a) Offers that are available to all customers on the site.  Eg a
    #     3-for-2 offer.
    # (b) Offers that are linked to a voucher, and only become available once
    #     that voucher has been applied to the basket
    # (c) Offers that are linked to a user.  Eg, all students get 10% off.  The
    #     code to apply this offer needs to be coded
    # (d) Session offers - these are temporarily available to a user after some
    #     trigger event.  Eg, users coming from some affiliate site get 10%
    #     off.
    SITE, VOUCHER, USER, SESSION = ("Site", "Voucher", "User", "Session")
    TYPE_CHOICES = (
        (SITE, _("Site offer - available to all users")),
        (VOUCHER,
         _("Voucher offer - only available after entering "
           "the appropriate voucher code")),
        (USER, _("User offer - available to certain types of user")),
        (SESSION,
         _("Session offer - temporary offer, available for "
           "a user for the duration of their session")),
    )
    offer_type = models.CharField(_("Type"),
                                  choices=TYPE_CHOICES,
                                  default=SITE,
                                  max_length=128)

    # We track a status variable so it's easier to load offers that are
    # 'available' in some sense.
    OPEN, SUSPENDED, CONSUMED = "Open", "Suspended", "Consumed"
    status = models.CharField(_("Status"), max_length=64, default=OPEN)

    condition = models.ForeignKey('offer.Condition',
                                  verbose_name=_("Condition"))
    benefit = models.ForeignKey('offer.Benefit', verbose_name=_("Benefit"))

    # Some complicated situations require offers to be applied in a set order.
    priority = models.IntegerField(
        _("Priority"),
        default=0,
        help_text=_("The highest priority offers are applied first"))

    # AVAILABILITY

    # Range of availability.  Note that if this is a voucher offer, then these
    # dates are ignored and only the dates from the voucher are used to
    # determine availability.
    start_datetime = models.DateTimeField(_("Start date"),
                                          blank=True,
                                          null=True)
    end_datetime = models.DateTimeField(
        _("End date"),
        blank=True,
        null=True,
        help_text=_("Offers are active until the end of the 'end date'"))

    # Use this field to limit the number of times this offer can be applied in
    # total.  Note that a single order can apply an offer multiple times so
    # this is not the same as the number of orders that can use it.
    max_global_applications = models.PositiveIntegerField(
        _("Max global applications"),
        help_text=_("The number of times this offer can be used before it "
                    "is unavailable"),
        blank=True,
        null=True)

    # Use this field to limit the number of times this offer can be used by a
    # single user.  This only works for signed-in users - it doesn't really
    # make sense for sites that allow anonymous checkout.
    max_user_applications = models.PositiveIntegerField(
        _("Max user applications"),
        help_text=_("The number of times a single user can use this offer"),
        blank=True,
        null=True)

    # Use this field to limit the number of times this offer can be applied to
    # a basket (and hence a single order).
    max_basket_applications = models.PositiveIntegerField(
        blank=True,
        null=True,
        help_text=_("The number of times this offer can be applied to a "
                    "basket (and order)"))

    # Use this field to limit the amount of discount an offer can lead to.
    # This can be helpful with budgeting.
    max_discount = models.DecimalField(
        _("Max discount"),
        decimal_places=2,
        max_digits=12,
        null=True,
        blank=True,
        help_text=_("When an offer has given more discount to orders "
                    "than this threshold, then the offer becomes "
                    "unavailable"))

    # TRACKING

    total_discount = models.DecimalField(_("Total Discount"),
                                         decimal_places=2,
                                         max_digits=12,
                                         default=D('0.00'))
    num_applications = models.PositiveIntegerField(_("Number of applications"),
                                                   default=0)
    num_orders = models.PositiveIntegerField(_("Number of Orders"), default=0)

    redirect_url = ExtendedURLField(_("URL redirect (optional)"), blank=True)
    date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)

    objects = models.Manager()
    active = ActiveOfferManager()

    # We need to track the voucher that this offer came from (if it is a
    # voucher offer)
    _voucher = None

    class Meta:
        ordering = ['-priority']
        verbose_name = _("Conditional offer")
        verbose_name_plural = _("Conditional offers")

        # The way offers are looked up involves the fields
        # (offer_type, status, start_datetime, end_datetime).  Ideally, you want
        # a DB index that covers these 4 fields (will add support for this in
        # Django 1.5)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)

        # Check to see if consumption thresholds have been broken
        if not self.is_suspended:
            if self.get_max_applications() == 0:
                self.status = self.CONSUMED
            else:
                self.status = self.OPEN

        return super(ConditionalOffer, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('offer:detail', kwargs={'slug': self.slug})

    def __unicode__(self):
        return self.name

    def clean(self):
        if (self.start_datetime and self.end_datetime
                and self.start_datetime > self.end_datetime):
            raise exceptions.ValidationError(
                _('End date should be later than start date'))

    @property
    def is_open(self):
        return self.status == self.OPEN

    @property
    def is_suspended(self):
        return self.status == self.SUSPENDED

    def suspend(self):
        self.status = self.SUSPENDED
        self.save()

    suspend.alters_data = True

    def unsuspend(self):
        self.status = self.OPEN
        self.save()

    suspend.alters_data = True

    def is_available(self, user=None, test_date=None):
        """
        Test whether this offer is available to be used
        """
        if self.is_suspended:
            return False
        if test_date is None:
            test_date = now()
        predicates = []
        if self.start_datetime:
            predicates.append(self.start_datetime > test_date)
        if self.end_datetime:
            predicates.append(test_date > self.end_datetime)
        if any(predicates):
            return 0
        return self.get_max_applications(user) > 0

    def is_condition_satisfied(self, basket):
        return self.condition.proxy().is_satisfied(basket)

    def is_condition_partially_satisfied(self, basket):
        return self.condition.proxy().is_partially_satisfied(basket)

    def get_upsell_message(self, basket):
        return self.condition.proxy().get_upsell_message(basket)

    def apply_benefit(self, basket):
        """
        Applies the benefit to the given basket and returns the discount.
        """
        if not self.is_condition_satisfied(basket):
            return D('0.00')
        return self.benefit.proxy().apply(basket, self.condition.proxy(), self)

    def set_voucher(self, voucher):
        self._voucher = voucher

    def get_voucher(self):
        return self._voucher

    def get_max_applications(self, user=None):
        """
        Return the number of times this offer can be applied to a basket for a
        given user.
        """
        if self.max_discount and self.total_discount >= self.max_discount:
            return 0

        # Hard-code a maximum value as we need some sensible upper limit for
        # when there are not other caps.
        limits = [10000]
        if self.max_user_applications and user:
            limits.append(
                max(
                    0, self.max_user_applications -
                    self.get_num_user_applications(user)))
        if self.max_basket_applications:
            limits.append(self.max_basket_applications)
        if self.max_global_applications:
            limits.append(
                max(0, self.max_global_applications - self.num_applications))
        return min(limits)

    def get_num_user_applications(self, user):
        OrderDiscount = models.get_model('order', 'OrderDiscount')
        aggregates = OrderDiscount.objects.filter(
            offer_id=self.id,
            order__user=user).aggregate(total=models.Sum('frequency'))
        return aggregates['total'] if aggregates['total'] is not None else 0

    def shipping_discount(self, charge):
        return self.benefit.proxy().shipping_discount(charge)

    def record_usage(self, discount):
        self.num_applications += discount['freq']
        self.total_discount += discount['discount']
        self.num_orders += 1
        self.save()

    record_usage.alters_data = True

    def availability_description(self):
        """
        Return a description of when this offer is available
        """
        restrictions = self.availability_restrictions()
        descriptions = [r['description'] for r in restrictions]
        return "<br/>".join(descriptions)

    def availability_restrictions(self):
        restrictions = []
        if self.is_suspended:
            restrictions.append({
                'description': _("Offer is suspended"),
                'is_satisfied': False
            })

        if self.max_global_applications:
            remaining = self.max_global_applications - self.num_applications
            desc = _("Limited to %(total)d uses "
                     "(%(remainder)d remaining)") % {
                         'total': self.max_global_applications,
                         'remainder': remaining
                     }
            restrictions.append({
                'description': desc,
                'is_satisfied': remaining > 0
            })

        if self.max_user_applications:
            if self.max_user_applications == 1:
                desc = _("Limited to 1 use per user")
            else:
                desc = _("Limited to %(total)d uses per user") % {
                    'total': self.max_user_applications
                }
            restrictions.append({'description': desc, 'is_satisfied': True})

        if self.max_basket_applications:
            if self.max_user_applications == 1:
                desc = _("Limited to 1 use per basket")
            else:
                desc = _("Limited to %(total)d uses per basket") % {
                    'total': self.max_basket_applications
                }
            restrictions.append({'description': desc, 'is_satisfied': True})

        def format_datetime(dt):
            # Only show hours/minutes if they have been specified
            if dt.hour == 0 and dt.minute == 0:
                return date(dt, settings.DATE_FORMAT)
            return date(dt, settings.DATETIME_FORMAT)

        if self.start_datetime or self.end_datetime:
            today = now()
            if self.start_datetime and self.end_datetime:
                desc = _("Available between %(start)s and %(end)s") % {
                    'start': format_datetime(self.start_datetime),
                    'end': format_datetime(self.end_datetime)
                }
                is_satisfied = self.start_datetime <= today <= self.end_datetime
            elif self.start_datetime:
                desc = _("Available from %(start)s") % {
                    'start': format_datetime(self.start_datetime)
                }
                is_satisfied = today >= self.start_datetime
            elif self.end_datetime:
                desc = _("Available until %(end)s") % {
                    'end': format_datetime(self.end_datetime)
                }
                is_satisfied = today <= self.end_datetime
            restrictions.append({
                'description': desc,
                'is_satisfied': is_satisfied
            })

        if self.max_discount:
            desc = _("Limited to a cost of %(max)s") % {
                'max': currency(self.max_discount)
            }
            restrictions.append({
                'description':
                desc,
                'is_satisfied':
                self.total_discount < self.max_discount
            })

        return restrictions
Пример #11
0
class ConditionalOffer(models.Model):
    """
    A conditional offer (eg buy 1, get 10% off)
    """
    name = models.CharField(
        _("Name"),
        max_length=128,
        unique=True,
        help_text=_("This is displayed within the customer's basket"))
    slug = models.SlugField(_("Slug"), max_length=128, unique=True, null=True)
    description = models.TextField(_("Description"), blank=True, null=True)

    # Offers come in a few different types:
    # (a) Offers that are available to all customers on the site.  Eg a
    #     3-for-2 offer.
    # (b) Offers that are linked to a voucher, and only become available once
    #     that voucher has been applied to the basket
    # (c) Offers that are linked to a user.  Eg, all students get 10% off.  The
    #     code to apply this offer needs to be coded
    # (d) Session offers - these are temporarily available to a user after some
    #     trigger event.  Eg, users coming from some affiliate site get 10% off.
    SITE, VOUCHER, USER, SESSION = ("Site", "Voucher", "User", "Session")
    TYPE_CHOICES = (
        (SITE, _("Site offer - available to all users")),
        (VOUCHER,
         _("Voucher offer - only available after entering the appropriate voucher code"
           )),
        (USER, _("User offer - available to certain types of user")),
        (SESSION,
         _("Session offer - temporary offer, available for a user for the duration of their session"
           )),
    )
    offer_type = models.CharField(_("Type"),
                                  choices=TYPE_CHOICES,
                                  default=SITE,
                                  max_length=128)

    condition = models.ForeignKey('offer.Condition',
                                  verbose_name=_("Condition"))
    benefit = models.ForeignKey('offer.Benefit', verbose_name=_("Benefit"))

    # Some complicated situations require offers to be applied in a set order.
    priority = models.IntegerField(
        _("Priority"),
        default=0,
        help_text=_("The highest priority offers are applied first"))

    # AVAILABILITY

    # Range of availability.  Note that if this is a voucher offer, then these
    # dates are ignored and only the dates from the voucher are used to
    # determine availability.
    start_date = models.DateField(_("Start Date"), blank=True, null=True)
    end_date = models.DateField(
        _("End Date"),
        blank=True,
        null=True,
        help_text=_("Offers are not active on their end date, only "
                    "the days preceding"))

    # Use this field to limit the number of times this offer can be applied in
    # total.  Note that a single order can apply an offer multiple times so
    # this is not the same as the number of orders that can use it.
    max_global_applications = models.PositiveIntegerField(
        _("Max global applications"),
        help_text=_("The number of times this offer can be used before it "
                    "is unavailable"),
        blank=True,
        null=True)

    # Use this field to limit the number of times this offer can be used by a
    # single user.  This only works for signed-in users - it doesn't really
    # make sense for sites that allow anonymous checkout.
    max_user_applications = models.PositiveIntegerField(
        _("Max user applications"),
        help_text=_("The number of times a single user can use this offer"),
        blank=True,
        null=True)

    # Use this field to limit the number of times this offer can be applied to
    # a basket (and hence a single order).
    max_basket_applications = models.PositiveIntegerField(
        blank=True,
        null=True,
        help_text=_("The number of times this offer can be applied to a "
                    "basket (and order)"))

    # Use this field to limit the amount of discount an offer can lead to.
    # This can be helpful with budgeting.
    max_discount = models.DecimalField(
        _("Max discount"),
        decimal_places=2,
        max_digits=12,
        null=True,
        blank=True,
        help_text=_("When an offer has given more discount to orders "
                    "than this threshold, then the offer becomes "
                    "unavailable"))

    # TRACKING

    total_discount = models.DecimalField(_("Total Discount"),
                                         decimal_places=2,
                                         max_digits=12,
                                         default=D('0.00'))
    num_applications = models.PositiveIntegerField(_("Number of applications"),
                                                   default=0)
    num_orders = models.PositiveIntegerField(_("Number of Orders"), default=0)

    redirect_url = ExtendedURLField(_("URL redirect (optional)"), blank=True)
    date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)

    objects = models.Manager()
    active = ActiveOfferManager()

    # We need to track the voucher that this offer came from (if it is a
    # voucher offer)
    _voucher = None

    class Meta:
        ordering = ['-priority']
        verbose_name = _("Conditional Offer")
        verbose_name_plural = _("Conditional Offers")

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        return super(ConditionalOffer, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('offer:detail', kwargs={'slug': self.slug})

    def __unicode__(self):
        return self.name

    def clean(self):
        if self.start_date and self.end_date and self.start_date > self.end_date:
            raise exceptions.ValidationError(
                _('End date should be later than start date'))

    def is_active(self, test_date=None):
        """
        Test whether this offer is active and can be used by customers
        """
        if test_date is None:
            test_date = datetime.date.today()
        predicates = [self.get_max_applications() > 0]
        if self.start_date:
            predicates.append(self.start_date <= test_date)
        if self.end_date:
            predicates.append(test_date < self.end_date)
        if self.max_discount:
            predicates.append(self.total_discount < self.max_discount)
        return all(predicates)

    def is_condition_satisfied(self, basket):
        return self._proxy_condition().is_satisfied(basket)

    def is_condition_partially_satisfied(self, basket):
        return self._proxy_condition().is_partially_satisfied(basket)

    def get_upsell_message(self, basket):
        return self._proxy_condition().get_upsell_message(basket)

    def apply_benefit(self, basket):
        """
        Applies the benefit to the given basket and returns the discount.
        """
        if not self.is_condition_satisfied(basket):
            return D('0.00')
        return self._proxy_benefit().apply(basket, self._proxy_condition(),
                                           self)

    def set_voucher(self, voucher):
        self._voucher = voucher

    def get_voucher(self):
        return self._voucher

    def get_max_applications(self, user=None):
        """
        Return the number of times this offer can be applied to a basket
        """
        limits = [10000]
        if self.max_user_applications and user:
            limits.append(
                max(
                    0, self.max_user_applications -
                    self.get_num_user_applications(user)))
        if self.max_basket_applications:
            limits.append(self.max_basket_applications)
        if self.max_global_applications:
            limits.append(
                max(0, self.max_global_applications - self.num_applications))
        return min(limits)

    def get_num_user_applications(self, user):
        OrderDiscount = models.get_model('order', 'OrderDiscount')
        aggregates = OrderDiscount.objects.filter(
            offer_id=self.id,
            order__user=user).aggregate(total=models.Sum('frequency'))
        return aggregates['total'] if aggregates['total'] is not None else 0

    def shipping_discount(self, charge):
        return self._proxy_benefit().shipping_discount(charge)

    def _proxy_condition(self):
        """
        Returns the appropriate proxy model for the condition
        """
        field_dict = dict(self.condition.__dict__)
        for field in field_dict.keys():
            if field.startswith('_'):
                del field_dict[field]

        if self.condition.proxy_class:
            klass = load_proxy(self.condition.proxy_class)
            return klass(**field_dict)
        klassmap = {
            self.condition.COUNT: CountCondition,
            self.condition.VALUE: ValueCondition,
            self.condition.COVERAGE: CoverageCondition
        }
        if self.condition.type in klassmap:
            return klassmap[self.condition.type](**field_dict)
        return self.condition

    def _proxy_benefit(self):
        """
        Returns the appropriate proxy model for the benefit
        """
        field_dict = dict(self.benefit.__dict__)
        for field in field_dict.keys():
            if field.startswith('_'):
                del field_dict[field]

        klassmap = {
            self.benefit.PERCENTAGE: PercentageDiscountBenefit,
            self.benefit.FIXED: AbsoluteDiscountBenefit,
            self.benefit.MULTIBUY: MultibuyDiscountBenefit,
            self.benefit.FIXED_PRICE: FixedPriceBenefit,
            self.benefit.SHIPPING_ABSOLUTE: ShippingAbsoluteDiscountBenefit,
            self.benefit.SHIPPING_FIXED_PRICE: ShippingFixedPriceBenefit,
            self.benefit.SHIPPING_PERCENTAGE: ShippingPercentageDiscountBenefit
        }
        if self.benefit.type in klassmap:
            return klassmap[self.benefit.type](**field_dict)
        return self.benefit

    def record_usage(self, discount):
        self.num_applications += discount['freq']
        self.total_discount += discount['discount']
        self.num_orders += 1
        self.save()

    record_usage.alters_data = True

    def availability_description(self):
        """
        Return a description of when this offer is available
        """
        sentences = []
        if self.max_global_applications:
            desc = _("Can be used %(total)d times "
                     "(%(remainder)d remaining)") % {
                         'total': self.max_global_applications,
                         'remainder':
                         self.max_global_applications - self.num_applications
                     }
            sentences.append(desc)
        if self.max_user_applications:
            if self.max_user_applications == 1:
                desc = _("Can be used once per user")
            else:
                desc = _("Can be used %(total)d times per user") % {
                    'total': self.max_user_applications
                }
            sentences.append(desc)
        if self.max_basket_applications:
            if self.max_user_applications == 1:
                desc = _("Can be used once per basket")
            else:
                desc = _("Can be used %(total)d times per basket") % {
                    'total': self.max_basket_applications
                }
            sentences.append(desc)
        if self.start_date and self.end_date:
            desc = _("Available between %(start)s and %(end)s") % {
                'start': self.start_date,
                'end': self.end_date
            }
            sentences.append(desc)
        elif self.start_date:
            sentences.append(
                _("Available until %(start)s") % {'start': self.start_date})
        elif self.end_date:
            sentences.append(
                _("Available until %(end)s") % {'end': self.end_date})
        if self.max_discount:
            sentences.append(
                _("Available until a discount of %(max)s "
                  "has been awarded") % {'max': currency(self.max_discount)})
        return "<br/>".join(sentences)