Data model changes

This commit is contained in:
Raphael Michel
2019-12-15 18:28:51 +01:00
parent 018d345008
commit 089a468a5d
12 changed files with 131 additions and 14 deletions

View File

@@ -850,7 +850,17 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
addon_to=pos.addon_to, addon_to=pos.addon_to,
invoice_address=ia, 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 = price.gross
pos.price_before_voucher = pbv
pos.tax_rate = price.rate pos.tax_rate = price.rate
pos.tax_value = price.tax pos.tax_value = price.tax
pos.tax_rule = pos.item.tax_rule pos.tax_rule = pos.item.tax_rule

View File

@@ -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),
),
]

View File

@@ -991,6 +991,9 @@ class AbstractPosition(models.Model):
verbose_name=_("Variation"), verbose_name=_("Variation"),
on_delete=models.PROTECT on_delete=models.PROTECT
) )
price_before_voucher = models.DecimalField(
decimal_places=2, max_digits=10, null=True,
)
price = models.DecimalField( price = models.DecimalField(
decimal_places=2, max_digits=10, decimal_places=2, max_digits=10,
verbose_name=_("Price") verbose_name=_("Price")

View File

@@ -4,7 +4,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator from django.core.validators import MinLengthValidator
from django.db import models 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.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
@@ -17,7 +17,7 @@ from ..decimal import round_decimal
from .base import LoggedModel from .base import LoggedModel
from .event import Event, SubEvent from .event import Event, SubEvent
from .items import Item, ItemVariation, Quota from .items import Item, ItemVariation, Quota
from .orders import Order from .orders import CartPosition, Order, OrderPosition
def _generate_random_code(prefix=None): def _generate_random_code(prefix=None):
@@ -114,6 +114,13 @@ class Voucher(LoggedModel):
verbose_name=_("Redeemed"), verbose_name=_("Redeemed"),
default=0 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( valid_until = models.DateTimeField(
blank=True, null=True, db_index=True, blank=True, null=True, db_index=True,
verbose_name=_("Valid until") verbose_name=_("Valid until")
@@ -470,3 +477,38 @@ class Voucher(LoggedModel):
return self.item.seat_category_mappings.filter(**kwargs).exists() return self.item.seat_category_mappings.filter(**kwargs).exists()
else: else:
return bool(subevent.seating_plan) if subevent else self.event.seating_plan 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

View File

@@ -108,11 +108,12 @@ error_messages = {
class CartManager: class CartManager:
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas', 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',)) RemoveOperation = namedtuple('RemoveOperation', ('position',))
VoucherOperation = namedtuple('VoucherOperation', ('position', 'voucher', 'price')) VoucherOperation = namedtuple('VoucherOperation', ('position', 'voucher', 'price'))
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher', ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
'quotas', 'subevent', 'seat')) 'quotas', 'subevent', 'seat', 'price_before_voucher'))
order = { order = {
RemoveOperation: 10, RemoveOperation: 10,
VoucherOperation: 15, VoucherOperation: 15,
@@ -384,6 +385,7 @@ class CartManager:
else: else:
price = self._get_price(cp.item, cp.variation, cp.voucher, price, cp.subevent, price = self._get_price(cp.item, cp.variation, cp.voucher, price, cp.subevent,
force_custom_price=True) force_custom_price=True)
pbv = TAXED_ZERO
else: else:
bundled_sum = Decimal('0.00') bundled_sum = Decimal('0.00')
if not cp.addon_to_id: 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, price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
cp_is_net=True, bundled_sum=bundled_sum) cp_is_net=True, bundled_sum=bundled_sum)
price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='') 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: else:
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
bundled_sum=bundled_sum) 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) quotas = list(cp.quotas)
if not quotas: if not quotas:
@@ -414,7 +421,7 @@ class CartManager:
op = self.ExtendOperation( op = self.ExtendOperation(
position=cp, item=cp.item, variation=cp.variation, voucher=cp.voucher, count=1, 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) self._check_item_constraints(op)
@@ -569,16 +576,18 @@ class CartManager:
bop = self.AddOperation( bop = self.AddOperation(
count=bundle.count, item=bitem, variation=bvar, price=bprice, count=bundle.count, item=bitem, variation=bvar, price=bprice,
voucher=None, quotas=bundle_quotas, addon_to='FAKE', subevent=subevent, 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) self._check_item_constraints(bop)
bundled.append(bop) bundled.append(bop)
price = self._get_price(item, variation, voucher, i.get('price'), subevent, bundled_sum=bundled_sum) 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( op = self.AddOperation(
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas, 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) self._check_item_constraints(op)
operations.append(op) operations.append(op)
@@ -898,7 +907,8 @@ class CartManager:
event=self.event, item=op.item, variation=op.variation, event=self.event, item=op.item, variation=op.variation,
price=op.price.gross, expires=self._expiry, cart_id=self.cart_id, 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, 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: if self.event.settings.attendee_names_asked:
scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme) scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
@@ -945,6 +955,7 @@ class CartManager:
elif available_count == 1: elif available_count == 1:
op.position.expires = self._expiry op.position.expires = self._expiry
op.position.price = op.price.gross op.position.price = op.price.gross
op.position.price_before_voucher = op.price_before_voucher.gross
try: try:
op.position.save(force_update=True) op.position.save(force_update=True)
except DatabaseError: except DatabaseError:
@@ -964,6 +975,7 @@ class CartManager:
# be expected # be expected
continue continue
op.position.price_before_voucher = op.position.price
op.position.price = op.price.gross op.position.price = op.price.gross
op.position.voucher = op.voucher op.position.voucher = op.voucher
op.position.save() op.position.save()

View File

@@ -530,6 +530,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
bprice = cp.price bprice = cp.price
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False, 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)
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 changed_prices[cp.pk] = bprice
else: else:
bundled_sum = 0 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, 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)
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: if price is False or len(quotas) == 0:
err = err or error_messages['unavailable'] err = err or error_messages['unavailable']
@@ -552,6 +556,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
delete(cp) delete(cp)
continue continue
cp.price_before_voucher = pbv.gross
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross): if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
cp.price = price.gross cp.price = price.gross
cp.includes_tax = bool(price.rate) cp.includes_tax = bool(price.rate)

