forked from CGM_Public/pretix_original
Refs #145 -- Multi-use vouchers
This commit is contained in:
38
src/pretix/base/migrations/0047_auto_20161126_1300.py
Normal file
38
src/pretix/base/migrations/0047_auto_20161126_1300.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2016-11-26 13:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0046_order_meta_info'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='max_usages',
|
||||
field=models.PositiveIntegerField(default=1, help_text='Number of times this voucher can be redeemed.', verbose_name='Maximum usages'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='redeemed',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Redeemed'),
|
||||
),
|
||||
]
|
||||
@@ -4,8 +4,9 @@ from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Tuple
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models import F, Func, Q, Sum
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@@ -577,12 +578,18 @@ class Quota(LoggedModel):
|
||||
from pretix.base.models import Voucher
|
||||
|
||||
now_dt = now_dt or now()
|
||||
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
|
||||
func = 'MAX'
|
||||
else:
|
||||
func = 'GREATEST'
|
||||
|
||||
return Voucher.objects.filter(
|
||||
Q(block_quota=True) &
|
||||
Q(redeemed=False) &
|
||||
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
|
||||
Q(Q(self._position_lookup) | Q(quota=self))
|
||||
).values('id').distinct().count()
|
||||
).values('id').aggregate(
|
||||
free=Sum(Func(F('max_usages') - F('redeemed'), 0, function=func))
|
||||
)['free'] or 0
|
||||
|
||||
def count_in_cart(self, now_dt: datetime=None) -> int:
|
||||
from pretix.base.models import CartPosition
|
||||
@@ -617,9 +624,9 @@ class Quota(LoggedModel):
|
||||
return (
|
||||
( # Orders for items which do not have any variations
|
||||
Q(variation__isnull=True) &
|
||||
Q(item__quotas__in=[self])
|
||||
Q(item__quotas=self)
|
||||
) | ( # Orders for items which do have any variations
|
||||
Q(variation__quotas__in=[self])
|
||||
Q(variation__quotas=self)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import List, Union
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@@ -459,6 +460,8 @@ class OrderPosition(AbstractPosition):
|
||||
|
||||
@classmethod
|
||||
def transform_cart_positions(cls, cp: List, order) -> list:
|
||||
from . import Voucher
|
||||
|
||||
ops = []
|
||||
for cartpos in cp:
|
||||
op = OrderPosition(order=order)
|
||||
@@ -471,8 +474,7 @@ class OrderPosition(AbstractPosition):
|
||||
answ.cartposition = None
|
||||
answ.save()
|
||||
if cartpos.voucher:
|
||||
cartpos.voucher.redeemed = True
|
||||
cartpos.voucher.save()
|
||||
Voucher.objects.filter(pk=cartpos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||
cartpos.delete()
|
||||
return ops
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ class Voucher(LoggedModel):
|
||||
:type event: Event
|
||||
:param code: The secret voucher code
|
||||
:type code: str
|
||||
:param redeemed: Whether or not this voucher has already been redeemed
|
||||
:param max_usages: The number of times this voucher can be redeemed
|
||||
:type max_usages: int
|
||||
:param redeemed: The number of times this voucher already has been redeemed
|
||||
:type redeemed: bool
|
||||
:param valid_until: The expiration date of this voucher (optional)
|
||||
:type valid_until: datetime
|
||||
@@ -68,10 +70,14 @@ class Voucher(LoggedModel):
|
||||
max_length=255, default=generate_code,
|
||||
db_index=True,
|
||||
)
|
||||
redeemed = models.BooleanField(
|
||||
max_usages = models.PositiveIntegerField(
|
||||
verbose_name=_("Maximum usages"),
|
||||
help_text=_("Number of times this voucher can be redeemed."),
|
||||
default=1
|
||||
)
|
||||
redeemed = models.PositiveIntegerField(
|
||||
verbose_name=_("Redeemed"),
|
||||
default=False,
|
||||
db_index=True
|
||||
default=0
|
||||
)
|
||||
valid_until = models.DateTimeField(
|
||||
blank=True, null=True, db_index=True,
|
||||
@@ -197,7 +203,7 @@ class Voucher(LoggedModel):
|
||||
Returns True if a voucher has not yet been redeemed, but is still
|
||||
within its validity (if valid_until is set).
|
||||
"""
|
||||
if self.redeemed:
|
||||
if self.redeemed >= self.max_usages:
|
||||
return False
|
||||
if self.valid_until and self.valid_until < now():
|
||||
return False
|
||||
|
||||
@@ -33,7 +33,8 @@ error_messages = {
|
||||
'ended': _('The presale period has ended.'),
|
||||
'price_too_high': _('The entered price is to high.'),
|
||||
'voucher_invalid': _('This voucher code is not known in our database.'),
|
||||
'voucher_redeemed': _('This voucher code has already been used and can only be used once.'),
|
||||
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'),
|
||||
'voucher_redeemed_partial': _('This voucher code can only be redeemed %d more times.'),
|
||||
'voucher_double': _('You already used this voucher code. Remove the associated line from your '
|
||||
'cart if you want to use it for a different product.'),
|
||||
'voucher_expired': _('This voucher is expired.'),
|
||||
@@ -114,17 +115,26 @@ def _add_new_items(event: Event, items: List[dict],
|
||||
if i.get('voucher'):
|
||||
try:
|
||||
voucher = Voucher.objects.get(code=i.get('voucher').strip(), event=event)
|
||||
if voucher.redeemed:
|
||||
if voucher.redeemed >= voucher.max_usages:
|
||||
return error_messages['voucher_redeemed']
|
||||
if voucher.valid_until is not None and voucher.valid_until < now_dt:
|
||||
return error_messages['voucher_expired']
|
||||
if not voucher.applies_to(item, variation):
|
||||
return error_messages['voucher_invalid_item']
|
||||
doubleuse = CartPosition.objects.filter(voucher=voucher, cart_id=cart_id, event=event)
|
||||
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=voucher) & Q(event=event) &
|
||||
(Q(expires__gte=now_dt) | Q(cart_id=cart_id))
|
||||
)
|
||||
if 'cp' in i:
|
||||
doubleuse = doubleuse.exclude(pk=i['cp'].pk)
|
||||
if doubleuse.exists():
|
||||
return error_messages['voucher_double']
|
||||
redeemed_in_carts = redeemed_in_carts.exclude(pk=i['cp'].pk)
|
||||
v_avail = voucher.max_usages - voucher.redeemed - redeemed_in_carts.count()
|
||||
|
||||
if v_avail < 1:
|
||||
return error_messages['voucher_redeemed']
|
||||
if i['count'] > v_avail:
|
||||
return error_messages['voucher_redeemed_partial'] % v_avail
|
||||
|
||||
except Voucher.DoesNotExist:
|
||||
return error_messages['voucher_invalid']
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import List, Optional
|
||||
import pytz
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import make_aware, now
|
||||
@@ -18,7 +19,7 @@ from pretix.base.i18n import (
|
||||
)
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, Item, ItemVariation, Order, OrderPosition, Quota,
|
||||
User,
|
||||
User, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import InvoiceAddress
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
@@ -46,11 +47,15 @@ error_messages = {
|
||||
'server was too busy. Please try again.'),
|
||||
'not_started': _('The presale period for this event has not yet started.'),
|
||||
'ended': _('The presale period has ended.'),
|
||||
'voucher_invalid': _('This voucher code is not known in our database.'),
|
||||
'voucher_redeemed': _('This voucher code has already been used an can only be used once.'),
|
||||
'voucher_expired': _('This voucher is expired.'),
|
||||
'voucher_invalid_item': _('This voucher is not valid for this item.'),
|
||||
'voucher_required': _('You need a valid voucher code to order one of the products in your cart.'),
|
||||
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'),
|
||||
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum '
|
||||
'number of times allowed. We removed this item from your cart.'),
|
||||
'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item '
|
||||
'from your cart.'),
|
||||
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
|
||||
'removed this item from your cart.'),
|
||||
'voucher_required': _('You need a valid voucher code to order one of the products in your cart. We removed this '
|
||||
'item from your cart.'),
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -163,8 +168,7 @@ def _cancel_order(order, user=None):
|
||||
|
||||
for position in order.positions.all():
|
||||
if position.voucher:
|
||||
position.voucher.redeemed = False
|
||||
position.voucher.save()
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
|
||||
|
||||
return order
|
||||
|
||||
@@ -184,7 +188,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
err = None
|
||||
_check_date(event, now_dt)
|
||||
|
||||
voucherids = set()
|
||||
for i, cp in enumerate(positions):
|
||||
if not cp.item.active or (cp.variation and not cp.variation.active):
|
||||
err = err or error_messages['unavailable']
|
||||
@@ -193,11 +196,14 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
|
||||
|
||||
if cp.voucher:
|
||||
if cp.voucher.redeemed or cp.voucher_id in voucherids:
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
||||
).exclude(pk=cp.pk)
|
||||
v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
|
||||
if v_avail < 1:
|
||||
err = err or error_messages['voucher_redeemed']
|
||||
cp.delete() # Sorry! But you should have never gotten into this state at all.
|
||||
cp.delete() # Sorry!
|
||||
continue
|
||||
voucherids.add(cp.voucher_id)
|
||||
|
||||
if cp.item.require_voucher and cp.voucher is None:
|
||||
cp.delete()
|
||||
@@ -225,6 +231,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
if cp.voucher:
|
||||
if cp.voucher.valid_until and cp.voucher.valid_until < now_dt:
|
||||
err = err or error_messages['voucher_expired']
|
||||
cp.delete()
|
||||
continue
|
||||
if cp.voucher.price is not None:
|
||||
price = cp.voucher.price
|
||||
|
||||
Reference in New Issue
Block a user