forked from CGM_Public/pretix_original
Compare commits
9 Commits
language-s
...
shorter-lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0b73bf19 | ||
|
|
ed31f31c04 | ||
|
|
b1e78b5b78 | ||
|
|
4e2d31154a | ||
|
|
2e5a598b5f | ||
|
|
4b535b067a | ||
|
|
4f6eb903c7 | ||
|
|
4d916df7c0 | ||
|
|
61a331493e |
@@ -279,12 +279,13 @@ class Order(LoggedModel):
|
||||
|
||||
if now() > last_date:
|
||||
return error_messages['late']
|
||||
if self.status == self.STATUS_PENDING:
|
||||
return True
|
||||
if not self.event.settings.get('payment_term_accept_late'):
|
||||
return error_messages['late']
|
||||
|
||||
return self._is_still_available()
|
||||
if self.status == self.STATUS_PENDING:
|
||||
return True
|
||||
else:
|
||||
return self._is_still_available()
|
||||
|
||||
def _is_still_available(self, now_dt: datetime=None) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from collections import Counter
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.i18n import LazyLocaleException
|
||||
@@ -35,8 +37,6 @@ error_messages = {
|
||||
'voucher_invalid': _('This voucher code is not known in our database.'),
|
||||
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'),
|
||||
'voucher_redeemed_partial': _('This voucher code can only be redeemed %d more times.'),
|
||||
'voucher_double': _('You already used this voucher code. Remove the associated line from your '
|
||||
'cart if you want to use it for a different product.'),
|
||||
'voucher_expired': _('This voucher is expired.'),
|
||||
'voucher_invalid_item': _('This voucher is not valid for this product.'),
|
||||
'voucher_required': _('You need a valid voucher code to order this product.'),
|
||||
@@ -65,7 +65,7 @@ def _re_add_expired_positions(items: List[dict], event: Event, cart_id: str, now
|
||||
'variation': cp.variation_id,
|
||||
'count': 1,
|
||||
'price': cp.price,
|
||||
'cp': cp,
|
||||
'_cp': cp,
|
||||
'voucher': cp.voucher.code if cp.voucher else None
|
||||
})
|
||||
positions.add(cp)
|
||||
@@ -74,7 +74,7 @@ def _re_add_expired_positions(items: List[dict], event: Event, cart_id: str, now
|
||||
|
||||
def _delete_expired(expired: List[CartPosition], now_dt: datetime) -> None:
|
||||
for cp in expired:
|
||||
if cp.expires <= now_dt:
|
||||
if cp.expires <= now_dt: # Has not been extended
|
||||
cp.delete()
|
||||
|
||||
|
||||
@@ -85,8 +85,17 @@ def _check_date(event: Event, now_dt: datetime) -> None:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
|
||||
def _add_new_items(event: Event, items: List[dict],
|
||||
cart_id: str, expiry: datetime, now_dt: datetime) -> Optional[str]:
|
||||
def _parse_items_and_check_constraints(event: Event, items: List[dict], cart_id: str,
|
||||
now_dt: datetime) -> Counter:
|
||||
"""
|
||||
This method does three things:
|
||||
|
||||
* Extend the item list with the database objects for the item, variation, etc.
|
||||
|
||||
* Check all constraints that are placed on the items, vouchers etc. to be valid and calculates the correct prices
|
||||
|
||||
* Return a counter object that contains the quota changes that are required to perform the operation
|
||||
"""
|
||||
err = None
|
||||
|
||||
# Fetch items from the database
|
||||
@@ -99,6 +108,9 @@ def _add_new_items(event: Event, items: List[dict],
|
||||
).select_related("item", "item__event").prefetch_related("quotas")
|
||||
variations_cache = {v.id: v for v in variations_query}
|
||||
|
||||
quotadiff = Counter()
|
||||
vouchers = Counter()
|
||||
|
||||
for i in items:
|
||||
# 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
|
||||
@@ -116,58 +128,44 @@ def _add_new_items(event: Event, items: List[dict],
|
||||
try:
|
||||
voucher = Voucher.objects.get(code=i.get('voucher').strip(), event=event)
|
||||
if voucher.redeemed >= voucher.max_usages:
|
||||
return error_messages['voucher_redeemed']
|
||||
raise CartError(error_messages['voucher_redeemed'])
|
||||
if voucher.valid_until is not None and voucher.valid_until < now_dt:
|
||||
return error_messages['voucher_expired']
|
||||
raise CartError(error_messages['voucher_expired'])
|
||||
if not voucher.applies_to(item, variation):
|
||||
return error_messages['voucher_invalid_item']
|
||||
raise CartError(error_messages['voucher_invalid_item'])
|
||||
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=voucher) & Q(event=event) &
|
||||
(Q(expires__gte=now_dt) | Q(cart_id=cart_id))
|
||||
Q(voucher=voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
||||
)
|
||||
if 'cp' in i:
|
||||
redeemed_in_carts = redeemed_in_carts.exclude(pk=i['cp'].pk)
|
||||
redeemed_in_carts = redeemed_in_carts.exclude(pk=i['_cp'].pk)
|
||||
v_avail = voucher.max_usages - voucher.redeemed - redeemed_in_carts.count()
|
||||
|
||||
if v_avail < 1:
|
||||
return error_messages['voucher_redeemed']
|
||||
if i['count'] > v_avail:
|
||||
return error_messages['voucher_redeemed_partial'] % v_avail
|
||||
raise CartError(error_messages['voucher_redeemed'])
|
||||
if i['count'] > v_avail - vouchers[voucher]:
|
||||
raise CartError(error_messages['voucher_redeemed_partial'] % v_avail)
|
||||
|
||||
vouchers[voucher] += i['count']
|
||||
except Voucher.DoesNotExist:
|
||||
return error_messages['voucher_invalid']
|
||||
raise CartError(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']
|
||||
raise CartError(error_messages['voucher_invalid_item'])
|
||||
|
||||
if item.require_voucher and voucher is None:
|
||||
return error_messages['voucher_required']
|
||||
raise CartError(error_messages['voucher_required'])
|
||||
|
||||
if item.hide_without_voucher and (voucher is None or voucher.item is None or voucher.item.pk != item.pk):
|
||||
return error_messages['voucher_required']
|
||||
raise CartError(error_messages['voucher_required'])
|
||||
|
||||
if len(quotas) == 0 or not item.is_available() or (variation and not variation.active):
|
||||
err = err or error_messages['unavailable']
|
||||
continue
|
||||
|
||||
# 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['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']
|
||||
else:
|
||||
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:
|
||||
@@ -179,44 +177,110 @@ def _add_new_items(event: Event, items: List[dict],
|
||||
if not isinstance(custom_price, Decimal):
|
||||
custom_price = Decimal(custom_price.replace(",", "."))
|
||||
if custom_price > 100000000:
|
||||
return error_messages['price_too_high']
|
||||
raise CartError(error_messages['price_too_high'])
|
||||
price = max(custom_price, price)
|
||||
|
||||
# Create a CartPosition for as much items as we can
|
||||
for k in range(quota_ok):
|
||||
if 'cp' in i and i['count'] == 1:
|
||||
# Recreating
|
||||
cp = i['cp']
|
||||
cp.expires = expiry
|
||||
cp.price = price
|
||||
cp.save()
|
||||
# Check that all quotas allow us to buy i['count'] instances of the object
|
||||
if not voucher or (not voucher.allow_ignore_quota and not voucher.block_quota):
|
||||
for quota in quotas:
|
||||
quotadiff[quota] += i['count']
|
||||
i['_quotas'] = quotas
|
||||
else:
|
||||
i['_quotas'] = []
|
||||
|
||||
i['_price'] = price
|
||||
i['_item'] = item
|
||||
i['_variation'] = variation
|
||||
i['_voucher'] = voucher
|
||||
|
||||
if err:
|
||||
raise CartError(err)
|
||||
|
||||
return quotadiff
|
||||
|
||||
|
||||
def _check_quota_and_create_positions(event: Event, items: List[dict], cart_id: str, now_dt: datetime,
|
||||
expiry: datetime, quotadiff: Counter):
|
||||
"""
|
||||
This method takes the modified items and the quotadiff from _parse_items_and_check_constraints
|
||||
and then
|
||||
|
||||
* checks that the given quotas are available
|
||||
|
||||
* creates as many cart positions as possible
|
||||
"""
|
||||
err = None
|
||||
quotas_ok = {}
|
||||
cartpositions = []
|
||||
|
||||
with event.lock():
|
||||
for quota, count in quotadiff.items():
|
||||
avail = quota.availability(now_dt)
|
||||
if avail[1] is not None and avail[1] < 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']
|
||||
else:
|
||||
err = err or error_messages['in_part']
|
||||
quotas_ok[quota] = min(count, avail[1])
|
||||
else:
|
||||
CartPosition.objects.create(
|
||||
event=event, item=item, variation=variation,
|
||||
price=price,
|
||||
expires=expiry,
|
||||
cart_id=cart_id, voucher=voucher
|
||||
)
|
||||
return err
|
||||
quotas_ok[quota] = count
|
||||
|
||||
for i in items:
|
||||
# Create a CartPosition for as much items as we can
|
||||
requested_count = i['count']
|
||||
available_count = requested_count
|
||||
if i['_quotas']:
|
||||
available_count = min(requested_count, min(quotas_ok[q] for q in i['_quotas']))
|
||||
|
||||
for q in i['_quotas']:
|
||||
quotas_ok[q] -= available_count
|
||||
|
||||
for k in range(available_count):
|
||||
if '_cp' in i and i['count'] == 1:
|
||||
# Recreating an existing position
|
||||
cp = i['_cp']
|
||||
cp.expires = expiry
|
||||
cp.price = i['_price']
|
||||
cp.save()
|
||||
else:
|
||||
cartpositions.append(CartPosition(
|
||||
event=event, item=i['_item'], variation=i['_variation'],
|
||||
price=i['_price'],
|
||||
expires=expiry,
|
||||
cart_id=cart_id, voucher=i['_voucher']
|
||||
))
|
||||
|
||||
CartPosition.objects.bulk_create(cartpositions)
|
||||
|
||||
if err:
|
||||
raise CartError(err)
|
||||
|
||||
|
||||
def _add_items_to_cart(event: Event, items: List[dict], cart_id: str=None) -> None:
|
||||
with event.lock() as now_dt:
|
||||
_check_date(event, now_dt)
|
||||
existing = CartPosition.objects.filter(Q(cart_id=cart_id) & Q(event=event)).count()
|
||||
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,))
|
||||
now_dt = now()
|
||||
_check_date(event, now_dt)
|
||||
|
||||
expiry = now_dt + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
|
||||
_extend_existing(event, cart_id, expiry, now_dt)
|
||||
existing = CartPosition.objects.filter(Q(cart_id=cart_id) & Q(event=event)).count()
|
||||
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,))
|
||||
|
||||
expired = _re_add_expired_positions(items, event, cart_id, now_dt)
|
||||
expiry = now_dt + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
|
||||
_extend_existing(event, cart_id, expiry, now_dt)
|
||||
|
||||
expired = _re_add_expired_positions(items, event, cart_id, now_dt)
|
||||
|
||||
try:
|
||||
if items:
|
||||
err = _add_new_items(event, items, cart_id, expiry, now_dt)
|
||||
_delete_expired(expired, now_dt)
|
||||
if err:
|
||||
raise CartError(err)
|
||||
quotadiff = _parse_items_and_check_constraints(event, items, cart_id, now_dt)
|
||||
_check_quota_and_create_positions(event, items, cart_id, now_dt, expiry, quotadiff)
|
||||
except CartError as e:
|
||||
_delete_expired(expired, now_dt)
|
||||
raise e
|
||||
else:
|
||||
_delete_expired(expired, now_dt)
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
from collections import Counter, namedtuple
|
||||
@@ -50,6 +51,8 @@ error_messages = {
|
||||
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'),
|
||||
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum '
|
||||
'number of times allowed. We removed this item from your cart.'),
|
||||
'voucher_redeemed_partial': _('The voucher code used for one of the items in your cart can only be redeemed %d '
|
||||
'more times. We removed this item from your cart.'),
|
||||
'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item '
|
||||
'from your cart.'),
|
||||
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
|
||||
@@ -82,7 +85,15 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
|
||||
:param user: The user that performed the change
|
||||
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
|
||||
"""
|
||||
with order.event.lock() as now_dt:
|
||||
lock_func = order.event.lock
|
||||
if order.status == order.STATUS_PENDING and order.expires > now() + timedelta(minutes=10):
|
||||
# No lock necessary in this case. The 10 minute offset is just to be safe and prevent
|
||||
# collisions with the cronjob.
|
||||
@contextlib.contextmanager
|
||||
def lock_func():
|
||||
yield now()
|
||||
|
||||
with lock_func() as now_dt:
|
||||
can_be_paid = order._can_be_paid()
|
||||
if not force and can_be_paid is not True:
|
||||
raise Quota.QuotaExceededException(can_be_paid)
|
||||
@@ -185,6 +196,9 @@ def _check_date(event: Event, now_dt: datetime):
|
||||
|
||||
|
||||
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition]):
|
||||
"""
|
||||
Checks constraints on all positions except quota
|
||||
"""
|
||||
err = None
|
||||
_check_date(event, now_dt)
|
||||
|
||||
@@ -193,17 +207,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
err = err or error_messages['unavailable']
|
||||
cp.delete()
|
||||
continue
|
||||
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
|
||||
|
||||
if cp.voucher:
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
||||
).exclude(pk=cp.pk)
|
||||
v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
|
||||
if v_avail < 1:
|
||||
err = err or error_messages['voucher_redeemed']
|
||||
cp.delete() # Sorry!
|
||||
continue
|
||||
cp._quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
|
||||
|
||||
if cp.item.require_voucher and cp.voucher is None:
|
||||
cp.delete()
|
||||
@@ -223,7 +227,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
price = cp.item.default_price if cp.variation is None else (
|
||||
cp.variation.default_price if cp.variation.default_price is not None else cp.item.default_price)
|
||||
|
||||
if price is False or len(quotas) == 0:
|
||||
if price is False or len(cp._quotas) == 0:
|
||||
err = err or error_messages['unavailable']
|
||||
cp.delete()
|
||||
continue
|
||||
@@ -242,63 +246,19 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
cp.save()
|
||||
err = err or error_messages['price_changed']
|
||||
continue
|
||||
|
||||
quota_ok = True
|
||||
|
||||
ignore_all_quotas = cp.expires >= now_dt or (
|
||||
cp.voucher and (cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)))
|
||||
|
||||
if not ignore_all_quotas:
|
||||
for quota in quotas:
|
||||
if cp.voucher and cp.voucher.block_quota and cp.voucher.quota_id == quota.pk:
|
||||
continue
|
||||
avail = quota.availability(now_dt)
|
||||
if avail[0] != Quota.AVAILABILITY_OK:
|
||||
# This quota is sold out/currently unavailable, so do not sell this at all
|
||||
err = err or error_messages['unavailable']
|
||||
quota_ok = False
|
||||
break
|
||||
|
||||
if quota_ok:
|
||||
positions[i] = cp
|
||||
cp.expires = now_dt + timedelta(
|
||||
minutes=event.settings.get('reservation_time', as_type=int))
|
||||
cp.save()
|
||||
else:
|
||||
cp.delete() # Sorry!
|
||||
if err:
|
||||
raise OrderError(err)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||
payment_provider: BasePaymentProvider, locale: str=None, address: int=None,
|
||||
payment_provider: BasePaymentProvider, expires: datetime, locale: str=None, address: int=None,
|
||||
meta_info: dict=None):
|
||||
from datetime import date, time
|
||||
|
||||
total = sum([c.price for c in positions])
|
||||
payment_fee = payment_provider.calculate_fee(total)
|
||||
total += payment_fee
|
||||
|
||||
tz = pytz.timezone(event.settings.timezone)
|
||||
exp_by_date = now_dt.astimezone(tz) + timedelta(days=event.settings.get('payment_term_days', as_type=int))
|
||||
exp_by_date = exp_by_date.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
if event.settings.get('payment_term_weekdays'):
|
||||
if exp_by_date.weekday() == 5:
|
||||
exp_by_date += timedelta(days=2)
|
||||
elif exp_by_date.weekday() == 6:
|
||||
exp_by_date += timedelta(days=1)
|
||||
|
||||
expires = exp_by_date
|
||||
|
||||
if event.settings.get('payment_term_last'):
|
||||
last_date = make_aware(datetime.combine(
|
||||
event.settings.get('payment_term_last', as_type=date),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
if last_date < expires:
|
||||
expires = last_date
|
||||
|
||||
order = Order.objects.create(
|
||||
status=Order.STATUS_PENDING,
|
||||
event=event,
|
||||
@@ -330,6 +290,94 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
return order
|
||||
|
||||
|
||||
def _check_quota_on_expired_positions(event: Event, positions: List[CartPosition], now_dt: datetime):
|
||||
err = None
|
||||
quotadiff = Counter()
|
||||
vouchers = Counter()
|
||||
for cp in positions:
|
||||
if not cp.id:
|
||||
continue
|
||||
|
||||
ignore_all_quotas = cp.expires >= now_dt or (
|
||||
cp.voucher and (cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)))
|
||||
|
||||
if ignore_all_quotas:
|
||||
cp._quotas = []
|
||||
elif cp.voucher and cp.voucher.block_quota and cp.voucher.quota_id:
|
||||
cp._quotas = [q for q in cp._quotas if cp.voucher.quota_id != q.pk]
|
||||
|
||||
for quota in cp._quotas:
|
||||
quotadiff[quota] += 1
|
||||
|
||||
quotas_ok = {}
|
||||
for quota, count in quotadiff.items():
|
||||
avail = quota.availability(now_dt)
|
||||
if avail[1] is not None and avail[1] < count:
|
||||
# This quota is not available or less than items are than requested left, so we have to
|
||||
# reduce the number of bought items
|
||||
if avail[0] != Quota.AVAILABILITY_OK:
|
||||
err = err or error_messages['unavailable']
|
||||
else:
|
||||
err = err or error_messages['in_part']
|
||||
quotas_ok[quota] = min(count, avail[1])
|
||||
else:
|
||||
quotas_ok[quota] = count
|
||||
|
||||
for cp in positions:
|
||||
if not cp.id:
|
||||
continue
|
||||
|
||||
if cp.voucher:
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
||||
).exclude(pk__in=[cp2.pk for cp2 in positions])
|
||||
v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
|
||||
if v_avail < 1:
|
||||
err = err or error_messages['voucher_redeemed']
|
||||
cp.delete() # Sorry!
|
||||
continue
|
||||
if v_avail - vouchers[cp.voucher] < 1:
|
||||
err = err or (error_messages['voucher_redeemed_partial'] % v_avail)
|
||||
cp.delete() # Sorry!
|
||||
continue
|
||||
vouchers[cp.voucher] += 1
|
||||
|
||||
if cp._quotas:
|
||||
if min(quotas_ok[q] for q in cp._quotas) > 0:
|
||||
cp.expires = now_dt + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
|
||||
cp.save()
|
||||
for q in cp._quotas:
|
||||
quotas_ok[q] -= 1
|
||||
else:
|
||||
cp.delete()
|
||||
|
||||
if err:
|
||||
raise OrderError(err)
|
||||
|
||||
|
||||
def _calculate_expiry(event: Event, now_dt: datetime):
|
||||
from datetime import date, time
|
||||
|
||||
tz = pytz.timezone(event.settings.timezone)
|
||||
expires = now_dt.astimezone(tz) + timedelta(days=event.settings.get('payment_term_days', as_type=int))
|
||||
expires = expires.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
if event.settings.get('payment_term_weekdays'):
|
||||
if expires.weekday() == 5:
|
||||
expires += timedelta(days=2)
|
||||
elif expires.weekday() == 6:
|
||||
expires += timedelta(days=1)
|
||||
|
||||
if event.settings.get('payment_term_last'):
|
||||
last_date = make_aware(datetime.combine(
|
||||
event.settings.get('payment_term_last', as_type=date),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
if last_date < expires:
|
||||
expires = last_date
|
||||
|
||||
return expires
|
||||
|
||||
|
||||
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
email: str, locale: str, address: int, meta_info: dict=None):
|
||||
|
||||
@@ -343,13 +391,18 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
if not pprov:
|
||||
raise OrderError(error_messages['internal'])
|
||||
|
||||
now_dt = now()
|
||||
|
||||
positions = list(CartPosition.objects.filter(id__in=position_ids).select_related('item', 'variation'))
|
||||
if set(str(p) for p in position_ids) != set(str(p.id) for p in positions):
|
||||
raise OrderError(error_messages['internal'])
|
||||
|
||||
_check_positions(event, now_dt, positions)
|
||||
expires = _calculate_expiry(event, now_dt)
|
||||
|
||||
with event.lock() as now_dt:
|
||||
positions = list(CartPosition.objects.filter(
|
||||
id__in=position_ids).select_related('item', 'variation'))
|
||||
if len(position_ids) != len(positions):
|
||||
raise OrderError(error_messages['internal'])
|
||||
_check_positions(event, now_dt, positions)
|
||||
order = _create_order(event, email, positions, now_dt, pprov,
|
||||
_check_quota_on_expired_positions(event, positions, now_dt)
|
||||
order = _create_order(event, email, positions, now_dt, pprov, expires,
|
||||
locale=locale, address=address, meta_info=meta_info)
|
||||
|
||||
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
|
||||
@@ -570,12 +623,12 @@ class OrderChangeManager:
|
||||
# Do nothing
|
||||
return
|
||||
with transaction.atomic():
|
||||
self._check_free_to_paid()
|
||||
self._check_complete_cancel()
|
||||
with self.order.event.lock():
|
||||
if self.order.status != Order.STATUS_PENDING:
|
||||
raise OrderError(self.error_messages['not_pending'])
|
||||
self._check_free_to_paid()
|
||||
self._check_quotas()
|
||||
self._check_complete_cancel()
|
||||
self._perform_operations()
|
||||
self._recalculate_total_and_payment_fee()
|
||||
self._reissue_invoice()
|
||||
|
||||
@@ -7,9 +7,8 @@ from django.utils.timezone import make_aware, now
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Event, Item, Order, OrderPosition, Organizer
|
||||
from pretix.base.payment import FreeOrderProvider
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, _create_order, expire_orders,
|
||||
OrderChangeManager, OrderError, _calculate_expiry, expire_orders,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,10 +27,7 @@ def test_expiry_days(event):
|
||||
today = now()
|
||||
event.settings.set('payment_term_days', 5)
|
||||
event.settings.set('payment_term_weekdays', False)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
locale='de')
|
||||
assert (order.expires - today).days == 5
|
||||
assert (_calculate_expiry(event, today) - today).days == 5
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -39,18 +35,12 @@ def test_expiry_weekdays(event):
|
||||
today = make_aware(datetime(2016, 9, 20, 15, 0, 0, 0))
|
||||
event.settings.set('payment_term_days', 5)
|
||||
event.settings.set('payment_term_weekdays', True)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
locale='de')
|
||||
assert (order.expires - today).days == 6
|
||||
assert order.expires.weekday() == 0
|
||||
assert (_calculate_expiry(event, today) - today).days == 6
|
||||
assert _calculate_expiry(event, today).weekday() == 0
|
||||
|
||||
today = make_aware(datetime(2016, 9, 19, 15, 0, 0, 0))
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
locale='de')
|
||||
assert (order.expires - today).days == 7
|
||||
assert order.expires.weekday() == 0
|
||||
assert (_calculate_expiry(event, today) - today).days == 7
|
||||
assert _calculate_expiry(event, today).weekday() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -59,15 +49,9 @@ def test_expiry_last(event):
|
||||
event.settings.set('payment_term_days', 5)
|
||||
event.settings.set('payment_term_weekdays', False)
|
||||
event.settings.set('payment_term_last', now() + timedelta(days=3))
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
locale='de')
|
||||
assert (order.expires - today).days == 3
|
||||
assert (_calculate_expiry(event, today) - today).days == 3
|
||||
event.settings.set('payment_term_last', now() + timedelta(days=7))
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
locale='de')
|
||||
assert (order.expires - today).days == 5
|
||||
assert (_calculate_expiry(event, today) - today).days == 5
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -349,6 +349,20 @@ class CartTest(CartTestMixin, TestCase):
|
||||
self.assertIsNone(objs[0].variation)
|
||||
self.assertEqual(objs[0].price, 23)
|
||||
|
||||
def test_quota_partly_multiple_products(self):
|
||||
self.quota_tickets.size = 1
|
||||
self.quota_tickets.save()
|
||||
self.quota_tickets.items.add(self.shirt)
|
||||
self.quota_tickets.variations.add(self.shirt_red)
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_%d' % self.ticket.id: '1',
|
||||
'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
|
||||
def test_renew_in_time(self):
|
||||
cp = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
@@ -539,10 +553,10 @@ class CartTest(CartTestMixin, TestCase):
|
||||
)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1',
|
||||
'_voucher_code': v.code,
|
||||
}, follow=True)
|
||||
obj = CartPosition.objects.get(id=cp1.id)
|
||||
self.assertGreater(obj.expires, now())
|
||||
self.assertEqual(obj.voucher, v)
|
||||
|
||||
def test_voucher_variation(self):
|
||||
v = Voucher.objects.create(item=self.shirt, variation=self.shirt_red, event=self.event)
|
||||
@@ -801,10 +815,9 @@ class CartTest(CartTestMixin, TestCase):
|
||||
'_voucher_code': v.code,
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertIn('already been used', doc.select('.alert-danger')[0].text)
|
||||
self.assertIn('only be redeemed 1 more time', doc.select('.alert-danger')[0].text)
|
||||
positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event)
|
||||
assert positions.count() == 1
|
||||
assert all(cp.voucher == v for cp in positions)
|
||||
assert positions.count() == 0
|
||||
|
||||
def test_voucher_multiuse_redeemed(self):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||
@@ -843,9 +856,9 @@ class CartTest(CartTestMixin, TestCase):
|
||||
'_voucher_code': v.code,
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertIn('already been used', doc.select('.alert-danger')[0].text)
|
||||
self.assertIn('only be redeemed 1 more time', doc.select('.alert-danger')[0].text)
|
||||
positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event)
|
||||
assert positions.count() == 1
|
||||
assert positions.count() == 0
|
||||
|
||||
def test_voucher_multiuse_redeemed_in_other_cart(self):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||
|
||||
@@ -405,10 +405,31 @@ class CheckoutTestCase(TestCase):
|
||||
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, "lxml")
|
||||
self.assertIn("has already been", doc.select(".alert-danger")[0].text)
|
||||
self.assertIn("only be redeemed 1 more time", doc.select(".alert-danger")[0].text)
|
||||
assert CartPosition.objects.filter(cart_id=self.session_key).count() == 1
|
||||
|
||||
def test_voucher_multiuse_ok(self):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1)
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=12, expires=now() + timedelta(minutes=10), voucher=v
|
||||
)
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=12, 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, "lxml")
|
||||
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key).exists())
|
||||
self.assertEqual(Order.objects.count(), 1)
|
||||
self.assertEqual(OrderPosition.objects.count(), 2)
|
||||
v.refresh_from_db()
|
||||
assert v.redeemed == 3
|
||||
|
||||
def test_voucher_multiuse_ok_expired(self):
|
||||
v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event,
|
||||
valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1)
|
||||
CartPosition.objects.create(
|
||||
@@ -472,7 +493,7 @@ class CheckoutTestCase(TestCase):
|
||||
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, "lxml")
|
||||
self.assertIn("has already been", doc.select(".alert-danger")[0].text)
|
||||
self.assertIn("only be redeemed 1 more time", doc.select(".alert-danger")[0].text)
|
||||
assert CartPosition.objects.filter(cart_id=self.session_key).count() == 1
|
||||
|
||||
def test_voucher_ignore_quota(self):
|
||||
|
||||
@@ -9,4 +9,8 @@ TEMPLATES[0]['DIRS'].append(os.path.join(TEST_DIR, 'templates')) # NOQA
|
||||
INSTALLED_APPS.append('tests.testdummy') # NOQA
|
||||
|
||||
for a in PLUGINS:
|
||||
INSTALLED_APPS.remove(a)
|
||||
INSTALLED_APPS.remove(a)
|
||||
|
||||
DATABASES['default'] = {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user