From 089a468a5dbb7bed000ca48ec7e183e0362a6af6 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Sun, 15 Dec 2019 18:28:51 +0100 Subject: [PATCH] Data model changes --- src/pretix/api/serializers/order.py | 10 ++++ .../migrations/0142_auto_20191215_1522.py | 29 ++++++++++++ src/pretix/base/models/orders.py | 3 ++ src/pretix/base/models/vouchers.py | 46 ++++++++++++++++++- src/pretix/base/services/cart.py | 24 +++++++--- src/pretix/base/services/orders.py | 6 +++ src/pretix/control/forms/vouchers.py | 4 +- .../templates/pretixcontrol/order/index.html | 4 +- .../pretixcontrol/vouchers/bulk.html | 1 + .../pretixcontrol/vouchers/detail.html | 1 + .../pretixcontrol/vouchers/index.html | 11 ++++- src/pretix/control/views/vouchers.py | 6 ++- 12 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 src/pretix/base/migrations/0142_auto_20191215_1522.py 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 @@ {% trans "Advanced settings" %} {% 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 @@ {% trans "Advanced settings" %} {% 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)