forked from CGM_Public/pretix_original
Allow for vouchers that are valid for multiple items
This commit is contained in:
26
src/pretix/base/migrations/0020_auto_20160418_2106.py
Normal file
26
src/pretix/base/migrations/0020_auto_20160418_2106.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.2 on 2016-04-18 21:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0019_auto_20160326_1139'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='quota',
|
||||
field=models.ForeignKey(blank=True, help_text='If enabled, the voucher is valid for any product affected by this quota.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quota', to='pretixbase.Quota', verbose_name='Quota'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='questionanswer',
|
||||
name='options',
|
||||
field=models.ManyToManyField(blank=True, related_name='answers', to='pretixbase.QuestionOption'),
|
||||
),
|
||||
]
|
||||
21
src/pretix/base/migrations/0021_auto_20160418_2117.py
Normal file
21
src/pretix/base/migrations/0021_auto_20160418_2117.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.2 on 2016-04-18 21:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0020_auto_20160418_2106'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='item',
|
||||
field=models.ForeignKey(blank=True, help_text="This product is added to the user's cart if the voucher is redeemed.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='pretixbase.Item', verbose_name='Product'),
|
||||
),
|
||||
]
|
||||
16
src/pretix/base/migrations/0022_merge.py
Normal file
16
src/pretix/base/migrations/0022_merge.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.2 on 2016-04-23 09:44
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0020_auto_20160421_1943'),
|
||||
('pretixbase', '0021_auto_20160418_2117'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
@@ -513,10 +513,9 @@ class Quota(LoggedModel):
|
||||
def count_blocking_vouchers(self) -> int:
|
||||
from pretix.base.models import Voucher
|
||||
return Voucher.objects.filter(
|
||||
Q(item__quotas__in=[self]) &
|
||||
Q(block_quota=True) &
|
||||
Q(redeemed=False) &
|
||||
self._position_lookup
|
||||
Q(Q(self._position_lookup) | Q(quota=self))
|
||||
).distinct().count()
|
||||
|
||||
def count_in_cart(self) -> int:
|
||||
@@ -546,8 +545,8 @@ class Quota(LoggedModel):
|
||||
def _position_lookup(self) -> Q:
|
||||
return (
|
||||
( # Orders for items which do not have any variations
|
||||
Q(variation__isnull=True)
|
||||
& Q(item__quotas__in=[self])
|
||||
Q(variation__isnull=True) &
|
||||
Q(item__quotas__in=[self])
|
||||
) | ( # Orders for items which do have any variations
|
||||
Q(variation__quotas__in=[self])
|
||||
)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import random
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .base import LoggedModel
|
||||
from .event import Event
|
||||
from .items import Item, ItemVariation
|
||||
from .items import Item, ItemVariation, Quota
|
||||
from .orders import CartPosition, OrderPosition
|
||||
|
||||
|
||||
@@ -59,6 +61,7 @@ class Voucher(LoggedModel):
|
||||
item = models.ForeignKey(
|
||||
Item, related_name='vouchers',
|
||||
verbose_name=_("Product"),
|
||||
null=True, blank=True,
|
||||
help_text=_(
|
||||
"This product is added to the user's cart if the voucher is redeemed."
|
||||
)
|
||||
@@ -71,6 +74,14 @@ class Voucher(LoggedModel):
|
||||
"This variation of the product select above is being used."
|
||||
)
|
||||
)
|
||||
quota = models.ForeignKey(
|
||||
Quota, related_name='quota',
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Quota"),
|
||||
help_text=_(
|
||||
"If enabled, the voucher is valid for any product affected by this quota."
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Voucher")
|
||||
@@ -80,6 +91,21 @@ class Voucher(LoggedModel):
|
||||
def __str__(self):
|
||||
return self.code
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.quota:
|
||||
if self.item:
|
||||
raise ValidationError(_('You cannot select a quota and a specific product at the same time.'))
|
||||
elif self.item:
|
||||
if self.variation and (not self.item or not self.item.has_variations):
|
||||
raise ValidationError(_('You cannot select a variation without having selected a product that provides '
|
||||
'variations.'))
|
||||
if self.item.has_variations and not self.variation and self.block_quota:
|
||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
||||
'Otherwise it might be unclear which quotas to block.'))
|
||||
else:
|
||||
raise ValidationError(_('You need to specify either a quota or a product.'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.code = self.code.upper()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@@ -31,7 +31,8 @@ error_messages = {
|
||||
'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_expired': _('This voucher is expired.'),
|
||||
'voucher_invalid_item': _('This voucher is not valid for this item.'),
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +45,7 @@ def _extend_existing(event: Event, cart_id: str, expiry: datetime) -> None:
|
||||
).update(expires=expiry)
|
||||
|
||||
|
||||
def _re_add_expired_positions(items: List[CartPosition], event: Event, cart_id: str) -> List[CartPosition]:
|
||||
def _re_add_expired_positions(items: List[dict], event: Event, cart_id: str) -> List[CartPosition]:
|
||||
positions = set()
|
||||
# For items that are already expired, we have to delete and re-add them, as they might
|
||||
# be no longer available or prices might have changed. Sorry!
|
||||
@@ -52,7 +53,14 @@ def _re_add_expired_positions(items: List[CartPosition], event: Event, cart_id:
|
||||
Q(cart_id=cart_id) & Q(event=event) & Q(expires__lte=now())
|
||||
)
|
||||
for cp in expired:
|
||||
items.insert(0, (cp.item_id, cp.variation_id, 1, cp.price, cp))
|
||||
items.insert(0, {
|
||||
'item': cp.item_id,
|
||||
'variation': cp.variation_id,
|
||||
'count': 1,
|
||||
'price': cp.price,
|
||||
'cp': cp,
|
||||
'voucher': cp.voucher
|
||||
})
|
||||
positions.add(cp)
|
||||
return positions
|
||||
|
||||
@@ -70,17 +78,17 @@ def _check_date(event: Event) -> None:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
|
||||
def _add_new_items(event: Event, items: List[Tuple[int, Optional[int], int, Optional[str]]],
|
||||
def _add_new_items(event: Event, items: List[dict],
|
||||
cart_id: str, expiry: datetime) -> Optional[str]:
|
||||
err = None
|
||||
|
||||
# Fetch items from the database
|
||||
items_query = Item.objects.filter(event=event, id__in=[i[0] for i in items]).prefetch_related(
|
||||
items_query = Item.objects.filter(event=event, id__in=[i['item'] for i in items]).prefetch_related(
|
||||
"quotas")
|
||||
items_cache = {i.id: i for i in items_query}
|
||||
variations_query = ItemVariation.objects.filter(
|
||||
item__event=event,
|
||||
id__in=[i[1] for i in items if i[1] is not None]
|
||||
id__in=[i['variation'] for i in items if i['variation'] is not None]
|
||||
).select_related("item", "item__event").prefetch_related("quotas")
|
||||
variations_cache = {v.id: v for v in variations_query}
|
||||
|
||||
@@ -88,26 +96,51 @@ def _add_new_items(event: Event, items: List[Tuple[int, Optional[int], int, Opti
|
||||
# Check whether the specified items are part of what we just fetched from the database
|
||||
# If they are not, the user supplied item IDs which either do not exist or belong to
|
||||
# a different event
|
||||
if i[0] not in items_cache or (i[1] is not None and i[1] not in variations_cache):
|
||||
if i['item'] not in items_cache or (i['variation'] is not None and i['variation'] not in variations_cache):
|
||||
err = err or error_messages['not_for_sale']
|
||||
continue
|
||||
|
||||
item = items_cache[i[0]]
|
||||
variation = variations_cache[i[1]] if i[1] is not None else None
|
||||
item = items_cache[i['item']]
|
||||
variation = variations_cache[i['variation']] if i['variation'] is not None else None
|
||||
|
||||
# Check whether a voucher has been provided
|
||||
voucher = None
|
||||
if i.get('voucher'):
|
||||
try:
|
||||
voucher = Voucher.objects.get(code=i.get('voucher'), event=event)
|
||||
if voucher.redeemed:
|
||||
return error_messages['voucher_redeemed']
|
||||
if voucher.valid_until is not None and voucher.valid_until < now():
|
||||
return error_messages['voucher_expired']
|
||||
if voucher.item and voucher.item.pk != item.pk:
|
||||
return error_messages['voucher_invalid_item']
|
||||
if voucher.variation and (not variation or variation.pk != voucher.variation.pk):
|
||||
return error_messages['voucher_invalid_item']
|
||||
doubleuse = CartPosition.objects.filter(voucher=voucher, cart_id=cart_id, event=event)
|
||||
if 'cp' in i:
|
||||
doubleuse = doubleuse.exclude(pk=i['cp'].pk)
|
||||
if doubleuse.exists():
|
||||
return error_messages['voucher_redeemed']
|
||||
except Voucher.DoesNotExist:
|
||||
return error_messages['voucher_invalid']
|
||||
|
||||
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
|
||||
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
|
||||
|
||||
if voucher and voucher.quota and voucher.quota.pk not in [q.pk for q in quotas]:
|
||||
return error_messages['voucher_invalid_item']
|
||||
|
||||
if len(quotas) == 0 or not item.is_available():
|
||||
err = err or error_messages['unavailable']
|
||||
continue
|
||||
|
||||
# Check that all quotas allow us to buy i[2] instances of the object
|
||||
quota_ok = i[2]
|
||||
# Check that all quotas allow us to buy i['count'] instances of the object
|
||||
quota_ok = i['count']
|
||||
if not voucher or (not voucher.allow_ignore_quota and not voucher.block_quota):
|
||||
for quota in quotas:
|
||||
avail = quota.availability()
|
||||
if avail[1] is not None and avail[1] < i[2]:
|
||||
# This quota is not available or less than i[2] items are left, so we have to
|
||||
if avail[1] is not None and avail[1] < i['count']:
|
||||
# This quota is not available or less than i['count'] items are left, so we have to
|
||||
# reduce the number of bought items
|
||||
if avail[0] != Quota.AVAILABILITY_OK:
|
||||
err = err or error_messages['unavailable']
|
||||
@@ -115,19 +148,23 @@ def _add_new_items(event: Event, items: List[Tuple[int, Optional[int], int, Opti
|
||||
err = err or error_messages['in_part']
|
||||
quota_ok = min(quota_ok, avail[1])
|
||||
|
||||
if voucher and voucher.price is not None:
|
||||
price = voucher.price
|
||||
else:
|
||||
price = item.default_price if variation is None else (
|
||||
variation.default_price if variation.default_price is not None else item.default_price)
|
||||
if item.free_price and len(i) > 3 and i[3]:
|
||||
custom_price = i[3]
|
||||
|
||||
if item.free_price and 'price' in i and i['price'] is not None and i['price'] != "":
|
||||
custom_price = i['price']
|
||||
if not isinstance(custom_price, Decimal):
|
||||
custom_price = Decimal(custom_price.replace(",", "."))
|
||||
price = max(custom_price, price)
|
||||
|
||||
# Create a CartPosition for as much items as we can
|
||||
for k in range(quota_ok):
|
||||
if len(i) > 4 and i[2] == 1:
|
||||
if 'cp' in i and i['count'] == 1:
|
||||
# Recreating
|
||||
cp = i[4]
|
||||
cp = i['cp']
|
||||
cp.expires = expiry
|
||||
cp.price = price
|
||||
cp.save()
|
||||
@@ -136,44 +173,16 @@ def _add_new_items(event: Event, items: List[Tuple[int, Optional[int], int, Opti
|
||||
event=event, item=item, variation=variation,
|
||||
price=price,
|
||||
expires=expiry,
|
||||
cart_id=cart_id
|
||||
cart_id=cart_id, voucher=voucher
|
||||
)
|
||||
return err
|
||||
|
||||
|
||||
def _add_voucher(event: Event, voucher: str, expiry: datetime, cart_id: str):
|
||||
try:
|
||||
v = Voucher.objects.get(code=voucher, event=event)
|
||||
if v.redeemed:
|
||||
raise CartError(error_messages['voucher_redeemed'])
|
||||
if v.valid_until is not None and v.valid_until < now():
|
||||
raise CartError(error_messages['voucher_expired'])
|
||||
|
||||
quotas = list(v.item.quotas.all())
|
||||
if len(quotas) == 0 or not v.item.is_available():
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
if not v.allow_ignore_quota and not v.block_quota:
|
||||
for quota in quotas:
|
||||
avail = quota.availability()
|
||||
if avail[1] is not None and avail[1] < 1:
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
CartPosition.objects.create(
|
||||
event=event, item=v.item, variation=v.variation,
|
||||
price=v.price if v.price is not None else v.item.default_price,
|
||||
expires=expiry, cart_id=cart_id, voucher=v
|
||||
)
|
||||
except Voucher.DoesNotExist:
|
||||
raise CartError(error_messages['voucher_invalid'])
|
||||
|
||||
|
||||
def _add_items_to_cart(event: Event, items: List[Tuple[int, Optional[int], int, Optional[str]]], cart_id: str=None,
|
||||
voucher: str=None) -> None:
|
||||
def _add_items_to_cart(event: Event, items: List[dict], cart_id: str=None) -> None:
|
||||
with event.lock():
|
||||
_check_date(event)
|
||||
existing = CartPosition.objects.filter(Q(cart_id=cart_id) & Q(event=event)).count()
|
||||
if sum(i[2] for i in items) + existing > int(event.settings.max_items_per_order):
|
||||
if sum(i['count'] for i in items) + existing > int(event.settings.max_items_per_order):
|
||||
# TODO: i18n plurals
|
||||
raise CartError(error_messages['max_items'], (event.settings.max_items_per_order,))
|
||||
|
||||
@@ -186,43 +195,37 @@ def _add_items_to_cart(event: Event, items: List[Tuple[int, Optional[int], int,
|
||||
_delete_expired(expired)
|
||||
if err:
|
||||
raise CartError(err)
|
||||
elif not voucher:
|
||||
raise CartError(error_messages['empty'])
|
||||
|
||||
if voucher:
|
||||
_add_voucher(event, voucher, expiry, cart_id)
|
||||
|
||||
|
||||
def add_items_to_cart(event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]], cart_id: str=None,
|
||||
voucher: str=None) -> None:
|
||||
def add_items_to_cart(event: int, items: List[dict], cart_id: str=None) -> None:
|
||||
"""
|
||||
Adds a list of items to a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param items: A list of tuple of the form (item id, variation id or None, number, custom_price)
|
||||
:param items: A list of tuple of the form (item id, variation id or None, number, custom_price, voucher)
|
||||
:param session: Session ID of a guest
|
||||
:param coupon: A coupon that should also be reeemed
|
||||
:raises CartError: On any error that occured
|
||||
"""
|
||||
event = Event.objects.get(id=event)
|
||||
try:
|
||||
_add_items_to_cart(event, items, cart_id, voucher)
|
||||
_add_items_to_cart(event, items, cart_id)
|
||||
except EventLock.LockTimeoutException:
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
def _remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]],
|
||||
cart_id: str) -> None:
|
||||
def _remove_items_from_cart(event: Event, items: List[dict], cart_id: str) -> None:
|
||||
with event.lock():
|
||||
for item, variation, cnt, price in items:
|
||||
cw = Q(cart_id=cart_id) & Q(item_id=item) & Q(event=event)
|
||||
if variation:
|
||||
cw &= Q(variation_id=variation)
|
||||
for i in items:
|
||||
cw = Q(cart_id=cart_id) & Q(item_id=i['item']) & Q(event=event)
|
||||
if i['variation']:
|
||||
cw &= Q(variation_id=i['variation'])
|
||||
else:
|
||||
cw &= Q(variation__isnull=True)
|
||||
# Prefer to delete positions that have the same price as the one the user clicked on, after thet
|
||||
# prefer the most expensive ones.
|
||||
if price:
|
||||
correctprice = CartPosition.objects.filter(cw).filter(price=Decimal(price.replace(",", ".")))[:cnt]
|
||||
cnt = i['count']
|
||||
if i['price']:
|
||||
correctprice = CartPosition.objects.filter(cw).filter(price=Decimal(i['price'].replace(",", ".")))[:cnt]
|
||||
for cp in correctprice:
|
||||
cp.delete()
|
||||
cnt -= len(correctprice)
|
||||
@@ -231,8 +234,7 @@ def _remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], in
|
||||
cp.delete()
|
||||
|
||||
|
||||
def remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]],
|
||||
cart_id: str=None) -> None:
|
||||
def remove_items_from_cart(event: int, items: List[dict], cart_id: str=None) -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
@@ -250,20 +252,18 @@ if settings.HAS_CELERY:
|
||||
from pretix.celery import app
|
||||
|
||||
@app.task(bind=True, max_retries=5, default_retry_delay=1)
|
||||
def add_items_to_cart_task(self, event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]],
|
||||
cart_id: str, voucher: str=None):
|
||||
def add_items_to_cart_task(self, event: int, items: List[dict], cart_id: str):
|
||||
event = Event.objects.get(id=event)
|
||||
try:
|
||||
try:
|
||||
_add_items_to_cart(event, items, cart_id, voucher)
|
||||
_add_items_to_cart(event, items, cart_id)
|
||||
except EventLock.LockTimeoutException:
|
||||
self.retry(exc=CartError(error_messages['busy']))
|
||||
except CartError as e:
|
||||
return e
|
||||
|
||||
@app.task(bind=True, max_retries=5, default_retry_delay=1)
|
||||
def remove_items_from_cart_task(self, event: int, items: List[Tuple[int, Optional[int], int]],
|
||||
cart_id: str):
|
||||
def remove_items_from_cart_task(self, event: int, items: List[dict], cart_id: str):
|
||||
event = Event.objects.get(id=event)
|
||||
try:
|
||||
try:
|
||||
|
||||
@@ -158,6 +158,7 @@ def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]):
|
||||
err = None
|
||||
_check_date(event)
|
||||
|
||||
voucherids = set()
|
||||
for i, cp in enumerate(positions):
|
||||
if not cp.item.active:
|
||||
err = err or error_messages['unavailable']
|
||||
@@ -166,11 +167,13 @@ def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]):
|
||||
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
|
||||
|
||||
if cp.voucher:
|
||||
if cp.voucher.redeemed:
|
||||
if cp.voucher.redeemed or cp.voucher_id in voucherids:
|
||||
err = err or error_messages['voucher_redeemed']
|
||||
cp.delete() # Sorry! But you should have never gotten into this state at all.
|
||||
continue
|
||||
voucherids.add(cp.voucher_id)
|
||||
|
||||
if cp.expires >= dt:
|
||||
if cp.expires >= dt and not cp.voucher:
|
||||
# Other checks are not necessary
|
||||
continue
|
||||
|
||||
@@ -183,7 +186,7 @@ def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]):
|
||||
continue
|
||||
|
||||
if cp.voucher:
|
||||
if cp.voucher.valid_until < now():
|
||||
if cp.voucher.valid_until and cp.voucher.valid_until < now():
|
||||
err = err or error_messages['voucher_expired']
|
||||
continue
|
||||
if cp.voucher.price is not None:
|
||||
|
||||
@@ -2,7 +2,7 @@ from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Item, ItemVariation, Voucher
|
||||
from pretix.base.models import Item, ItemVariation, Quota, Voucher
|
||||
|
||||
|
||||
class VoucherForm(I18nModelForm):
|
||||
@@ -29,6 +29,8 @@ class VoucherForm(I18nModelForm):
|
||||
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
|
||||
elif instance.item:
|
||||
initial['itemvar'] = str(instance.item.pk)
|
||||
elif instance.quota:
|
||||
initial['itemvar'] = 'q-%d' % instance.quota.pk
|
||||
except Item.DoesNotExist:
|
||||
pass
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -36,22 +38,39 @@ class VoucherForm(I18nModelForm):
|
||||
for i in self.instance.event.items.prefetch_related('variations').all():
|
||||
variations = list(i.variations.all())
|
||||
if variations:
|
||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name)))
|
||||
for v in variations:
|
||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
||||
else:
|
||||
choices.append((str(i.pk), i.name))
|
||||
for q in self.instance.event.quotas.all():
|
||||
choices.append(('q-%d' % q.pk, 'Any product in quota "{quota}"'.format(quota=q)))
|
||||
self.fields['itemvar'].choices = choices
|
||||
|
||||
def save(self, commit=True):
|
||||
if '-' in self.cleaned_data['itemvar']:
|
||||
itemid, varid = self.cleaned_data['itemvar'].split('-')
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
itemid = quotaid = None
|
||||
if self.data['itemvar'].startswith('q-'):
|
||||
quotaid = self.data['itemvar'][2:]
|
||||
elif '-' in self.data['itemvar']:
|
||||
itemid, varid = self.data['itemvar'].split('-')
|
||||
else:
|
||||
itemid, varid = self.cleaned_data['itemvar'], None
|
||||
itemid, varid = self.data['itemvar'], None
|
||||
|
||||
if itemid:
|
||||
self.instance.item = Item.objects.get(pk=itemid, event=self.instance.event)
|
||||
if varid:
|
||||
self.instance.variation = ItemVariation.objects.get(pk=varid, item=self.instance.item)
|
||||
else:
|
||||
self.instance.variation = None
|
||||
self.instance.quota = None
|
||||
else:
|
||||
self.instance.quota = Quota.objects.get(pk=quotaid, event=self.instance.event)
|
||||
self.instance.item = None
|
||||
self.instance.variation = None
|
||||
return data
|
||||
|
||||
def save(self, commit=True):
|
||||
super().save(commit)
|
||||
|
||||
return ['item']
|
||||
|
||||
@@ -27,7 +27,15 @@
|
||||
</td>
|
||||
<td>{% if v.redeemed %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
|
||||
<td>{{ v.valid_until|date }}</td>
|
||||
<td>{{ v.item }}</td>
|
||||
<td>
|
||||
{% if v.item %}
|
||||
{{ v.item }}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with quota=v.quota.name %}
|
||||
Any product in quota "{{ quota }}"
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:event.voucher.delete" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
|
||||
@@ -189,27 +189,6 @@
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% if event.presale_is_running %}
|
||||
{% if vouchers_exist %}
|
||||
<div class="row-fluid voucher-row">
|
||||
<div class="col-md-4 col-md-offset-8 col-xs-12">
|
||||
<div id="voucher-box">
|
||||
<label for="voucher">{% trans "Redeem a voucher" %}</label>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-ticket fa-fw"></i></span>
|
||||
<input type="text" class="form-control" name="voucher" id="voucher"
|
||||
placeholder="{% trans "Voucher code" %}">
|
||||
</div>
|
||||
</div>
|
||||
<div id="voucher-toggle">
|
||||
<a href="javascript:void(0);">
|
||||
<span class="fa fa-ticket"></span> {% trans "Redeem a voucher" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row-fluid checkout-button-row">
|
||||
<div class="col-md-4 col-md-offset-8 col-xs-12">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||
@@ -221,4 +200,24 @@
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if vouchers_exist %}
|
||||
<h2>{% trans "Redeem a voucher" %}</h2>
|
||||
<form method="get" action="{% eventurl event "presale:event.redeem" %}">
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-8 col-sm-6 col-xs-12">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-ticket fa-fw"></i></span>
|
||||
<input type="text" class="form-control" name="voucher" id="voucher"
|
||||
placeholder="{% trans "Voucher code" %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6 col-xs-12">
|
||||
<button class="btn btn-block btn-primary" type="submit">
|
||||
{% trans "Redeem voucher" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
154
src/pretix/presale/templates/pretixpresale/event/voucher.html
Normal file
154
src/pretix/presale/templates/pretixpresale/event/voucher.html
Normal file
@@ -0,0 +1,154 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load thumbnail %}
|
||||
{% block title %}{% trans "Voucher redemption" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{% trans "Voucher redemption" %}</h2>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You entered a voucher code that allows you to buy one of the following products at the specified price:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %}
|
||||
<form method="post" data-asynctask
|
||||
action="{% eventurl request.event "presale:event.cart.add" %}?next={{ request.path|urlencode }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="_voucher_code" value="{{ voucher.code }}">
|
||||
{% for tup in items_by_category %}
|
||||
<section>
|
||||
{% if tup.0 %}<h3>{{ tup.0.name }}</h3>{% endif %}
|
||||
{% for item in tup.1 %}
|
||||
{% if item.has_variations %}
|
||||
<div class="item-with-variations">
|
||||
<div class="row-fluid product-row headline">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name }}"
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumbnail_url:'productlist' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
<strong>{{ item.name }}</strong>
|
||||
{% if item.description %}<p>{{ item.description }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{% if item.min_price != item.max_price or item.free_price %}
|
||||
{% blocktrans trimmed with minprice=item.min_price|floatformat:2 currency=event.currency %}
|
||||
from {{ currency }} {{ minprice }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{{ event.currency }} {{ item.min_price|floatformat:2 }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 availability-box">
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="">
|
||||
{% for var in item.available_variations %}
|
||||
<div class="row-fluid product-row variation">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{{ var }}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price"
|
||||
placeholder="0"
|
||||
min="{{ var.price|stringformat:"0.2f" }}"
|
||||
name="price_{{ item.id }}_{{ var.id }}"
|
||||
step="0.01" value="{{ var.price|stringformat:"0.2f" }}">
|
||||
</div>
|
||||
{% else %}
|
||||
{{ event.currency }} {{ var.price|floatformat:2 }}
|
||||
{% endif %}
|
||||
{% if item.tax_rate %}
|
||||
<small>{% blocktrans trimmed with rate=item.tax_rate %}
|
||||
incl. {{ rate }}% taxes
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if var.cached_availability.0 == 100 %}
|
||||
<div class="col-md-2 col-xs-6 availability-box available radio-box">
|
||||
<label>
|
||||
<input type="radio" name="_voucher_item"
|
||||
{% if options == 1 %}checked="checked"{% endif %}
|
||||
value="variation_{{ item.id }}_{{ var.id }}">
|
||||
</label>
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row-fluid product-row simple">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name }}"
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumbnail_url:'productlist' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
<strong>{{ item.name }}</strong>
|
||||
{% if item.description %}
|
||||
<p class="description">{{ item.description }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price" placeholder="0"
|
||||
min="{{ item.price|stringformat:"0.2f" }}"
|
||||
name="price_{{ item.id }}"
|
||||
step="0.01" value="{{ item.price|stringformat:"0.2f" }}">
|
||||
</div>
|
||||
{% else %}
|
||||
{{ event.currency }} {{ item.price|floatformat:2 }}
|
||||
{% endif %}
|
||||
{% if item.tax_rate %}
|
||||
<small>{% blocktrans trimmed with rate=item.tax_rate %}
|
||||
incl. {{ rate }}% taxes
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if item.cached_availability.0 == 100 %}
|
||||
<div class="col-md-2 col-xs-6 availability-box available radio-box">
|
||||
<label>
|
||||
<input type="radio" name="_voucher_item"
|
||||
{% if options == 1 %}checked="checked"{% endif %}
|
||||
value="item_{{ item.id }}">
|
||||
</label>
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with avail=item.cached_availability.0 %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% if event.presale_is_running %}
|
||||
<div class="row-fluid checkout-button-row">
|
||||
<div class="col-md-4 col-md-offset-8 col-xs-12">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||
<i class="fa fa-shopping-cart"></i> {% trans "Add to cart" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -14,6 +14,8 @@ event_patterns = [
|
||||
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
|
||||
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
|
||||
url(r'^checkout/start$', pretix.presale.views.checkout.CheckoutView.as_view(), name='event.checkout.start'),
|
||||
url(r'^redeem$', pretix.presale.views.cart.RedeemView.as_view(),
|
||||
name='event.redeem'),
|
||||
url(r'^checkout/(?P<step>[^/]+)/$', pretix.presale.views.checkout.CheckoutView.as_view(),
|
||||
name='event.checkout'),
|
||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
from django.contrib import messages
|
||||
from django.db.models import Count, Q
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import View
|
||||
from django.views.generic import TemplateView, View
|
||||
|
||||
from pretix.base.models import Quota, Voucher
|
||||
from pretix.base.services.cart import (
|
||||
CartError, add_items_to_cart, remove_items_from_cart,
|
||||
)
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.views import EventViewMixin
|
||||
from pretix.presale.views.async import AsyncAction
|
||||
from pretix.presale.views.event import item_group_by_category
|
||||
|
||||
|
||||
class CartActionMixin:
|
||||
@@ -26,30 +30,58 @@ class CartActionMixin:
|
||||
def get_error_url(self):
|
||||
return self.get_next_url()
|
||||
|
||||
def _items_from_post_data(self, warn=True):
|
||||
def _items_from_post_data(self):
|
||||
"""
|
||||
Parses the POST data and returns a list of tuples in the
|
||||
form (item id, variation id or None, number)
|
||||
"""
|
||||
|
||||
# Compatibility patch that makes the frontend code a lot easier
|
||||
req_items = list(self.request.POST.items())
|
||||
if '_voucher_item' in self.request.POST and '_voucher_code' in self.request.POST:
|
||||
req_items.append((
|
||||
'%s_voucher' % self.request.POST['_voucher_item'], self.request.POST['_voucher_code']
|
||||
))
|
||||
pass
|
||||
|
||||
items = []
|
||||
for key, value in self.request.POST.items():
|
||||
for key, value in req_items:
|
||||
if value.strip() == '' or '_' not in key:
|
||||
continue
|
||||
|
||||
price = self.request.POST.get('price_' + key.split("_", 1)[1], "")
|
||||
parts = key.split("_")
|
||||
if parts[-1] == "voucher":
|
||||
voucher = value
|
||||
value = 1
|
||||
parts = parts[:-1]
|
||||
else:
|
||||
voucher = None
|
||||
price = self.request.POST.get('price_' + "_".join(parts[1:]), "")
|
||||
if key.startswith('item_'):
|
||||
try:
|
||||
items.append((int(key.split("_")[1]), None, int(value), price))
|
||||
items.append({
|
||||
'item': int(parts[1]),
|
||||
'variation': None,
|
||||
'count': int(value),
|
||||
'price': price,
|
||||
'voucher': voucher
|
||||
})
|
||||
except ValueError:
|
||||
messages.error(self.request, _('Please enter numbers only.'))
|
||||
return []
|
||||
elif key.startswith('variation_'):
|
||||
try:
|
||||
items.append((int(key.split("_")[1]), int(key.split("_")[2]), int(value), price))
|
||||
items.append({
|
||||
'item': int(parts[1]),
|
||||
'variation': int(parts[2]),
|
||||
'count': int(value),
|
||||
'price': price,
|
||||
'voucher': voucher
|
||||
})
|
||||
except ValueError:
|
||||
messages.error(self.request, _('Please enter numbers only.'))
|
||||
return []
|
||||
if len(items) == 0 and warn:
|
||||
if len(items) == 0:
|
||||
messages.warning(self.request, _('You did not select any products.'))
|
||||
return []
|
||||
return items
|
||||
@@ -95,11 +127,9 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
return super().get_error_message(exception)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
voucher = self.request.POST.get('voucher')
|
||||
items = self._items_from_post_data(warn=not voucher)
|
||||
if items or voucher:
|
||||
return self.do(self.request.event.id, items, self.request.session.session_key,
|
||||
voucher)
|
||||
items = self._items_from_post_data()
|
||||
if items:
|
||||
return self.do(self.request.event.id, items, self.request.session.session_key)
|
||||
else:
|
||||
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
|
||||
return JsonResponse({
|
||||
@@ -107,3 +137,100 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
})
|
||||
else:
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
|
||||
class RedeemView(EventViewMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/voucher.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['voucher'] = self.voucher
|
||||
|
||||
# Fetch all items
|
||||
items = self.request.event.items.all().filter(
|
||||
Q(active=True)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
)
|
||||
|
||||
if self.voucher.item_id:
|
||||
items = items.filter(pk=self.voucher.item_id)
|
||||
elif self.voucher.quota_id:
|
||||
items = items.filter(quotas__in=[self.voucher.quota_id])
|
||||
|
||||
items = items.select_related(
|
||||
'category', # for re-grouping
|
||||
).prefetch_related(
|
||||
'quotas', 'variations__quotas', 'quotas__event' # for .availability()
|
||||
).annotate(quotac=Count('quotas')).filter(
|
||||
quotac__gt=0
|
||||
).distinct().order_by('category__position', 'category_id', 'position', 'name')
|
||||
|
||||
for item in items:
|
||||
item.available_variations = list(item.variations.filter(active=True, quotas__isnull=False).distinct())
|
||||
if self.voucher.item_id and self.voucher.variation_id:
|
||||
item.available_variations = [v for v in item.available_variations if v.pk == self.voucher.variation_id]
|
||||
|
||||
item.has_variations = item.variations.exists()
|
||||
if not item.has_variations:
|
||||
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
|
||||
item.cached_availability = (Quota.AVAILABILITY_OK, 1)
|
||||
else:
|
||||
item.cached_availability = item.check_quotas()
|
||||
if self.voucher.price is not None:
|
||||
item.price = self.voucher.price
|
||||
else:
|
||||
item.price = item.default_price
|
||||
else:
|
||||
for var in item.available_variations:
|
||||
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
|
||||
var.cached_availability = (Quota.AVAILABILITY_OK, 1)
|
||||
else:
|
||||
var.cached_availability = list(var.check_quotas())
|
||||
if self.voucher.price is not None:
|
||||
var.price = self.voucher.price
|
||||
else:
|
||||
var.price = var.default_price if var.default_price is not None else item.default_price
|
||||
|
||||
if len(item.available_variations) > 0:
|
||||
item.min_price = min([v.price for v in item.available_variations])
|
||||
item.max_price = max([v.price for v in item.available_variations])
|
||||
|
||||
items = [item for item in items if len(item.available_variations) > 0 or not item.has_variations]
|
||||
context['options'] = sum([(len(item.available_variations) if item.has_variations else 1)
|
||||
for item in items])
|
||||
|
||||
# Regroup those by category
|
||||
context['items_by_category'] = item_group_by_category(items)
|
||||
|
||||
return context
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
from pretix.base.services.cart import error_messages
|
||||
|
||||
err = None
|
||||
v = request.GET.get('voucher')
|
||||
|
||||
if v:
|
||||
try:
|
||||
self.voucher = Voucher.objects.get(code=v, event=request.event)
|
||||
if self.voucher.redeemed:
|
||||
err = error_messages['voucher_redeemed']
|
||||
if self.voucher.valid_until is not None and self.voucher.valid_until < now():
|
||||
err = error_messages['voucher_expired']
|
||||
except Voucher.DoesNotExist:
|
||||
err = error_messages['voucher_invalid']
|
||||
else:
|
||||
return redirect(eventreverse(request.event, 'presale:event.index'))
|
||||
|
||||
if request.event.presale_start and now() < request.event.presale_start:
|
||||
err = error_messages['not_started']
|
||||
if request.event.presale_end and now() > request.event.presale_end:
|
||||
err = error_messages['ended']
|
||||
|
||||
if err:
|
||||
messages.error(request, err)
|
||||
return redirect(eventreverse(request.event, 'presale:event.index'))
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -4,7 +4,21 @@ from django.db.models import Count, Q
|
||||
from django.utils.timezone import now
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from pretix.presale.views import CartMixin, EventViewMixin
|
||||
from . import CartMixin, EventViewMixin
|
||||
|
||||
|
||||
def item_group_by_category(items):
|
||||
return sorted(
|
||||
[
|
||||
# a group is a tuple of a category and a list of items
|
||||
(cat, [i for i in items if i.category == cat])
|
||||
for cat in set([i.category for i in items])
|
||||
# insert categories into a set for uniqueness
|
||||
# a set is unsorted, so sort again by category
|
||||
],
|
||||
key=lambda group: (group[0].position, group[0].id) if (
|
||||
group[0] is not None and group[0].id is not None) else (0, 0)
|
||||
)
|
||||
|
||||
|
||||
class EventIndex(EventViewMixin, CartMixin, TemplateView):
|
||||
@@ -48,17 +62,7 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
|
||||
items = [item for item in items if len(item.available_variations) > 0 or not item.has_variations]
|
||||
|
||||
# Regroup those by category
|
||||
context['items_by_category'] = sorted(
|
||||
[
|
||||
# a group is a tuple of a category and a list of items
|
||||
(cat, [i for i in items if i.category == cat])
|
||||
for cat in set([i.category for i in items])
|
||||
# insert categories into a set for uniqueness
|
||||
# a set is unsorted, so sort again by category
|
||||
],
|
||||
key=lambda group: (group[0].position, group[0].id) if (
|
||||
group[0] is not None and group[0].id is not None) else (0, 0)
|
||||
)
|
||||
context['items_by_category'] = item_group_by_category(items)
|
||||
|
||||
vouchers_exist = self.request.event.get_cache().get('vouchers_exist')
|
||||
if vouchers_exist is None:
|
||||
|
||||
@@ -33,6 +33,16 @@
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.radio-box {
|
||||
text-align: center;
|
||||
label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
line-height: 19px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.voucher-row {
|
||||
margin-top: 10px;
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.utils.timezone import now
|
||||
|
||||
from pretix.base.models import (
|
||||
CachedFile, CartPosition, Event, Item, ItemCategory, ItemVariation, Order,
|
||||
OrderPosition, Organizer, Question, Quota, User,
|
||||
OrderPosition, Organizer, Question, Quota, User, Voucher,
|
||||
)
|
||||
from pretix.base.services.orders import mark_order_paid
|
||||
|
||||
@@ -174,6 +174,42 @@ class QuotaTestCase(BaseQuotaTestCase):
|
||||
self.quota.save()
|
||||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, None))
|
||||
|
||||
def test_voucher_product(self):
|
||||
self.quota.items.add(self.item1)
|
||||
self.quota.size = 1
|
||||
self.quota.save()
|
||||
|
||||
v = Voucher.objects.create(item=self.item1, event=self.event)
|
||||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||
|
||||
v.block_quota = True
|
||||
v.save()
|
||||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||||
|
||||
def test_voucher_variation(self):
|
||||
self.quota.variations.add(self.var1)
|
||||
self.quota.size = 1
|
||||
self.quota.save()
|
||||
|
||||
v = Voucher.objects.create(item=self.item2, variation=self.var1, event=self.event)
|
||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||
|
||||
v.block_quota = True
|
||||
v.save()
|
||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||||
|
||||
def test_voucher_quota(self):
|
||||
self.quota.variations.add(self.var1)
|
||||
self.quota.size = 1
|
||||
self.quota.save()
|
||||
|
||||
v = Voucher.objects.create(quota=self.quota, event=self.event)
|
||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||
|
||||
v.block_quota = True
|
||||
v.save()
|
||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||||
|
||||
|
||||
class OrderTestCase(BaseQuotaTestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -442,7 +442,7 @@ class CartTest(CartTestMixin, TestCase):
|
||||
def test_voucher(self):
|
||||
v = Voucher.objects.create(item=self.ticket, event=self.event)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'voucher': v.code
|
||||
'item_%d_voucher' % self.ticket.id: v.code,
|
||||
}, follow=True)
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
@@ -453,17 +453,35 @@ class CartTest(CartTestMixin, TestCase):
|
||||
def test_voucher_variation(self):
|
||||
v = Voucher.objects.create(item=self.shirt, variation=self.shirt_red, event=self.event)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'voucher': v.code
|
||||
'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code,
|
||||
}, follow=True)
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
self.assertEqual(objs[0].item, self.shirt)
|
||||
self.assertEqual(objs[0].variation, self.shirt_red)
|
||||
|
||||
def test_voucher_quota(self):
|
||||
v = Voucher.objects.create(quota=self.quota_shirts, event=self.event)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code,
|
||||
}, follow=True)
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
self.assertEqual(objs[0].item, self.shirt)
|
||||
self.assertEqual(objs[0].variation, self.shirt_red)
|
||||
|
||||
def test_voucher_quota_invalid_item(self):
|
||||
v = Voucher.objects.create(quota=self.quota_tickets, event=self.event)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code,
|
||||
}, follow=True)
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 0)
|
||||
|
||||
def test_voucher_price(self):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'voucher': v.code
|
||||
'item_%d_voucher' % self.ticket.id: v.code,
|
||||
}, follow=True)
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
@@ -474,7 +492,7 @@ class CartTest(CartTestMixin, TestCase):
|
||||
def test_voucher_redemed(self):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, redeemed=True)
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'voucher': v.code
|
||||
'item_%d_voucher' % self.ticket.id: v.code,
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content)
|
||||
self.assertIn('already been used', doc.select('.alert-danger')[0].text)
|
||||
@@ -484,7 +502,7 @@ class CartTest(CartTestMixin, TestCase):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||
valid_until=now() - timedelta(days=2))
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'voucher': v.code
|
||||
'item_%d_voucher' % self.ticket.id: v.code,
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content)
|
||||
self.assertIn('expired', doc.select('.alert-danger')[0].text)
|
||||
@@ -492,7 +510,7 @@ class CartTest(CartTestMixin, TestCase):
|
||||
|
||||
def test_voucher_invalid(self):
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'voucher': 'ABC'
|
||||
'item_%d_voucher' % self.ticket.id: 'ABC',
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content)
|
||||
self.assertIn('not known', doc.select('.alert-danger')[0].text)
|
||||
@@ -503,7 +521,7 @@ class CartTest(CartTestMixin, TestCase):
|
||||
self.quota_tickets.save()
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event)
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'voucher': v.code
|
||||
'item_%d_voucher' % self.ticket.id: v.code,
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content)
|
||||
self.assertIn('no longer available', doc.select('.alert-danger')[0].text)
|
||||
@@ -515,7 +533,7 @@ class CartTest(CartTestMixin, TestCase):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||
allow_ignore_quota=True)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'voucher': v.code
|
||||
'item_%d_voucher' % self.ticket.id: v.code,
|
||||
}, follow=True)
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
@@ -535,10 +553,28 @@ class CartTest(CartTestMixin, TestCase):
|
||||
self.assertIn('no longer available', doc.select('.alert-danger')[0].text)
|
||||
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'voucher': v.code
|
||||
'item_%d_voucher' % self.ticket.id: v.code,
|
||||
}, follow=True)
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
self.assertEqual(objs[0].item, self.ticket)
|
||||
self.assertIsNone(objs[0].variation)
|
||||
self.assertEqual(objs[0].price, Decimal('12.00'))
|
||||
|
||||
def test_voucher_doubled(self):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event)
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_%d_voucher' % self.ticket.id: v.code,
|
||||
}, follow=True)
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
self.assertEqual(objs[0].item, self.ticket)
|
||||
self.assertIsNone(objs[0].variation)
|
||||
self.assertEqual(objs[0].price, Decimal('12.00'))
|
||||
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_%d_voucher' % self.ticket.id: v.code,
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content)
|
||||
self.assertIn('already been used', doc.select('.alert-danger')[0].text)
|
||||
self.assertEqual(1, CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count())
|
||||
|
||||
@@ -309,6 +309,7 @@ class CheckoutTestCase(TestCase):
|
||||
self.assertEqual(Order.objects.count(), 1)
|
||||
self.assertEqual(OrderPosition.objects.count(), 1)
|
||||
self.assertEqual(OrderPosition.objects.first().voucher, v)
|
||||
self.assertTrue(Voucher.objects.get(pk=v.pk).redeemed)
|
||||
|
||||
def test_voucher_price_changed(self):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||
@@ -392,6 +393,34 @@ class CheckoutTestCase(TestCase):
|
||||
self.assertEqual(Order.objects.count(), 1)
|
||||
self.assertEqual(OrderPosition.objects.count(), 1)
|
||||
|
||||
def test_voucher_double(self):
|
||||
self.quota_tickets.size = 2
|
||||
self.quota_tickets.save()
|
||||
v = Voucher.objects.create(item=self.ticket, event=self.event,
|
||||
valid_until=now() + timedelta(days=2), block_quota=True)
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10), voucher=v
|
||||
)
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10), voucher=v
|
||||
)
|
||||
self._set_session('payment', 'banktransfer')
|
||||
|
||||
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content)
|
||||
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, voucher=v).count(), 1)
|
||||
self.assertEqual(len(doc.select(".alert-danger")), 1)
|
||||
self.assertFalse(Order.objects.exists())
|
||||
|
||||
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content)
|
||||
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, voucher=v).exists())
|
||||
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||
self.assertEqual(Order.objects.count(), 1)
|
||||
self.assertEqual(OrderPosition.objects.count(), 1)
|
||||
|
||||
def test_confirm_expired_partial(self):
|
||||
self.quota_tickets.size = 1
|
||||
self.quota_tickets.save()
|
||||
|
||||
Reference in New Issue
Block a user