diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py
index 53d62a3608..3a7c565e7e 100644
--- a/src/pretix/api/serializers/order.py
+++ b/src/pretix/api/serializers/order.py
@@ -850,7 +850,17 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
addon_to=pos.addon_to,
invoice_address=ia,
)
+ pbv = get_price(
+ item=pos.item,
+ variation=pos.variation,
+ voucher=None,
+ custom_price=None,
+ subevent=pos.subevent,
+ addon_to=pos.addon_to,
+ invoice_address=ia,
+ )
pos.price = price.gross
+ pos.price_before_voucher = pbv
pos.tax_rate = price.rate
pos.tax_value = price.tax
pos.tax_rule = pos.item.tax_rule
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..fa7d764cc6
--- /dev/null
+++ b/src/pretix/base/migrations/0142_auto_20191215_1522.py
@@ -0,0 +1,29 @@
+# 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..2ffb2dc338 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -991,6 +991,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..a84ac10f00 100644
--- a/src/pretix/base/models/vouchers.py
+++ b/src/pretix/base/models/vouchers.py
@@ -4,7 +4,7 @@ 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.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
@@ -17,7 +17,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 CartPosition, Order, OrderPosition
def _generate_random_code(prefix=None):
@@ -114,6 +114,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. If this is reached, "
+ "the voucher becomes inactive."),
+ 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")
@@ -470,3 +477,38 @@ 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
+ # TODO: reason about expired orders
+ ]
+ ).order_by().values('voucher_id').annotate(s=Sum(F('price_before_voucher') - F('price'))).values('s')
+ return qs.annotate(budget_used_orders=Subquery(opq, output_field=models.DecimalField(max_digits=10, decimal_places=2)))
+
+ def budget_used(self, ignore_cartpos=None):
+ ops = OrderPosition.objects.filter(
+ voucher=self,
+ price_before_voucher__isnull=False,
+ order__status__in=[
+ Order.STATUS_PAID,
+ Order.STATUS_PENDING
+ # TODO: reason about expired orders
+ ]
+ ).aggregate(s=Sum(F('price_before_voucher') - F('price')))['s'] or Decimal('0.00')
+ cpq = CartPosition.objects.filter(
+ voucher=self,
+ price_before_voucher__isnull=False,
+ expires__gt=now()
+ )
+ if isinstance(ignore_cartpos, (tuple, list)):
+ cpq = cpq.exclude(pk__in=ignore_cartpos)
+ else:
+ cpq = cpq.exclude(pk=ignore_cartpos)
+ cps = cpq.aggregate(s=Sum(F('price_before_voucher') - F('price')))['s'] or Decimal('0.00')
+ return ops + cps
diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py
index 936c4352a9..0f571adddd 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)
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.append(op)
@@ -898,7 +907,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 self.event.settings.attendee_names_asked:
scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
@@ -945,6 +955,7 @@ class CartManager:
elif available_count == 1:
op.position.expires = self._expiry
op.position.price = op.price.gross
+ op.position.price_before_voucher = op.price_before_voucher.gross
try:
op.position.save(force_update=True)
except DatabaseError:
@@ -964,6 +975,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 86497c9352..2902e4f825 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -530,6 +530,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
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)
+ pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
+ invoice_address=address, force_custom_price=True)
changed_prices[cp.pk] = bprice
else:
bundled_sum = 0
@@ -540,6 +542,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
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)
+ 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)
if price is False or len(quotas) == 0:
err = err or error_messages['unavailable']
@@ -552,6 +556,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
delete(cp)
continue
+ cp.price_before_voucher = pbv.gross
+
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)
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/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 %}