mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
Add a maximum budget to vouchers (#1526)
* Data model changes * Fix test failures * Adjustments * Some tests and API support * Check when extending orders * Make things more deterministic, fix style * Do not apply negative discounts * Update price_before_voucher on item/subevent changes * Add tests for price_before_voucher in combination with free price * Fix InvoiceAddress.DoesNotExist
This commit is contained in:
@@ -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()
|
||||
|
||||
30
src/pretix/base/migrations/0142_auto_20191215_1522.py
Normal file
30
src/pretix/base/migrations/0142_auto_20191215_1522.py
Normal file
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -80,6 +80,15 @@
|
||||
{{ position.custom_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if position.voucher and position.voucher.budget %}
|
||||
<div class="alert alert-warning">
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-sm-offset-3">
|
||||
<strong>{% trans "Current value" %}</strong>
|
||||
|
||||
@@ -272,7 +272,9 @@
|
||||
{% endif %}
|
||||
{% if line.voucher %}
|
||||
<br/><span class="fa fa-tags"></span> {% trans "Voucher code used:" %}
|
||||
<a href="{% url "control:event.voucher" event=request.event.slug organizer=request.event.organizer.slug voucher=line.voucher.pk %}">
|
||||
<a
|
||||
{% if line.price_before_voucher|default_if_none:"NONE" != "NONE" %}data-toggle="tooltip" title="{% blocktrans trimmed with price=line.price_before_voucher|money:request.event.currency %}Original price: {{ price }}{% endblocktrans %}"{% endif %}
|
||||
href="{% url "control:event.voucher" event=request.event.slug organizer=request.event.organizer.slug voucher=line.voucher.pk %}">
|
||||
{{ line.voucher.code }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% 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" %}
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% 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" %}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load urlreplace %}
|
||||
{% load money %}
|
||||
{% block title %}{% trans "Vouchers" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Vouchers" %}</h1>
|
||||
@@ -143,7 +144,15 @@
|
||||
<strong><a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}">{{ v.code }}</a></strong>
|
||||
{% if not v.is_active %}</del>{% endif %}
|
||||
</td>
|
||||
<td>{{ v.redeemed }} / {{ v.max_usages }}</td>
|
||||
<td>
|
||||
{{ v.redeemed }} / {{ v.max_usages }}
|
||||
{% if v.budget|default_if_none:"NONE" != "NONE" %}
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
{{ v.budget_used_orders|money:request.event.currency }} / {{ v.budget|money:request.event.currency }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ v.valid_until|date }}</td>
|
||||
<td>
|
||||
{{ v.tag }}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user