diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py
index 53d62a3608..bb77475506 100644
--- a/src/pretix/api/serializers/order.py
+++ b/src/pretix/api/serializers/order.py
@@ -720,6 +720,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
consume_carts = validated_data.pop('consume_carts', [])
delete_cps = []
quota_avail_cache = {}
+ v_budget = {}
voucher_usage = Counter()
if consume_carts:
for cp in CartPosition.objects.filter(
@@ -742,9 +743,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
errs = [{} for p in positions_data]
for i, pos_data in enumerate(positions_data):
+
if pos_data.get('voucher'):
v = pos_data['voucher']
+ if pos_data.get('addon_to'):
+ errs[i]['voucher'] = ['Vouchers are currently not supported for add-on products.']
+ continue
+
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
continue
@@ -768,6 +774,44 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
'The voucher has already been used the maximum number of times.'
]
+ if v.budget is not None:
+ price = pos_data.get('price')
+ if price is None:
+ price = get_price(
+ item=pos_data.get('item'),
+ variation=pos_data.get('variation'),
+ voucher=v,
+ custom_price=None,
+ subevent=pos_data.get('subevent'),
+ addon_to=pos_data.get('addon_to'),
+ invoice_address=ia,
+ ).gross
+ pbv = get_price(
+ item=pos_data['item'],
+ variation=pos_data.get('variation'),
+ voucher=None,
+ custom_price=None,
+ subevent=pos_data.get('subevent'),
+ addon_to=pos_data.get('addon_to'),
+ invoice_address=ia,
+ )
+
+ if v not in v_budget:
+ v_budget[v] = v.budget - v.budget_used()
+ disc = pbv.gross - price
+ if disc > v_budget[v]:
+ new_disc = v_budget[v]
+ v_budget[v] -= new_disc
+ if new_disc == Decimal('0.00') or pos_data.get('price') is not None:
+ errs[i]['voucher'] = [
+ 'The voucher has a remaining budget of {}, therefore a discount of {} can not be '
+ 'given.'.format(v_budget[v] + new_disc, disc)
+ ]
+ continue
+ pos_data['price'] = price + (disc - new_disc)
+ else:
+ v_budget[v] -= disc
+
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
if pos_data.get('seat'):
if not seated:
@@ -856,6 +900,17 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos.tax_rule = pos.item.tax_rule
else:
pos._calculate_tax()
+
+ pos.price_before_voucher = get_price(
+ item=pos.item,
+ variation=pos.variation,
+ voucher=None,
+ custom_price=None,
+ subevent=pos.subevent,
+ addon_to=pos.addon_to,
+ invoice_address=ia,
+ ).gross
+
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
diff --git a/src/pretix/base/migrations/0142_auto_20191215_1522.py b/src/pretix/base/migrations/0142_auto_20191215_1522.py
new file mode 100644
index 0000000000..57690caee5
--- /dev/null
+++ b/src/pretix/base/migrations/0142_auto_20191215_1522.py
@@ -0,0 +1,30 @@
+# Generated by Django 2.2.7 on 2019-12-15 15:22
+
+from decimal import Decimal
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0141_seat_sorting_rank'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='cartposition',
+ name='price_before_voucher',
+ field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
+ ),
+ migrations.AddField(
+ model_name='orderposition',
+ name='price_before_voucher',
+ field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
+ ),
+ migrations.AddField(
+ model_name='voucher',
+ name='budget',
+ field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
+ ),
+ ]
diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py
index 14252e3f7a..8ec4cfe9c3 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -687,10 +687,12 @@ class Order(LockModel, LoggedModel):
error_messages = {
'unavailable': _('The ordered product "{item}" is no longer available.'),
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
+ 'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
}
now_dt = now_dt or now()
- positions = self.positions.all().select_related('item', 'variation', 'seat')
+ positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
quota_cache = {}
+ v_budget = {}
try:
for i, op in enumerate(positions):
if op.seat:
@@ -699,6 +701,16 @@ class Order(LockModel, LoggedModel):
if force:
continue
+ if op.voucher and op.voucher.budget is not None and op.price_before_voucher is not None:
+ if op.voucher not in v_budget:
+ v_budget[op.voucher] = op.voucher.budget - op.voucher.budget_used()
+ disc = op.price_before_voucher - op.price
+ if disc > v_budget[op.voucher]:
+ raise Quota.QuotaExceededException(error_messages['voucher_budget'].format(
+ voucher=op.voucher.code
+ ))
+ v_budget[op.voucher] -= disc
+
quotas = list(op.quotas)
if len(quotas) == 0:
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
@@ -991,6 +1003,9 @@ class AbstractPosition(models.Model):
verbose_name=_("Variation"),
on_delete=models.PROTECT
)
+ price_before_voucher = models.DecimalField(
+ decimal_places=2, max_digits=10, null=True,
+ )
price = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Price")
diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py
index 3bc82abb1f..8239bed77b 100644
--- a/src/pretix/base/models/vouchers.py
+++ b/src/pretix/base/models/vouchers.py
@@ -4,7 +4,8 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator
from django.db import models
-from django.db.models import Q
+from django.db.models import F, OuterRef, Q, Subquery, Sum
+from django.db.models.functions import Coalesce
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
@@ -17,7 +18,7 @@ from ..decimal import round_decimal
from .base import LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation, Quota
-from .orders import Order
+from .orders import Order, OrderPosition
def _generate_random_code(prefix=None):
@@ -114,6 +115,13 @@ class Voucher(LoggedModel):
verbose_name=_("Redeemed"),
default=0
)
+ budget = models.DecimalField(
+ verbose_name=_("Maximum discount budget"),
+ help_text=_("This is the maximum monetary amount that will be discounted using this voucher across all usages. "
+ "If this is sum reached, the voucher can no longer be used."),
+ decimal_places=2, max_digits=10,
+ null=True, blank=True
+ )
valid_until = models.DateTimeField(
blank=True, null=True, db_index=True,
verbose_name=_("Valid until")
@@ -430,7 +438,7 @@ class Voucher(LoggedModel):
return False
return True
- def calculate_price(self, original_price: Decimal) -> Decimal:
+ def calculate_price(self, original_price: Decimal, max_discount: Decimal=None) -> Decimal:
"""
Returns how the price given in original_price would be modified if this
voucher is applied, i.e. replaced by a different price or reduced by a
@@ -448,7 +456,9 @@ class Voucher(LoggedModel):
p = original_price
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
if places < 2:
- return p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
+ p = p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
+ if max_discount is not None:
+ p = max(p, original_price - max_discount)
return p
return original_price
@@ -470,3 +480,26 @@ class Voucher(LoggedModel):
return self.item.seat_category_mappings.filter(**kwargs).exists()
else:
return bool(subevent.seating_plan) if subevent else self.event.seating_plan
+
+ @classmethod
+ def annotate_budget_used_orders(cls, qs):
+ opq = OrderPosition.objects.filter(
+ voucher_id=OuterRef('pk'),
+ price_before_voucher__isnull=False,
+ order__status__in=[
+ Order.STATUS_PAID,
+ Order.STATUS_PENDING
+ ]
+ ).order_by().values('voucher_id').annotate(s=Sum(F('price_before_voucher') - F('price'))).values('s')
+ return qs.annotate(budget_used_orders=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=10, decimal_places=2)), Decimal('0.00')))
+
+ def budget_used(self):
+ ops = OrderPosition.objects.filter(
+ voucher=self,
+ price_before_voucher__isnull=False,
+ order__status__in=[
+ Order.STATUS_PAID,
+ Order.STATUS_PENDING
+ ]
+ ).aggregate(s=Sum(F('price_before_voucher') - F('price')))['s'] or Decimal('0.00')
+ return ops
diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py
index 1479c9f1e1..09f2185e4f 100644
--- a/src/pretix/base/services/cart.py
+++ b/src/pretix/base/services/cart.py
@@ -108,11 +108,12 @@ error_messages = {
class CartManager:
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas',
- 'addon_to', 'subevent', 'includes_tax', 'bundled', 'seat'))
+ 'addon_to', 'subevent', 'includes_tax', 'bundled', 'seat',
+ 'price_before_voucher'))
RemoveOperation = namedtuple('RemoveOperation', ('position',))
VoucherOperation = namedtuple('VoucherOperation', ('position', 'voucher', 'price'))
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
- 'quotas', 'subevent', 'seat'))
+ 'quotas', 'subevent', 'seat', 'price_before_voucher'))
order = {
RemoveOperation: 10,
VoucherOperation: 15,
@@ -384,6 +385,7 @@ class CartManager:
else:
price = self._get_price(cp.item, cp.variation, cp.voucher, price, cp.subevent,
force_custom_price=True)
+ pbv = TAXED_ZERO
else:
bundled_sum = Decimal('0.00')
if not cp.addon_to_id:
@@ -396,9 +398,14 @@ class CartManager:
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
cp_is_net=True, bundled_sum=bundled_sum)
price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='')
+ pbv = self._get_price(cp.item, cp.variation, None, cp.price, cp.subevent,
+ cp_is_net=True, bundled_sum=bundled_sum)
+ pbv = TaxedPrice(net=pbv.net, gross=pbv.net, rate=0, tax=0, name='')
else:
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
bundled_sum=bundled_sum)
+ pbv = self._get_price(cp.item, cp.variation, None, cp.price, cp.subevent,
+ bundled_sum=bundled_sum)
quotas = list(cp.quotas)
if not quotas:
@@ -414,7 +421,7 @@ class CartManager:
op = self.ExtendOperation(
position=cp, item=cp.item, variation=cp.variation, voucher=cp.voucher, count=1,
- price=price, quotas=quotas, subevent=cp.subevent, seat=cp.seat
+ price=price, quotas=quotas, subevent=cp.subevent, seat=cp.seat, price_before_voucher=pbv
)
self._check_item_constraints(op)
@@ -569,16 +576,18 @@ class CartManager:
bop = self.AddOperation(
count=bundle.count, item=bitem, variation=bvar, price=bprice,
voucher=None, quotas=bundle_quotas, addon_to='FAKE', subevent=subevent,
- includes_tax=bool(bprice.rate), bundled=[], seat=None
+ includes_tax=bool(bprice.rate), bundled=[], seat=None, price_before_voucher=bprice,
)
self._check_item_constraints(bop, operations)
bundled.append(bop)
price = self._get_price(item, variation, voucher, i.get('price'), subevent, bundled_sum=bundled_sum)
+ pbv = self._get_price(item, variation, None, i.get('price'), subevent, bundled_sum=bundled_sum)
op = self.AddOperation(
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas,
- addon_to=False, subevent=subevent, includes_tax=bool(price.rate), bundled=bundled, seat=seat
+ addon_to=False, subevent=subevent, includes_tax=bool(price.rate), bundled=bundled, seat=seat,
+ price_before_voucher=pbv
)
self._check_item_constraints(op, operations)
operations.append(op)
@@ -684,7 +693,8 @@ class CartManager:
op = self.AddOperation(
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
- addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=None
+ addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=None,
+ price_before_voucher=None
)
self._check_item_constraints(op, operations)
operations.append(op)
@@ -898,7 +908,8 @@ class CartManager:
event=self.event, item=op.item, variation=op.variation,
price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
- subevent=op.subevent, includes_tax=op.includes_tax, seat=op.seat
+ subevent=op.subevent, includes_tax=op.includes_tax, seat=op.seat,
+ price_before_voucher=op.price_before_voucher.gross if op.price_before_voucher is not None else None
)
if self.event.settings.attendee_names_asked:
scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
@@ -945,6 +956,8 @@ class CartManager:
elif available_count == 1:
op.position.expires = self._expiry
op.position.price = op.price.gross
+ if op.price_before_voucher is not None:
+ op.position.price_before_voucher = op.price_before_voucher.gross
try:
op.position.save(force_update=True)
except DatabaseError:
@@ -964,6 +977,7 @@ class CartManager:
# be expected
continue
+ op.position.price_before_voucher = op.position.price
op.position.price = op.price.gross
op.position.voucher = op.voucher
op.position.save()
diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py
index 5c59e81af3..d15e9ce246 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -70,6 +70,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_budget_used': _('The voucher code used for one of the items in your cart has already been too often. We '
+ 'adjusted the price of the item in 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 '
@@ -426,6 +428,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
products_seen = Counter()
changed_prices = {}
+ v_budget = {}
deleted_positions = set()
seats_seen = set()
@@ -468,6 +471,20 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
delete(cp)
continue
+ if cp.voucher.budget is not None:
+ if cp.voucher not in v_budget:
+ v_budget[cp.voucher] = cp.voucher.budget - cp.voucher.budget_used()
+ disc = cp.price_before_voucher - cp.price
+ if disc > v_budget[cp.voucher]:
+ new_disc = max(0, v_budget[cp.voucher])
+ cp.price = cp.price + (disc - new_disc)
+ cp.save()
+ err = err or error_messages['voucher_budget_used']
+ v_budget[cp.voucher] -= new_disc
+ continue
+ else:
+ v_budget[cp.voucher] -= disc
+
if cp.subevent and cp.subevent.presale_start and now_dt < cp.subevent.presale_start:
err = err or error_messages['some_subevent_not_started']
delete(cp)
@@ -522,6 +539,11 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
# Other checks are not necessary
continue
+ max_discount = None
+ if cp.price_before_voucher is not None and cp.voucher in v_budget:
+ current_discount = cp.price_before_voucher - cp.price
+ max_discount = max(v_budget[cp.voucher] + current_discount, 0)
+
if cp.is_bundled:
try:
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
@@ -529,7 +551,9 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
except ItemBundle.DoesNotExist:
bprice = cp.price
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
- invoice_address=address, force_custom_price=True)
+ invoice_address=address, force_custom_price=True, max_discount=max_discount)
+ pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
+ invoice_address=address, force_custom_price=True, max_discount=max_discount)
changed_prices[cp.pk] = bprice
else:
bundled_sum = 0
@@ -539,7 +563,14 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
bundled_sum += changed_prices.get(bundledp.pk, bundledp.price)
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
- addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum)
+ addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
+ max_discount=max_discount)
+ pbv = get_price(cp.item, cp.variation, None, cp.price, cp.subevent, custom_price_is_net=False,
+ addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
+ max_discount=max_discount)
+
+ if max_discount is not None:
+ v_budget[cp.voucher] = v_budget[cp.voucher] + current_discount - (pbv.gross - price.gross)
if price is False or len(quotas) == 0:
err = err or error_messages['unavailable']
@@ -552,6 +583,11 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
delete(cp)
continue
+ if pbv is not None and pbv.gross != price.gross:
+ cp.price_before_voucher = pbv.gross
+ else:
+ cp.price_before_voucher = None
+
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
cp.price = price.gross
cp.includes_tax = bool(price.rate)
@@ -802,7 +838,10 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
lockfn = event.lock
with lockfn() as now_dt:
- positions = list(positions.select_related('item', 'variation', 'subevent', 'seat', 'addon_to').prefetch_related('addons'))
+ positions = list(
+ positions.select_related('item', 'variation', 'subevent', 'seat', 'addon_to').prefetch_related('addons')
+ )
+ positions.sort(key=lambda k: position_ids.index(k.pk))
if len(positions) == 0:
raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions):
@@ -1362,6 +1401,16 @@ class OrderChangeManager:
op.position.item = op.item
op.position.variation = op.variation
op.position._calculate_tax()
+ if op.position.price_before_voucher is not None and op.position.voucher and not op.position.addon_to_id:
+ op.position.price_before_voucher = max(
+ op.position.price,
+ get_price(
+ op.position.item, op.position.variation,
+ subevent=op.position.subevent,
+ custom_price=op.position.price,
+ invoice_address=self._invoice_address
+ ).gross
+ )
op.position.save()
elif isinstance(op, self.SeatOperation):
self.order.log_action('pretix.event.order.changed.seat', user=self.user, auth=self.auth, data={
@@ -1385,6 +1434,16 @@ class OrderChangeManager:
})
op.position.subevent = op.subevent
op.position.save()
+ if op.position.price_before_voucher is not None and op.position.voucher and not op.position.addon_to_id:
+ op.position.price_before_voucher = max(
+ op.position.price,
+ get_price(
+ op.position.item, op.position.variation,
+ subevent=op.position.subevent,
+ custom_price=op.position.price,
+ invoice_address=self._invoice_address
+ ).gross
+ )
elif isinstance(op, self.FeeValueOperation):
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
'fee': op.fee.pk,
diff --git a/src/pretix/base/services/pricing.py b/src/pretix/base/services/pricing.py
index e46c8f9ef9..16de473748 100644
--- a/src/pretix/base/services/pricing.py
+++ b/src/pretix/base/services/pricing.py
@@ -12,7 +12,8 @@ def get_price(item: Item, variation: ItemVariation = None,
voucher: Voucher = None, custom_price: Decimal = None,
subevent: SubEvent = None, custom_price_is_net: bool = False,
addon_to: AbstractPosition = None, invoice_address: InvoiceAddress = None,
- force_custom_price: bool = False, bundled_sum: Decimal = Decimal('0.00')) -> TaxedPrice:
+ force_custom_price: bool = False, bundled_sum: Decimal = Decimal('0.00'),
+ max_discount: Decimal = None) -> TaxedPrice:
if addon_to:
try:
iao = addon_to.item.addons.get(addon_category_id=item.category_id)
@@ -32,7 +33,7 @@ def get_price(item: Item, variation: ItemVariation = None,
price = subevent.var_price_overrides[variation.pk]
if voucher:
- price = voucher.calculate_price(price)
+ price = voucher.calculate_price(price, max_discount=max_discount)
if item.tax_rule:
tax_rule = item.tax_rule
diff --git a/src/pretix/control/forms/vouchers.py b/src/pretix/control/forms/vouchers.py
index 8d79910d90..83c93889c3 100644
--- a/src/pretix/control/forms/vouchers.py
+++ b/src/pretix/control/forms/vouchers.py
@@ -38,7 +38,7 @@ class VoucherForm(I18nModelForm):
localized_fields = '__all__'
fields = [
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
- 'comment', 'max_usages', 'price_mode', 'subevent', 'show_hidden_items',
+ 'comment', 'max_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
]
field_classes = {
'valid_until': SplitDateTimeField,
@@ -268,7 +268,7 @@ class VoucherBulkForm(VoucherForm):
localized_fields = '__all__'
fields = [
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
- 'max_usages', 'price_mode', 'subevent', 'show_hidden_items'
+ 'max_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
]
field_classes = {
'valid_until': SplitDateTimeField,
diff --git a/src/pretix/control/templates/pretixcontrol/order/change.html b/src/pretix/control/templates/pretixcontrol/order/change.html
index e85d194981..2930d2a112 100644
--- a/src/pretix/control/templates/pretixcontrol/order/change.html
+++ b/src/pretix/control/templates/pretixcontrol/order/change.html
@@ -80,6 +80,15 @@
{{ position.custom_error }}
{% endif %}
+ {% if position.voucher and position.voucher.budget %}
+
+ {% blocktrans trimmed %}
+ This position has been created with a voucher with a limited budget. If you
+ change the price or item, the discount will still be calculated from the original
+ price at the time of purchase.
+ {% endblocktrans %}
+
+ {% endif %}
{% trans "Current value" %}
diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html
index 6bf7fea6ce..2ca5501b91 100644
--- a/src/pretix/control/templates/pretixcontrol/order/index.html
+++ b/src/pretix/control/templates/pretixcontrol/order/index.html
@@ -272,7 +272,9 @@
{% endif %}
{% if line.voucher %}
{% trans "Voucher code used:" %}
-
+
{{ line.voucher.code }}
{% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html b/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html
index 083c6b7d0f..7be73a1799 100644
--- a/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html
+++ b/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html
@@ -73,6 +73,7 @@
{% bootstrap_field form.block_quota layout="control" %}
{% bootstrap_field form.allow_ignore_quota layout="control" %}
+ {% bootstrap_field form.budget addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.show_hidden_items layout="control" %}
diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html
index 55e06388fe..8ff1267ff4 100644
--- a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html
+++ b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html
@@ -75,6 +75,7 @@
{% bootstrap_field form.block_quota layout="control" %}
{% bootstrap_field form.allow_ignore_quota layout="control" %}
+ {% bootstrap_field form.budget addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.show_hidden_items layout="control" %}
diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/index.html b/src/pretix/control/templates/pretixcontrol/vouchers/index.html
index 381e15d21c..280afef1f5 100644
--- a/src/pretix/control/templates/pretixcontrol/vouchers/index.html
+++ b/src/pretix/control/templates/pretixcontrol/vouchers/index.html
@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% load urlreplace %}
+{% load money %}
{% block title %}{% trans "Vouchers" %}{% endblock %}
{% block content %}
{% trans "Vouchers" %}
@@ -143,7 +144,15 @@
{{ v.code }}
{% if not v.is_active %}{% endif %}
-
{{ v.redeemed }} / {{ v.max_usages }} |
+
+ {{ v.redeemed }} / {{ v.max_usages }}
+ {% if v.budget|default_if_none:"NONE" != "NONE" %}
+
+
+ {{ v.budget_used_orders|money:request.event.currency }} / {{ v.budget|money:request.event.currency }}
+
+ {% endif %}
+ |
{{ v.valid_until|date }} |
{{ v.tag }}
diff --git a/src/pretix/control/views/vouchers.py b/src/pretix/control/views/vouchers.py
index 3aa78524e1..5b247f9019 100644
--- a/src/pretix/control/views/vouchers.py
+++ b/src/pretix/control/views/vouchers.py
@@ -37,9 +37,11 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
permission = 'can_view_vouchers'
def get_queryset(self):
- qs = self.request.event.vouchers.filter(waitinglistentries__isnull=True).select_related(
+ qs = Voucher.annotate_budget_used_orders(self.request.event.vouchers.filter(
+ waitinglistentries__isnull=True
+ ).select_related(
'item', 'variation', 'seat'
- )
+ ))
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py
index 90f1ae4843..e36174bc65 100644
--- a/src/tests/api/test_orders.py
+++ b/src/tests/api/test_orders.py
@@ -3105,6 +3105,81 @@ def test_order_create_auto_pricing_reverse_charge_require_valid_vatid(token_clie
assert p.tax_rate == Decimal('19.00')
+@pytest.mark.django_db
+def test_order_create_autopricing_voucher_budget_partially(token_client, organizer, event, item, quota, question,
+ taxrule):
+ with scopes_disabled():
+ voucher = event.vouchers.create(price_mode="set", value=21.50, item=item, budget=Decimal('2.50'),
+ max_usages=999)
+ res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
+ res['positions'][0]['item'] = item.pk
+ res['positions'][0]['answers'][0]['question'] = question.pk
+ res['positions'][0]['voucher'] = voucher.code
+ del res['positions'][0]['price']
+ del res['positions'][0]['positionid']
+ res['positions'].append(res['positions'][0])
+
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/orders/'.format(
+ organizer.slug, event.slug
+ ), format='json', data=res
+ )
+ print(resp.data)
+ assert resp.status_code == 201
+ with scopes_disabled():
+ o = Order.objects.get(code=resp.data['code'])
+ p = o.positions.first()
+ p2 = o.positions.last()
+ assert p.price == Decimal('21.50')
+ assert p2.price == Decimal('22.00')
+
+
+@pytest.mark.django_db
+def test_order_create_autopricing_voucher_budget_full(token_client, organizer, event, item, quota, question, taxrule):
+ with scopes_disabled():
+ voucher = event.vouchers.create(price_mode="set", value=21.50, item=item, budget=Decimal('0.50'),
+ max_usages=999)
+ res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
+ res['positions'][0]['item'] = item.pk
+ res['positions'][0]['answers'][0]['question'] = question.pk
+ res['positions'][0]['voucher'] = voucher.code
+ del res['positions'][0]['price']
+ del res['positions'][0]['positionid']
+ res['positions'].append(res['positions'][0])
+
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/orders/'.format(
+ organizer.slug, event.slug
+ ), format='json', data=res
+ )
+ assert resp.status_code == 400
+ assert resp.data == {'positions': [{}, {'voucher': ['The voucher has a remaining budget of 0.00, therefore a '
+ 'discount of 1.50 can not be given.']}]}
+
+
+@pytest.mark.django_db
+def test_order_create_voucher_budget_exceeded(token_client, organizer, event, item, quota, question, taxrule):
+ with scopes_disabled():
+ voucher = event.vouchers.create(price_mode="set", value=21.50, item=item, budget=Decimal('3.00'),
+ max_usages=999)
+ res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
+ res['positions'][0]['item'] = item.pk
+ res['positions'][0]['answers'][0]['question'] = question.pk
+ res['positions'][0]['voucher'] = voucher.code
+ res['positions'][0]['price'] = '19.00'
+ del res['positions'][0]['positionid']
+
+ resp = token_client.post(
+ '/api/v1/organizers/{}/events/{}/orders/'.format(
+ organizer.slug, event.slug
+ ), format='json', data=res
+ )
+ print(resp.data)
+ assert resp.status_code == 400
+ assert resp.data == {'positions': [{'voucher': ['The voucher has a remaining budget of 3.00, therefore a '
+ 'discount of 4.00 can not be given.']}]}
+
+
@pytest.mark.django_db
def test_order_create_voucher_price(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py
index 5b6a3bb3d6..a47e19cef7 100644
--- a/src/tests/base/test_models.py
+++ b/src/tests/base/test_models.py
@@ -795,6 +795,31 @@ class VoucherTestCase(BaseQuotaTestCase):
v = Voucher.objects.create(event=self.event, price_mode='percent', value=Decimal('23.00'))
assert v.calculate_price(Decimal('100.00')) == Decimal('77.00')
+ @classscope(attr='o')
+ def test_calculate_price_max_discount(self):
+ v = Voucher.objects.create(event=self.event, price_mode='subtract', value=Decimal('10.00'))
+ assert v.calculate_price(Decimal('23.42'), max_discount=Decimal('5.00')) == Decimal('18.42')
+
+ @classscope(attr='o')
+ def test_calculate_budget_used(self):
+ v = Voucher.objects.create(event=self.event, price_mode='sset', value=Decimal('20.00'))
+
+ order = Order.objects.create(
+ status=Order.STATUS_PENDING, event=self.event,
+ datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
+ )
+ OrderPosition.objects.create(order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
+ price_before_voucher=Decimal('23.00'))
+ assert v.budget_used() == Decimal('3.00')
+
+ order = Order.objects.create(
+ status=Order.STATUS_PAID, event=self.event,
+ datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
+ )
+ OrderPosition.objects.create(order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
+ price_before_voucher=Decimal('23.00'))
+ assert v.budget_used() == Decimal('6.00')
+
class OrderTestCase(BaseQuotaTestCase):
def setUp(self):
diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py
index 2146b6d190..d8546b89e7 100644
--- a/src/tests/base/test_orders.py
+++ b/src/tests/base/test_orders.py
@@ -878,6 +878,36 @@ class OrderChangeManagerTests(TestCase):
assert self.op1.tax_value == Decimal('3.67')
assert self.op1.tax_rule == self.shirt.tax_rule
+ @classscope(attr='o')
+ def test_change_item_change_price_before_voucher(self):
+ self.op1.voucher = self.event.vouchers.create(item=self.shirt, redeemed=1, price_mode='set', value='5.00')
+ self.op1.price = Decimal('5.00')
+ self.op1.price_before_voucher = Decimal('23.00')
+ self.op1.save()
+ p = self.op1.price
+ self.ocm.change_item(self.op1, self.shirt, None)
+ self.ocm.commit()
+ self.op1.refresh_from_db()
+ self.order.refresh_from_db()
+ assert self.op1.item == self.shirt
+ assert self.op1.price == p
+ assert self.op1.price_before_voucher == Decimal('12.00')
+
+ @classscope(attr='o')
+ def test_change_item_change_price_before_voucher_minimum_value(self):
+ self.op1.voucher = self.event.vouchers.create(item=self.shirt, redeemed=1, price_mode='set', value='20.00')
+ self.op1.price = Decimal('20.00')
+ self.op1.price_before_voucher = Decimal('23.00')
+ self.op1.save()
+ p = self.op1.price
+ self.ocm.change_item(self.op1, self.shirt, None)
+ self.ocm.commit()
+ self.op1.refresh_from_db()
+ self.order.refresh_from_db()
+ assert self.op1.item == self.shirt
+ assert self.op1.price == p
+ assert self.op1.price_before_voucher == Decimal('20.00')
+
@classscope(attr='o')
def test_change_item_success(self):
self.ocm.change_item(self.op1, self.shirt, None)
diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py
index 8d44bb489c..dd5c878260 100644
--- a/src/tests/control/test_orders.py
+++ b/src/tests/control/test_orders.py
@@ -845,6 +845,69 @@ def test_order_extend_expired_quota_partial(client, env):
assert o.status == Order.STATUS_EXPIRED
+@pytest.mark.django_db
+def test_order_extend_expired_voucher_budget_ok(client, env):
+ with scopes_disabled():
+ o = Order.objects.get(id=env[2].id)
+ o.expires = now() - timedelta(days=5)
+ o.status = Order.STATUS_EXPIRED
+ o.save()
+ v = env[0].vouchers.create(
+ code="foo", price_mode='subtract', value=Decimal('1.50'), budget=Decimal('1.50')
+ )
+ p = o.positions.first()
+ p.voucher = v
+ p.price_before_voucher = p.price
+ p.price -= Decimal('1.50')
+ p.save()
+
+ q = Quota.objects.create(event=env[0], size=100)
+ q.items.add(env[3])
+ newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
+ 'expires': newdate
+ }, follow=True)
+ assert b'alert-success' in response.content
+ with scopes_disabled():
+ o = Order.objects.get(id=env[2].id)
+ assert o.status == Order.STATUS_PENDING
+ assert v.budget_used() == Decimal('1.50')
+
+
+@pytest.mark.django_db
+def test_order_extend_expired_voucher_budget_fail(client, env):
+ with scopes_disabled():
+ o = Order.objects.get(id=env[2].id)
+ o.expires = now() - timedelta(days=5)
+ o.status = Order.STATUS_EXPIRED
+ olddate = o.expires
+ o.save()
+ v = env[0].vouchers.create(
+ code="foo", price_mode='subtract', value=Decimal('1.50'), budget=Decimal('0.00')
+ )
+ p = o.positions.first()
+ p.voucher = v
+ p.price_before_voucher = p.price
+ p.price -= Decimal('1.50')
+ p.save()
+
+ q = Quota.objects.create(event=env[0], size=100)
+ q.items.add(env[3])
+ newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
+ 'expires': newdate
+ }, follow=True)
+ assert b'alert-danger' in response.content
+ assert b'The voucher "FOO" no longer has sufficient budget.' in response.content
+ with scopes_disabled():
+ o = Order.objects.get(id=env[2].id)
+ assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == olddate.strftime("%Y-%m-%d %H:%M:%S")
+ assert o.status == Order.STATUS_EXPIRED
+ assert v.budget_used() == Decimal('0.00')
+
+
@pytest.mark.django_db
def test_order_mark_paid_overdue_quota_blocked_by_waiting_list(client, env):
with scopes_disabled():
diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py
index 051a487cc6..117c718c9c 100644
--- a/src/tests/presale/test_cart.py
+++ b/src/tests/presale/test_cart.py
@@ -1388,6 +1388,32 @@ class CartTest(CartTestMixin, TestCase):
self.assertEqual(objs[0].item, self.ticket)
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, Decimal('21.00'))
+ self.assertEqual(objs[0].price_before_voucher, Decimal('23.00'))
+
+ def test_voucher_free_price_before_voucher_cap(self):
+ with scopes_disabled():
+ v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event)
+ self.ticket.free_price = True
+ self.ticket.save()
+ response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
+ 'item_%d' % self.ticket.id: '1',
+ 'price_%d' % self.ticket.id: '41.00',
+ '_voucher_code': v.code,
+ }, follow=True)
+ self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
+ doc = BeautifulSoup(response.rendered_content, "lxml")
+ self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text)
+ self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text)
+ self.assertIn('41', doc.select('.cart .cart-row')[0].select('.price')[0].text)
+ self.assertIn('41', doc.select('.cart .cart-row')[0].select('.price')[1].text)
+ with scopes_disabled():
+ 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('41.00'))
+ self.assertEqual(objs[0].price_before_voucher, Decimal('41.00'))
def test_voucher_free_price_lower_bound(self):
with scopes_disabled():
@@ -1412,6 +1438,7 @@ class CartTest(CartTestMixin, TestCase):
self.assertEqual(objs[0].item, self.ticket)
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, Decimal('20.70'))
+ self.assertEqual(objs[0].price_before_voucher, Decimal('23.00'))
def test_voucher_redemed(self):
with scopes_disabled():
@@ -2438,6 +2465,20 @@ class CartAddonTest(CartTestMixin, TestCase):
assert cp2.expires > now()
assert cp2.addon_to_id == cp1.pk
+ @classscope(attr='orga')
+ def test_expand_expired_refresh_voucher(self):
+ v = Voucher.objects.create(item=self.ticket, value=Decimal('20.00'), event=self.event, price_mode='set',
+ valid_until=now() + timedelta(days=2), max_usages=999, redeemed=0)
+ cp1 = CartPosition.objects.create(
+ expires=now() - timedelta(minutes=10), item=self.ticket, price=Decimal('21.50'),
+ event=self.event, cart_id=self.session_key, voucher=v
+ )
+ self.cm.extend_expired_positions()
+ self.cm.commit()
+ cp1.refresh_from_db()
+ assert cp1.expires > now()
+ assert cp1.price_before_voucher == Decimal('23.00')
+
class CartBundleTest(CartTestMixin, TestCase):
@scopes_disabled()
@@ -2490,6 +2531,30 @@ class CartBundleTest(CartTestMixin, TestCase):
assert cp.price == 23 - 1.5
assert cp.addons.count() == 1
assert cp.voucher == v
+ assert cp.price_before_voucher == 23 - 1.5
+ a = cp.addons.get()
+ assert a.item == self.trans
+ assert a.price == 1.5
+ assert not a.voucher
+
+ @classscope(attr='orga')
+ def test_discounted_voucher_on_base_product(self):
+ v = self.event.vouchers.create(code="foo", item=self.ticket, price_mode='subtract', value=Decimal('1.50'))
+ self.cm.add_new_items([
+ {
+ 'item': self.ticket.pk,
+ 'variation': None,
+ 'voucher': v.code,
+ 'count': 1
+ }
+ ])
+ self.cm.commit()
+ cp = CartPosition.objects.get(addon_to__isnull=True)
+ assert cp.item == self.ticket
+ assert cp.price == 23 - 1.5 - 1.5
+ assert cp.addons.count() == 1
+ assert cp.voucher == v
+ assert cp.price_before_voucher == 23 - 1.5
a = cp.addons.get()
assert a.item == self.trans
assert a.price == 1.5
diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py
index b2a548fb37..03091ef7db 100644
--- a/src/tests/presale/test_checkout.py
+++ b/src/tests/presale/test_checkout.py
@@ -3000,3 +3000,140 @@ class CheckoutSeatingTest(BaseCheckoutTestCase, TestCase):
with self.assertRaises(OrderError):
_perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
assert not CartPosition.objects.filter(pk=self.cp1.pk).exists()
+
+
+class CheckoutVoucherBudgetTest(BaseCheckoutTestCase, TestCase):
+ @scopes_disabled()
+ def setUp(self):
+ super().setUp()
+ self.v = Voucher.objects.create(item=self.ticket, value=Decimal('21.50'), event=self.event, price_mode='set',
+ valid_until=now() + timedelta(days=2), max_usages=999, redeemed=0)
+ self.cp1 = CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price_before_voucher=23, price=21.5, expires=now() + timedelta(minutes=10), voucher=self.v
+ )
+ self.cp2 = CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price_before_voucher=23, price=21.5, expires=now() + timedelta(minutes=10), voucher=self.v
+ )
+
+ @scopes_disabled()
+ def test_no_budget(self):
+ oid = _perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ o = Order.objects.get(pk=oid)
+ op = o.positions.first()
+ assert op.item == self.ticket
+ assert op.price_before_voucher == Decimal('23.00')
+
+ @scopes_disabled()
+ def test_budget_exceeded_for_second_order(self):
+ self.v.budget = Decimal('1.50')
+ self.v.save()
+ oid = _perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ o = Order.objects.get(pk=oid)
+ op = o.positions.first()
+ assert op.item == self.ticket
+
+ with self.assertRaises(OrderError):
+ _perform_order(self.event, 'manual', [self.cp2.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ self.cp2.refresh_from_db()
+ assert self.cp2.price == Decimal('23.00')
+
+ @scopes_disabled()
+ def test_budget_exceeded_between_positions(self):
+ self.v.budget = Decimal('1.50')
+ self.v.save()
+ with self.assertRaises(OrderError):
+ _perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ self.cp1.refresh_from_db()
+ assert self.cp1.price == Decimal('21.50')
+ self.cp2.refresh_from_db()
+ assert self.cp2.price == Decimal('23.00')
+
+ @scopes_disabled()
+ def test_budget_exceeded_in_first_position(self):
+ self.v.budget = Decimal('1.00')
+ self.v.save()
+ with self.assertRaises(OrderError):
+ _perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ self.cp1.refresh_from_db()
+ assert self.cp1.price == Decimal('22.00')
+ self.cp2.refresh_from_db()
+ assert self.cp2.price == Decimal('23.00')
+
+ @scopes_disabled()
+ def test_budget_exceeded_in_second_position(self):
+ self.v.budget = Decimal('2.50')
+ self.v.save()
+ with self.assertRaises(OrderError):
+ _perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ self.cp1.refresh_from_db()
+ assert self.cp1.price == Decimal('21.50')
+ self.cp2.refresh_from_db()
+ assert self.cp2.price == Decimal('22.00')
+
+ @scopes_disabled()
+ def test_budget_exceeded_during_price_change(self):
+ self.v.budget = Decimal('2.50')
+ self.v.value = Decimal('21.00')
+ self.v.save()
+ self.cp1.expires = now() - timedelta(hours=1)
+ self.cp1.save()
+ self.cp2.expires = now() - timedelta(hours=1)
+ self.cp2.save()
+
+ with self.assertRaises(OrderError):
+ _perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ self.cp1.refresh_from_db()
+ assert self.cp1.price == Decimal('21.00')
+ self.cp2.refresh_from_db()
+ assert self.cp2.price == Decimal('22.50')
+
+ @scopes_disabled()
+ def test_budget_exceeded_expired_cart(self):
+ self.v.budget = Decimal('0.00')
+ self.v.value = Decimal('21.00')
+ self.v.save()
+ self.cp1.expires = now() - timedelta(hours=1)
+ self.cp1.save()
+ self.cp2.expires = now() - timedelta(hours=1)
+ self.cp2.save()
+
+ with self.assertRaises(OrderError):
+ _perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ self.cp1.refresh_from_db()
+ assert self.cp1.price == Decimal('23.00')
+ self.cp2.refresh_from_db()
+ assert self.cp2.price == Decimal('23.00')
+
+ @scopes_disabled()
+ def test_budget_overbooked_expired_cart(self):
+ self.v.budget = Decimal('1.50')
+ self.v.value = Decimal('21.50')
+ self.v.save()
+ self.cp1.expires = now() - timedelta(hours=1)
+ self.cp1.save()
+ self.cp2.expires = now() - timedelta(hours=1)
+ self.cp2.save()
+ oid = _perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ o = Order.objects.get(pk=oid)
+ op = o.positions.first()
+
+ assert op.item == self.ticket
+ self.v.budget = Decimal('1.00')
+ self.v.save()
+
+ with self.assertRaises(OrderError):
+ _perform_order(self.event, 'manual', [self.cp2.pk], 'admin@example.org', 'en', None, {},
+ 'web')
+ self.cp2.refresh_from_db()
+ assert self.cp2.price == Decimal('23.00')
|