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}