Source code for oscar.apps.voucher.abstract_models

from decimal import Decimal

from django.core import exceptions
from django.db import models
from django.db.models import Sum
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from oscar.core.compat import AUTH_USER_MODEL
from oscar.core.decorators import deprecated


[docs]class AbstractVoucherSet(models.Model): """A collection of vouchers (potentially auto-generated) a VoucherSet is a group of voucher that are generated automatically. - count: the number of vouchers in the set. If this is kept at zero, vouchers are created when and as needed. - code_length: the length of the voucher code. Codes are by default created with groups of 4 characters: XXXX-XXXX-XXXX. The dashes (-) do not count for the code_length. - :py:attr:`.start_datetime` and :py:attr:`.end_datetime` together define the validity range for all vouchers in the set. """ name = models.CharField(verbose_name=_("Name"), max_length=100, unique=True) count = models.PositiveIntegerField(verbose_name=_("Number of vouchers")) code_length = models.IntegerField(verbose_name=_("Length of Code"), default=12) description = models.TextField(verbose_name=_("Description")) date_created = models.DateTimeField(auto_now_add=True, db_index=True) start_datetime = models.DateTimeField(_("Start datetime")) end_datetime = models.DateTimeField(_("End datetime")) class Meta: abstract = True app_label = "voucher" get_latest_by = "date_created" ordering = ["-date_created"] verbose_name = _("VoucherSet") verbose_name_plural = _("VoucherSets") def __str__(self): return self.name
[docs] 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") )
def update_count(self): vouchers_count = self.vouchers.count() if self.count != vouchers_count: self.count = vouchers_count self.save()
[docs] def is_active(self, test_datetime=None): """Test whether this voucher set is currently active.""" test_datetime = test_datetime or timezone.now() return self.start_datetime <= test_datetime <= self.end_datetime
@property def num_basket_additions(self): value = self.vouchers.aggregate(result=Sum("num_basket_additions")) return value["result"] @property def num_orders(self): value = self.vouchers.aggregate(result=Sum("num_orders")) return value["result"] @property def total_discount(self): value = self.vouchers.aggregate(result=Sum("total_discount")) return value["result"]
[docs]class AbstractVoucher(models.Model): """ A voucher. This is simply a link to a collection of offers. Note that there are three possible "usage" modes: (a) Single use (b) Multi-use (c) Once per customer Oscar enforces those modes by creating VoucherApplication instances when a voucher is used for an order. """ name = models.CharField( _("Name"), max_length=128, unique=True, help_text=_( "This will be shown in the checkout" " and basket once the voucher is" " entered" ), ) code = models.CharField( _("Code"), max_length=128, db_index=True, unique=True, help_text=_("Case insensitive / No spaces allowed"), ) offers = models.ManyToManyField( "offer.ConditionalOffer", related_name="vouchers", verbose_name=_("Offers"), limit_choices_to={"offer_type": "Voucher"}, ) SINGLE_USE, MULTI_USE, ONCE_PER_CUSTOMER = ( "Single use", "Multi-use", "Once per customer", ) USAGE_CHOICES = ( (SINGLE_USE, _("Can be used once by one customer")), (MULTI_USE, _("Can be used multiple times by multiple customers")), (ONCE_PER_CUSTOMER, _("Can only be used once per customer")), ) usage = models.CharField( _("Usage"), max_length=128, choices=USAGE_CHOICES, default=MULTI_USE ) start_datetime = models.DateTimeField(_("Start datetime"), db_index=True) end_datetime = models.DateTimeField(_("End datetime"), db_index=True) # Reporting information. Not used to enforce any consumption limits. num_basket_additions = models.PositiveIntegerField( _("Times added to basket"), default=0 ) num_orders = models.PositiveIntegerField(_("Times on orders"), default=0) total_discount = models.DecimalField( _("Total discount"), decimal_places=2, max_digits=12, default=Decimal("0.00") ) voucher_set = models.ForeignKey( "voucher.VoucherSet", null=True, blank=True, related_name="vouchers", on_delete=models.CASCADE, ) date_created = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: abstract = True app_label = "voucher" ordering = ["-date_created"] get_latest_by = "date_created" verbose_name = _("Voucher") verbose_name_plural = _("Vouchers") def __str__(self): return self.name
[docs] 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") )
[docs] def save(self, *args, **kwargs): self.code = self.code.upper() super().save(*args, **kwargs)
[docs] def is_active(self, test_datetime=None): """ Test whether this voucher is currently active. """ test_datetime = test_datetime or timezone.now() return self.start_datetime <= test_datetime <= self.end_datetime
[docs] def is_expired(self): """ Test whether this voucher has passed its expiration date """ now = timezone.now() return self.end_datetime < now
[docs] def is_available_to_user(self, user=None): """ Test whether this voucher is available to the passed user. Returns a tuple of a boolean for whether it is successful, and a availability message. """ is_available, message = False, "" if self.usage == self.SINGLE_USE: is_available = not self.applications.exists() if not is_available: message = _("This voucher has already been used") elif self.usage == self.MULTI_USE: is_available = True elif self.usage == self.ONCE_PER_CUSTOMER: if not user.is_authenticated: is_available = False message = _("This voucher is only available to signed in users") else: is_available = not self.applications.filter( voucher=self, user=user ).exists() if not is_available: message = _( "You have already used this voucher in a previous order" ) return is_available, message
[docs] def is_available_for_basket(self, basket): """ Tests whether this voucher is available to the passed basket. Returns a tuple of a boolean for whether it is successful, and a availability message. """ is_available, message = self.is_available_to_user(user=basket.owner) if not is_available: return False, message is_available, message = False, _( "This voucher is not available for this basket" ) for offer in self.offers.all(): if offer.is_condition_satisfied(basket=basket): is_available = True message = "" break return is_available, message
[docs] def record_usage(self, order, user): """ Records a usage of this voucher in an order. """ if user.is_authenticated: self.applications.create(voucher=self, order=order, user=user) else: self.applications.create(voucher=self, order=order) self.num_orders += 1 self.save()
record_usage.alters_data = True
[docs] def record_discount(self, discount): """ Record a discount that this offer has given """ self.total_discount += discount["discount"] self.save()
record_discount.alters_data = True @property @deprecated def benefit(self): """ Returns the first offer's benefit instance. A voucher is commonly only linked to one offer. In that case, this helper can be used for convenience. """ return self.offers.first().benefit
[docs]class AbstractVoucherApplication(models.Model): """ For tracking how often a voucher has been used in an order. This is used to enforce the voucher usage mode in Voucher.is_available_to_user, and created in Voucher.record_usage. """ voucher = models.ForeignKey( "voucher.Voucher", on_delete=models.CASCADE, related_name="applications", verbose_name=_("Voucher"), ) # It is possible for an anonymous user to apply a voucher so we need to # allow the user to be nullable user = models.ForeignKey( AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_("User"), ) order = models.ForeignKey( "order.Order", on_delete=models.CASCADE, verbose_name=_("Order") ) date_created = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: abstract = True app_label = "voucher" ordering = ["-date_created"] verbose_name = _("Voucher Application") verbose_name_plural = _("Voucher Applications") def __str__(self): return _("'%(voucher)s' used by '%(user)s'") % { "voucher": self.voucher, "user": self.user, }