diff --git a/src/pretix/base/migrations/0017_auto_20160324_1615.py b/src/pretix/base/migrations/0017_auto_20160324_1615.py new file mode 100644 index 0000000000..d059935dfb --- /dev/null +++ b/src/pretix/base/migrations/0017_auto_20160324_1615.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-03-24 16:15 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0016_voucher_variation'), + ] + + operations = [ + migrations.AlterModelOptions( + name='logentry', + options={'ordering': ('-datetime',)}, + ), + migrations.AddField( + model_name='item', + name='free_price', + field=models.BooleanField(default=False, help_text='If this option is active, your users can choose the price themselves. The price configured above is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect additional donations for your event.', verbose_name='Free price'), + ), + ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index d5d1d0cf28..6e2bc7aad1 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -3,7 +3,7 @@ from datetime import datetime from decimal import Decimal from django.db import models -from django.db.models import Q, Case, Count, Sum, When +from django.db.models import Q from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -132,6 +132,13 @@ class Item(LoggedModel): verbose_name=_("Default price"), max_digits=7, decimal_places=2, null=True ) + free_price = models.BooleanField( + default=False, + verbose_name=_("Free price input"), + help_text=_("If this option is active, your users can choose the price themselves. The price configured above " + "is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect " + "additional donations for your event.") + ) tax_rate = models.DecimalField( verbose_name=_("Taxes included in percent"), max_digits=7, decimal_places=2, diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 23cd9df727..d1b7be3b2e 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from decimal import Decimal from django.conf import settings from django.db.models import Q @@ -51,7 +52,7 @@ def _re_add_expired_positions(items: List[CartPosition], event: Event, cart_id: Q(cart_id=cart_id) & Q(event=event) & Q(expires__lte=now()) ) for cp in expired: - items.insert(0, (cp.item_id, cp.variation_id, 1, cp)) + items.insert(0, (cp.item_id, cp.variation_id, 1, cp.price, cp)) positions.add(cp) return positions @@ -69,7 +70,7 @@ def _check_date(event: Event) -> None: raise CartError(error_messages['ended']) -def _add_new_items(event: Event, items: List[Tuple[int, Optional[int], int]], +def _add_new_items(event: Event, items: List[Tuple[int, Optional[int], int, Optional[str]]], cart_id: str, expiry: datetime) -> Optional[str]: err = None @@ -114,20 +115,24 @@ def _add_new_items(event: Event, items: List[Tuple[int, Optional[int], int]], err = err or error_messages['in_part'] quota_ok = min(quota_ok, avail[1]) + price = item.default_price if variation is None else ( + variation.default_price if variation.default_price is not None else item.default_price) + if item.free_price and len(i) > 3 and i[3]: + custom_price = Decimal(i[3].replace(",", ".")) + price = max(custom_price, price) + # Create a CartPosition for as much items as we can for k in range(quota_ok): - if len(i) > 3 and i[2] == 1: + if len(i) > 4 and i[2] == 1: # Recreating - cp = i[3] + cp = i[4] cp.expires = expiry - cp.price = item.default_price if variation is None else ( - variation.default_price if variation.default_price is not None else item.default_price) + cp.price = price cp.save() else: CartPosition.objects.create( event=event, item=item, variation=variation, - price=item.default_price if variation is None else ( - variation.default_price if variation.default_price is not None else item.default_price), + price=price, expires=expiry, cart_id=cart_id ) @@ -161,7 +166,7 @@ def _add_voucher(event: Event, voucher: str, expiry: datetime, cart_id: str): raise CartError(error_messages['voucher_invalid']) -def _add_items_to_cart(event: Event, items: List[Tuple[int, Optional[int], int]], cart_id: str=None, +def _add_items_to_cart(event: Event, items: List[Tuple[int, Optional[int], int, Optional[str]]], cart_id: str=None, voucher: str=None) -> None: with event.lock(): _check_date(event) @@ -186,12 +191,12 @@ def _add_items_to_cart(event: Event, items: List[Tuple[int, Optional[int], int]] _add_voucher(event, voucher, expiry, cart_id) -def add_items_to_cart(event: int, items: List[Tuple[int, Optional[int], int]], cart_id: str=None, +def add_items_to_cart(event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]], cart_id: str=None, voucher: str=None) -> None: """ Adds a list of items to a user's cart. :param event: The event ID in question - :param items: A list of tuple of the form (item id, variation id or None, number) + :param items: A list of tuple of the form (item id, variation id or None, number, custom_price) :param session: Session ID of a guest :param coupon: A coupon that should also be reeemed :raises CartError: On any error that occured @@ -203,19 +208,29 @@ def add_items_to_cart(event: int, items: List[Tuple[int, Optional[int], int]], c raise CartError(error_messages['busy']) -def _remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], int]], cart_id: str) -> None: +def _remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]], + cart_id: str) -> None: with event.lock(): - for item, variation, cnt in items: + for item, variation, cnt, price in items: cw = Q(cart_id=cart_id) & Q(item_id=item) & Q(event=event) if variation: cw &= Q(variation_id=variation) else: cw &= Q(variation__isnull=True) - for cp in CartPosition.objects.filter(cw).order_by("-price")[:cnt]: - cp.delete() + # Prefer to delete positions that have the same price as the one the user clicked on, after thet + # prefer the most expensive ones. + if price: + correctprice = CartPosition.objects.filter(cw).filter(price=Decimal(price.replace(",", ".")))[:cnt] + for cp in correctprice: + cp.delete() + cnt -= len(correctprice) + if cnt > 0: + for cp in CartPosition.objects.filter(cw).order_by("-price")[:cnt]: + cp.delete() -def remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], int]], cart_id: str=None) -> None: +def remove_items_from_cart(event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]], + cart_id: str=None) -> None: """ Removes a list of items from a user's cart. :param event: The event ID in question @@ -233,8 +248,8 @@ if settings.HAS_CELERY: from pretix.celery import app @app.task(bind=True, max_retries=5, default_retry_delay=1) - def add_items_to_cart_task(self, event: int, items: List[Tuple[int, Optional[int], int]], cart_id: str, - voucher: str=None): + def add_items_to_cart_task(self, event: int, items: List[Tuple[int, Optional[int], int, Optional[str]]], + cart_id: str, voucher: str=None): event = Event.objects.get(id=event) try: try: @@ -245,7 +260,8 @@ if settings.HAS_CELERY: return e @app.task(bind=True, max_retries=5, default_retry_delay=1) - def remove_items_from_cart_task(self, event: int, items: List[Tuple[int, Optional[int], int]], cart_id: str): + def remove_items_from_cart_task(self, event: int, items: List[Tuple[int, Optional[int], int]], + cart_id: str): event = Event.objects.get(id=event) try: try: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 85e832b5a0..926fcc9469 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -189,7 +189,7 @@ def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]): if cp.voucher.price is not None: price = cp.voucher.price - if price != cp.price: + if price != cp.price and not (cp.item.free_price and cp.price > price): positions[i] = cp cp.price = price cp.save() diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 7d8cdbe4b9..95d5986004 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -95,6 +95,7 @@ class ItemFormGeneral(I18nModelForm): 'description', 'picture', 'default_price', + 'free_price', 'tax_rate', 'available_from', 'available_until', diff --git a/src/pretix/control/templates/pretixcontrol/item/index.html b/src/pretix/control/templates/pretixcontrol/item/index.html index 078026ef4b..72aecb79e3 100644 --- a/src/pretix/control/templates/pretixcontrol/item/index.html +++ b/src/pretix/control/templates/pretixcontrol/item/index.html @@ -2,34 +2,35 @@ {% load i18n %} {% load bootstrap3 %} {% block inside %} -
diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html index 50ea2f3bcd..1e4e5d47e3 100644 --- a/src/pretix/presale/templates/pretixpresale/event/index.html +++ b/src/pretix/presale/templates/pretixpresale/event/index.html @@ -76,7 +76,7 @@ {% if item.description %}{{ item.description }}
{% endif %}{{ item.description }}
{% endif %} + {% if item.description %} +{{ item.description }}
{% endif %} {% if event.settings.show_quota_left %} {% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %} {% endif %}