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:
|
def count_blocking_vouchers(self) -> int:
|
||||||
from pretix.base.models import Voucher
|
from pretix.base.models import Voucher
|
||||||
return Voucher.objects.filter(
|
return Voucher.objects.filter(
|
||||||
Q(item__quotas__in=[self]) &
|
|
||||||
Q(block_quota=True) &
|
Q(block_quota=True) &
|
||||||
Q(redeemed=False) &
|
Q(redeemed=False) &
|
||||||
self._position_lookup
|
Q(Q(self._position_lookup) | Q(quota=self))
|
||||||
).distinct().count()
|
).distinct().count()
|
||||||
|
|
||||||
def count_in_cart(self) -> int:
|
def count_in_cart(self) -> int:
|
||||||
@@ -546,8 +545,8 @@ class Quota(LoggedModel):
|
|||||||
def _position_lookup(self) -> Q:
|
def _position_lookup(self) -> Q:
|
||||||
return (
|
return (
|
||||||
( # Orders for items which do not have any variations
|
( # Orders for items which do not have any variations
|
||||||
Q(variation__isnull=True)
|
Q(variation__isnull=True) &
|
||||||
& Q(item__quotas__in=[self])
|
Q(item__quotas__in=[self])
|
||||||
) | ( # Orders for items which do have any variations
|
) | ( # Orders for items which do have any variations
|
||||||
Q(variation__quotas__in=[self])
|
Q(variation__quotas__in=[self])
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import random
|
import random
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .base import LoggedModel
|
from .base import LoggedModel
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .items import Item, ItemVariation
|
from .items import Item, ItemVariation, Quota
|
||||||
from .orders import CartPosition, OrderPosition
|
from .orders import CartPosition, OrderPosition
|
||||||
|
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ class Voucher(LoggedModel):
|
|||||||
item = models.ForeignKey(
|
item = models.ForeignKey(
|
||||||
Item, related_name='vouchers',
|
Item, related_name='vouchers',
|
||||||
verbose_name=_("Product"),
|
verbose_name=_("Product"),
|
||||||
|
null=True, blank=True,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"This product is added to the user's cart if the voucher is redeemed."
|
"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."
|
"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:
|
class Meta:
|
||||||
verbose_name = _("Voucher")
|
verbose_name = _("Voucher")
|
||||||
@@ -80,6 +91,21 @@ class Voucher(LoggedModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.code
|
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):
|
def save(self, *args, **kwargs):
|
||||||
self.code = self.code.upper()
|
self.code = self.code.upper()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ error_messages = {
|
|||||||
'ended': _('The presale period has ended.'),
|
'ended': _('The presale period has ended.'),
|
||||||
'voucher_invalid': _('This voucher code is not known in our database.'),
|
'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_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)
|
).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()
|
positions = set()
|
||||||
# For items that are already expired, we have to delete and re-add them, as they might
|
# 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!
|
# 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())
|
Q(cart_id=cart_id) & Q(event=event) & Q(expires__lte=now())
|
||||||
)
|
)
|
||||||
for cp in expired:
|
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)
|
positions.add(cp)
|
||||||
return positions
|
return positions
|
||||||
|
|
||||||
@@ -70,17 +78,17 @@ def _check_date(event: Event) -> None:
|
|||||||
raise CartError(error_messages['ended'])
|
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]:
|
cart_id: str, expiry: datetime) -> Optional[str]:
|
||||||
err = None
|
err = None
|
||||||
|
|
||||||
# Fetch items from the database
|
# 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")
|
"quotas")
|
||||||
items_cache = {i.id: i for i in items_query}
|
items_cache = {i.id: i for i in items_query}
|
||||||
variations_query = ItemVariation.objects.filter(
|
variations_query = ItemVariation.objects.filter(
|
||||||
item__event=event,
|
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")
|
).select_related("item", "item__event").prefetch_related("quotas")
|
||||||
variations_cache = {v.id: v for v in variations_query}
|
variations_cache = {v.id: v for v in variations_query}
|
||||||
|
|
||||||
@@ -88,46 +96,75 @@ 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
|
# 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
|
# If they are not, the user supplied item IDs which either do not exist or belong to
|
||||||
# a different event
|
# 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']
|
err = err or error_messages['not_for_sale']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item = items_cache[i[0]]
|
item = items_cache[i['item']]
|
||||||
variation = variations_cache[i[1]] if i[1] is not None else None
|
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.
|
# 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())
|
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():
|
if len(quotas) == 0 or not item.is_available():
|
||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check that all quotas allow us to buy i[2] instances of the object
|
# Check that all quotas allow us to buy i['count'] instances of the object
|
||||||
quota_ok = i[2]
|
quota_ok = i['count']
|
||||||
for quota in quotas:
|
if not voucher or (not voucher.allow_ignore_quota and not voucher.block_quota):
|
||||||
avail = quota.availability()
|
for quota in quotas:
|
||||||
if avail[1] is not None and avail[1] < i[2]:
|
avail = quota.availability()
|
||||||
# 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']:
|
||||||
# reduce the number of bought items
|
# This quota is not available or less than i['count'] items are left, so we have to
|
||||||
if avail[0] != Quota.AVAILABILITY_OK:
|
# reduce the number of bought items
|
||||||
err = err or error_messages['unavailable']
|
if avail[0] != Quota.AVAILABILITY_OK:
|
||||||
else:
|
err = err or error_messages['unavailable']
|
||||||
err = err or error_messages['in_part']
|
else:
|
||||||
quota_ok = min(quota_ok, avail[1])
|
err = err or error_messages['in_part']
|
||||||
|
quota_ok = min(quota_ok, avail[1])
|
||||||
|
|
||||||
price = item.default_price if variation is None else (
|
if voucher and voucher.price is not None:
|
||||||
variation.default_price if variation.default_price is not None else item.default_price)
|
price = voucher.price
|
||||||
if item.free_price and len(i) > 3 and i[3]:
|
else:
|
||||||
custom_price = i[3]
|
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 'price' in i and i['price'] is not None and i['price'] != "":
|
||||||
|
custom_price = i['price']
|
||||||
if not isinstance(custom_price, Decimal):
|
if not isinstance(custom_price, Decimal):
|
||||||
custom_price = Decimal(custom_price.replace(",", "."))
|
custom_price = Decimal(custom_price.replace(",", "."))
|
||||||
price = max(custom_price, price)
|
price = max(custom_price, price)
|
||||||
|
|
||||||
# Create a CartPosition for as much items as we can
|
# Create a CartPosition for as much items as we can
|
||||||
for k in range(quota_ok):
|
for k in range(quota_ok):
|
||||||
if len(i) > 4 and i[2] == 1:
|
if 'cp' in i and i['count'] == 1:
|
||||||
# Recreating
|
# Recreating
|
||||||
cp = i[4]
|
cp = i['cp']
|
||||||
cp.expires = expiry
|
cp.expires = expiry
|
||||||
cp.price = price
|
cp.price = price
|
||||||
cp.save()
|
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,
|
event=event, item=item, variation=variation,
|
||||||
price=price,
|
price=price,
|
||||||
expires=expiry,
|
expires=expiry,
|
||||||
cart_id=cart_id
|
cart_id=cart_id, voucher=voucher
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
|
|
||||||
|
|
||||||
def _add_voucher(event: Event, voucher: str, expiry: datetime, cart_id: str):
|
def _add_items_to_cart(event: Event, items: List[dict], cart_id: str=None) -> None:
|
||||||
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:
|
|
||||||
with event.lock():
|
with event.lock():
|
||||||
_check_date(event)
|
_check_date(event)
|
||||||
existing = CartPosition.objects.filter(Q(cart_id=cart_id) & Q(event=event)).count()
|
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
|
# TODO: i18n plurals
|
||||||
raise CartError(error_messages['max_items'], (event.settings.max_items_per_order,))
|
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)
|
_delete_expired(expired)
|
||||||
if err:
|
if err:
|
||||||
raise CartError(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,
|
def add_items_to_cart(event: int, items: List[dict], cart_id: str=None) -> None:
|
||||||
voucher: str=None) -> None:
|
|
||||||
"""
|
"""
|
||||||
Adds a list of items to a user's cart.
|
Adds a list of items to a user's cart.
|
||||||
:param event: The event ID in question
|
: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 session: Session ID of a guest
|
||||||
:param coupon: A coupon that should also be reeemed
|
:param coupon: A coupon that should also be reeemed
|
||||||
:raises CartError: On any error that occured
|
:raises CartError: On any error that occured
|
||||||
"""
|
"""
|
||||||
event = Event.objects.get(id=event)
|
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:
|
except EventLock.LockTimeoutException:
|
||||||
raise CartError(error_messages['busy'])
|
raise CartError(error_messages['busy'])
|
||||||
|
|
||||||
|
|
||||||
def _remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]],
|
def _remove_items_from_cart(event: Event, items: List[dict], cart_id: str) -> None:
|
||||||
cart_id: str) -> None:
|
|
||||||
with event.lock():
|
with event.lock():
|
||||||
for item, variation, cnt, price in items:
|
for i in items:
|
||||||
cw = Q(cart_id=cart_id) & Q(item_id=item) & Q(event=event)
|
cw = Q(cart_id=cart_id) & Q(item_id=i['item']) & Q(event=event)
|
||||||
if variation:
|
if i['variation']:
|
||||||
cw &= Q(variation_id=variation)
|
cw &= Q(variation_id=i['variation'])
|
||||||
else:
|
else:
|
||||||
cw &= Q(variation__isnull=True)
|
cw &= Q(variation__isnull=True)
|
||||||
# Prefer to delete positions that have the same price as the one the user clicked on, after thet
|
# Prefer to delete positions that have the same price as the one the user clicked on, after thet
|
||||||
# prefer the most expensive ones.
|
# prefer the most expensive ones.
|
||||||
if price:
|
cnt = i['count']
|
||||||
correctprice = CartPosition.objects.filter(cw).filter(price=Decimal(price.replace(",", ".")))[:cnt]
|
if i['price']:
|
||||||
|
correctprice = CartPosition.objects.filter(cw).filter(price=Decimal(i['price'].replace(",", ".")))[:cnt]
|
||||||
for cp in correctprice:
|
for cp in correctprice:
|
||||||
cp.delete()
|
cp.delete()
|
||||||
cnt -= len(correctprice)
|
cnt -= len(correctprice)
|
||||||
@@ -231,8 +234,7 @@ def _remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], in
|
|||||||
cp.delete()
|
cp.delete()
|
||||||
|
|
||||||
|
|
||||||
def remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]],
|
def remove_items_from_cart(event: int, items: List[dict], cart_id: str=None) -> None:
|
||||||
cart_id: str=None) -> None:
|
|
||||||
"""
|
"""
|
||||||
Removes a list of items from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
@@ -250,20 +252,18 @@ if settings.HAS_CELERY:
|
|||||||
from pretix.celery import app
|
from pretix.celery import app
|
||||||
|
|
||||||
@app.task(bind=True, max_retries=5, default_retry_delay=1)
|
@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]]],
|
def add_items_to_cart_task(self, event: int, items: List[dict], cart_id: str):
|
||||||
cart_id: str, voucher: str=None):
|
|
||||||
event = Event.objects.get(id=event)
|
event = Event.objects.get(id=event)
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
_add_items_to_cart(event, items, cart_id, voucher)
|
_add_items_to_cart(event, items, cart_id)
|
||||||
except EventLock.LockTimeoutException:
|
except EventLock.LockTimeoutException:
|
||||||
self.retry(exc=CartError(error_messages['busy']))
|
self.retry(exc=CartError(error_messages['busy']))
|
||||||
except CartError as e:
|
except CartError as e:
|
||||||
return e
|
return e
|
||||||
|
|
||||||
@app.task(bind=True, max_retries=5, default_retry_delay=1)
|
@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]],
|
def remove_items_from_cart_task(self, event: int, items: List[dict], cart_id: str):
|
||||||
cart_id: str):
|
|
||||||
event = Event.objects.get(id=event)
|
event = Event.objects.get(id=event)
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]):
|
|||||||
err = None
|
err = None
|
||||||
_check_date(event)
|
_check_date(event)
|
||||||
|
|
||||||
|
voucherids = set()
|
||||||
for i, cp in enumerate(positions):
|
for i, cp in enumerate(positions):
|
||||||
if not cp.item.active:
|
if not cp.item.active:
|
||||||
err = err or error_messages['unavailable']
|
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())
|
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
|
||||||
|
|
||||||
if cp.voucher:
|
if cp.voucher:
|
||||||
if cp.voucher.redeemed:
|
if cp.voucher.redeemed or cp.voucher_id in voucherids:
|
||||||
err = err or error_messages['voucher_redeemed']
|
err = err or error_messages['voucher_redeemed']
|
||||||
|
cp.delete() # Sorry! But you should have never gotten into this state at all.
|
||||||
continue
|
continue
|
||||||
|
voucherids.add(cp.voucher_id)
|
||||||
|
|
||||||
if cp.expires >= dt:
|
if cp.expires >= dt and not cp.voucher:
|
||||||
# Other checks are not necessary
|
# Other checks are not necessary
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -183,7 +186,7 @@ def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if cp.voucher:
|
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']
|
err = err or error_messages['voucher_expired']
|
||||||
continue
|
continue
|
||||||
if cp.voucher.price is not None:
|
if cp.voucher.price is not None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from django import forms
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.forms import I18nModelForm
|
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):
|
class VoucherForm(I18nModelForm):
|
||||||
@@ -29,6 +29,8 @@ class VoucherForm(I18nModelForm):
|
|||||||
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
|
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
|
||||||
elif instance.item:
|
elif instance.item:
|
||||||
initial['itemvar'] = str(instance.item.pk)
|
initial['itemvar'] = str(instance.item.pk)
|
||||||
|
elif instance.quota:
|
||||||
|
initial['itemvar'] = 'q-%d' % instance.quota.pk
|
||||||
except Item.DoesNotExist:
|
except Item.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -36,22 +38,39 @@ class VoucherForm(I18nModelForm):
|
|||||||
for i in self.instance.event.items.prefetch_related('variations').all():
|
for i in self.instance.event.items.prefetch_related('variations').all():
|
||||||
variations = list(i.variations.all())
|
variations = list(i.variations.all())
|
||||||
if variations:
|
if variations:
|
||||||
|
choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name)))
|
||||||
for v in variations:
|
for v in variations:
|
||||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
||||||
else:
|
else:
|
||||||
choices.append((str(i.pk), i.name))
|
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
|
self.fields['itemvar'].choices = choices
|
||||||
|
|
||||||
def save(self, commit=True):
|
def clean(self):
|
||||||
if '-' in self.cleaned_data['itemvar']:
|
data = super().clean()
|
||||||
itemid, varid = self.cleaned_data['itemvar'].split('-')
|
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:
|
else:
|
||||||
itemid, varid = self.cleaned_data['itemvar'], None
|
itemid, varid = self.data['itemvar'], None
|
||||||
self.instance.item = Item.objects.get(pk=itemid, event=self.instance.event)
|
|
||||||
if varid:
|
if itemid:
|
||||||
self.instance.variation = ItemVariation.objects.get(pk=varid, item=self.instance.item)
|
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:
|
else:
|
||||||
|
self.instance.quota = Quota.objects.get(pk=quotaid, event=self.instance.event)
|
||||||
|
self.instance.item = None
|
||||||
self.instance.variation = None
|
self.instance.variation = None
|
||||||
|
return data
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
super().save(commit)
|
super().save(commit)
|
||||||
|
|
||||||
return ['item']
|
return ['item']
|
||||||
|
|||||||
@@ -27,7 +27,15 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{% if v.redeemed %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
|
<td>{% if v.redeemed %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
|
||||||
<td>{{ v.valid_until|date }}</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">
|
<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>
|
<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>
|
</td>
|
||||||
|
|||||||
@@ -189,27 +189,6 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if event.presale_is_running %}
|
{% 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="row-fluid checkout-button-row">
|
||||||
<div class="col-md-4 col-md-offset-8 col-xs-12">
|
<div class="col-md-4 col-md-offset-8 col-xs-12">
|
||||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||||
@@ -221,4 +200,24 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% 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 %}
|
{% 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/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'^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'^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(),
|
url(r'^checkout/(?P<step>[^/]+)/$', pretix.presale.views.checkout.CheckoutView.as_view(),
|
||||||
name='event.checkout'),
|
name='event.checkout'),
|
||||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
|
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.contrib import messages
|
||||||
|
from django.db.models import Count, Q
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext as _
|
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 (
|
from pretix.base.services.cart import (
|
||||||
CartError, add_items_to_cart, remove_items_from_cart,
|
CartError, add_items_to_cart, remove_items_from_cart,
|
||||||
)
|
)
|
||||||
from pretix.multidomain.urlreverse import eventreverse
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
from pretix.presale.views import EventViewMixin
|
from pretix.presale.views import EventViewMixin
|
||||||
from pretix.presale.views.async import AsyncAction
|
from pretix.presale.views.async import AsyncAction
|
||||||
|
from pretix.presale.views.event import item_group_by_category
|
||||||
|
|
||||||
|
|
||||||
class CartActionMixin:
|
class CartActionMixin:
|
||||||
@@ -26,30 +30,58 @@ class CartActionMixin:
|
|||||||
def get_error_url(self):
|
def get_error_url(self):
|
||||||
return self.get_next_url()
|
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
|
Parses the POST data and returns a list of tuples in the
|
||||||
form (item id, variation id or None, number)
|
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 = []
|
items = []
|
||||||
for key, value in self.request.POST.items():
|
for key, value in req_items:
|
||||||
if value.strip() == '' or '_' not in key:
|
if value.strip() == '' or '_' not in key:
|
||||||
continue
|
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_'):
|
if key.startswith('item_'):
|
||||||
try:
|
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:
|
except ValueError:
|
||||||
messages.error(self.request, _('Please enter numbers only.'))
|
messages.error(self.request, _('Please enter numbers only.'))
|
||||||
return []
|
return []
|
||||||
elif key.startswith('variation_'):
|
elif key.startswith('variation_'):
|
||||||
try:
|
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:
|
except ValueError:
|
||||||
messages.error(self.request, _('Please enter numbers only.'))
|
messages.error(self.request, _('Please enter numbers only.'))
|
||||||
return []
|
return []
|
||||||
if len(items) == 0 and warn:
|
if len(items) == 0:
|
||||||
messages.warning(self.request, _('You did not select any products.'))
|
messages.warning(self.request, _('You did not select any products.'))
|
||||||
return []
|
return []
|
||||||
return items
|
return items
|
||||||
@@ -95,11 +127,9 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
|||||||
return super().get_error_message(exception)
|
return super().get_error_message(exception)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
voucher = self.request.POST.get('voucher')
|
items = self._items_from_post_data()
|
||||||
items = self._items_from_post_data(warn=not voucher)
|
if items:
|
||||||
if items or voucher:
|
return self.do(self.request.event.id, items, self.request.session.session_key)
|
||||||
return self.do(self.request.event.id, items, self.request.session.session_key,
|
|
||||||
voucher)
|
|
||||||
else:
|
else:
|
||||||
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
|
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
@@ -107,3 +137,100 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
|||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
return redirect(self.get_error_url())
|
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.utils.timezone import now
|
||||||
from django.views.generic import TemplateView
|
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):
|
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]
|
items = [item for item in items if len(item.available_variations) > 0 or not item.has_variations]
|
||||||
|
|
||||||
# Regroup those by category
|
# Regroup those by category
|
||||||
context['items_by_category'] = sorted(
|
context['items_by_category'] = item_group_by_category(items)
|
||||||
[
|
|
||||||
# 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)
|
|
||||||
)
|
|
||||||
|
|
||||||
vouchers_exist = self.request.event.get_cache().get('vouchers_exist')
|
vouchers_exist = self.request.event.get_cache().get('vouchers_exist')
|
||||||
if vouchers_exist is None:
|
if vouchers_exist is None:
|
||||||
|
|||||||
@@ -33,6 +33,16 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.radio-box {
|
||||||
|
text-align: center;
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 19px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.voucher-row {
|
.voucher-row {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.utils.timezone import now
|
|||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, CartPosition, Event, Item, ItemCategory, ItemVariation, Order,
|
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
|
from pretix.base.services.orders import mark_order_paid
|
||||||
|
|
||||||
@@ -174,6 +174,42 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
self.quota.save()
|
self.quota.save()
|
||||||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, None))
|
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):
|
class OrderTestCase(BaseQuotaTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
def test_voucher(self):
|
def test_voucher(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, event=self.event)
|
v = Voucher.objects.create(item=self.ticket, event=self.event)
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
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)
|
}, follow=True)
|
||||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||||
self.assertEqual(len(objs), 1)
|
self.assertEqual(len(objs), 1)
|
||||||
@@ -453,17 +453,35 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
def test_voucher_variation(self):
|
def test_voucher_variation(self):
|
||||||
v = Voucher.objects.create(item=self.shirt, variation=self.shirt_red, event=self.event)
|
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), {
|
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)
|
}, follow=True)
|
||||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||||
self.assertEqual(len(objs), 1)
|
self.assertEqual(len(objs), 1)
|
||||||
self.assertEqual(objs[0].item, self.shirt)
|
self.assertEqual(objs[0].item, self.shirt)
|
||||||
self.assertEqual(objs[0].variation, self.shirt_red)
|
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):
|
def test_voucher_price(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event)
|
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), {
|
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)
|
}, follow=True)
|
||||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||||
self.assertEqual(len(objs), 1)
|
self.assertEqual(len(objs), 1)
|
||||||
@@ -474,7 +492,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
def test_voucher_redemed(self):
|
def test_voucher_redemed(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, redeemed=True)
|
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), {
|
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)
|
}, follow=True)
|
||||||
doc = BeautifulSoup(response.rendered_content)
|
doc = BeautifulSoup(response.rendered_content)
|
||||||
self.assertIn('already been used', doc.select('.alert-danger')[0].text)
|
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,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
valid_until=now() - timedelta(days=2))
|
valid_until=now() - timedelta(days=2))
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
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)
|
}, follow=True)
|
||||||
doc = BeautifulSoup(response.rendered_content)
|
doc = BeautifulSoup(response.rendered_content)
|
||||||
self.assertIn('expired', doc.select('.alert-danger')[0].text)
|
self.assertIn('expired', doc.select('.alert-danger')[0].text)
|
||||||
@@ -492,7 +510,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
|
|
||||||
def test_voucher_invalid(self):
|
def test_voucher_invalid(self):
|
||||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
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)
|
}, follow=True)
|
||||||
doc = BeautifulSoup(response.rendered_content)
|
doc = BeautifulSoup(response.rendered_content)
|
||||||
self.assertIn('not known', doc.select('.alert-danger')[0].text)
|
self.assertIn('not known', doc.select('.alert-danger')[0].text)
|
||||||
@@ -503,7 +521,7 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
self.quota_tickets.save()
|
self.quota_tickets.save()
|
||||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event)
|
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), {
|
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)
|
}, follow=True)
|
||||||
doc = BeautifulSoup(response.rendered_content)
|
doc = BeautifulSoup(response.rendered_content)
|
||||||
self.assertIn('no longer available', doc.select('.alert-danger')[0].text)
|
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,
|
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||||
allow_ignore_quota=True)
|
allow_ignore_quota=True)
|
||||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
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)
|
}, follow=True)
|
||||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||||
self.assertEqual(len(objs), 1)
|
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.assertIn('no longer available', doc.select('.alert-danger')[0].text)
|
||||||
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
|
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), {
|
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)
|
}, follow=True)
|
||||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||||
self.assertEqual(len(objs), 1)
|
self.assertEqual(len(objs), 1)
|
||||||
self.assertEqual(objs[0].item, self.ticket)
|
self.assertEqual(objs[0].item, self.ticket)
|
||||||
self.assertIsNone(objs[0].variation)
|
self.assertIsNone(objs[0].variation)
|
||||||
self.assertEqual(objs[0].price, Decimal('12.00'))
|
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(Order.objects.count(), 1)
|
||||||
self.assertEqual(OrderPosition.objects.count(), 1)
|
self.assertEqual(OrderPosition.objects.count(), 1)
|
||||||
self.assertEqual(OrderPosition.objects.first().voucher, v)
|
self.assertEqual(OrderPosition.objects.first().voucher, v)
|
||||||
|
self.assertTrue(Voucher.objects.get(pk=v.pk).redeemed)
|
||||||
|
|
||||||
def test_voucher_price_changed(self):
|
def test_voucher_price_changed(self):
|
||||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
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(Order.objects.count(), 1)
|
||||||
self.assertEqual(OrderPosition.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):
|
def test_confirm_expired_partial(self):
|
||||||
self.quota_tickets.size = 1
|
self.quota_tickets.size = 1
|
||||||
self.quota_tickets.save()
|
self.quota_tickets.save()
|
||||||
|
|||||||
Reference in New Issue
Block a user