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,
}