View File

@@ -38,7 +38,7 @@ class VoucherForm(I18nModelForm):
localized_fields = '__all__' localized_fields = '__all__'
fields = [ fields = [
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', '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 = { field_classes = {
'valid_until': SplitDateTimeField, 'valid_until': SplitDateTimeField,
@@ -268,7 +268,7 @@ class VoucherBulkForm(VoucherForm):
localized_fields = '__all__' localized_fields = '__all__'
fields = [ fields = [
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment', '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 = { field_classes = {
'valid_until': SplitDateTimeField, 'valid_until': SplitDateTimeField,

View File

@@ -272,7 +272,9 @@
{% endif %} {% endif %}
{% if line.voucher %} {% if line.voucher %}
<br/><span class="fa fa-tags"></span> {% trans "Voucher code used:" %} <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 }} {{ line.voucher.code }}
</a> </a>
{% endif %} {% endif %}

View File

@@ -73,6 +73,7 @@
<legend>{% trans "Advanced settings" %}</legend> <legend>{% trans "Advanced settings" %}</legend>
{% bootstrap_field form.block_quota layout="control" %} {% bootstrap_field form.block_quota layout="control" %}
{% bootstrap_field form.allow_ignore_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.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %} {% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.show_hidden_items layout="control" %} {% bootstrap_field form.show_hidden_items layout="control" %}

View File

@@ -75,6 +75,7 @@
<legend>{% trans "Advanced settings" %}</legend> <legend>{% trans "Advanced settings" %}</legend>
{% bootstrap_field form.block_quota layout="control" %} {% bootstrap_field form.block_quota layout="control" %}
{% bootstrap_field form.allow_ignore_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.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %} {% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.show_hidden_items layout="control" %} {% bootstrap_field form.show_hidden_items layout="control" %}

View File

@@ -2,6 +2,7 @@
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load urlreplace %} {% load urlreplace %}
{% load money %}
{% block title %}{% trans "Vouchers" %}{% endblock %} {% block title %}{% trans "Vouchers" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Vouchers" %}</h1> <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> <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 %} {% if not v.is_active %}</del>{% endif %}
</td> </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.valid_until|date }}</td>
<td> <td>
{{ v.tag }} {{ v.tag }}

View File

@@ -37,9 +37,11 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
permission = 'can_view_vouchers' permission = 'can_view_vouchers'
def get_queryset(self): 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' 'item', 'variation', 'seat'
) ))
if self.filter_form.is_valid(): if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs) qs = self.filter_form.filter_qs(qs)