forked from CGM_Public/pretix_original
Allow to mark add-ons as "included" in price
This commit is contained in:
70
src/pretix/base/migrations/0067_auto_20170712_1610.py
Normal file
70
src/pretix/base/migrations/0067_auto_20170712_1610.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.2 on 2017-07-12 16:10
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0066_auto_20170708_2102'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='subevent',
|
||||||
|
options={'ordering': ('date_from', 'name'), 'verbose_name': 'Date in event series', 'verbose_name_plural': 'Dates in event series'},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='itemaddon',
|
||||||
|
name='price_included',
|
||||||
|
field=models.BooleanField(default=False, help_text='If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost money individually.', verbose_name='Add-Ons are included in the price'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='subevent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='has_subevents',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Event series'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='subevent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='quota',
|
||||||
|
name='subevent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.SubEvent', verbose_name='Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='subevent',
|
||||||
|
name='active',
|
||||||
|
field=models.BooleanField(default=False, help_text='Only with this checkbox enabled, this date is visible in the frontend to users.', verbose_name='Active'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='subevent',
|
||||||
|
name='presale_end',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.', null=True, verbose_name='End of presale'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='subevent',
|
||||||
|
name='presale_start',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.', null=True, verbose_name='Start of presale'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='voucher',
|
||||||
|
name='subevent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='waitinglistentry',
|
||||||
|
name='subevent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -477,6 +477,12 @@ class ItemAddOn(models.Model):
|
|||||||
default=1,
|
default=1,
|
||||||
verbose_name=_('Maximum number')
|
verbose_name=_('Maximum number')
|
||||||
)
|
)
|
||||||
|
price_included = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_('Add-Ons are included in the price'),
|
||||||
|
help_text=_('If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost '
|
||||||
|
'money individually.')
|
||||||
|
)
|
||||||
position = models.PositiveIntegerField(
|
position = models.PositiveIntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
verbose_name=_("Position")
|
verbose_name=_("Position")
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ class CartManager:
|
|||||||
quota_diff = Counter() # Quota -> Number of usages
|
quota_diff = Counter() # Quota -> Number of usages
|
||||||
operations = []
|
operations = []
|
||||||
available_categories = defaultdict(set) # CartPos -> Category IDs to choose from
|
available_categories = defaultdict(set) # CartPos -> Category IDs to choose from
|
||||||
|
price_included = defaultdict(dict) # CartPos -> CategoryID -> bool(price is included)
|
||||||
toplevel_cp = self.positions.filter(
|
toplevel_cp = self.positions.filter(
|
||||||
addon_to__isnull=True
|
addon_to__isnull=True
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
@@ -349,6 +350,7 @@ class CartManager:
|
|||||||
# Prefill some of the cache containers
|
# Prefill some of the cache containers
|
||||||
for cp in toplevel_cp:
|
for cp in toplevel_cp:
|
||||||
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
|
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
|
||||||
|
price_included[cp.pk] = {iao.addon_category_id: iao.price_included for iao in cp.item.addons.all()}
|
||||||
cpcache[cp.pk] = cp
|
cpcache[cp.pk] = cp
|
||||||
current_addons[cp] = {
|
current_addons[cp] = {
|
||||||
(a.item_id, a.variation_id): a
|
(a.item_id, a.variation_id): a
|
||||||
@@ -392,7 +394,10 @@ class CartManager:
|
|||||||
for quota in quotas:
|
for quota in quotas:
|
||||||
quota_diff[quota] += 1
|
quota_diff[quota] += 1
|
||||||
|
|
||||||
price = self._get_price(item, variation, None, None, cp.subevent)
|
if price_included[cp.pk].get(item.category_id):
|
||||||
|
price = Decimal('0.00')
|
||||||
|
else:
|
||||||
|
price = self._get_price(item, variation, None, None, cp.subevent)
|
||||||
|
|
||||||
op = self.AddOperation(
|
op = self.AddOperation(
|
||||||
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
|
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
|
||||||
|
|||||||
@@ -282,7 +282,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
# Other checks are not necessary
|
# Other checks are not necessary
|
||||||
continue
|
continue
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
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']
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.models import Item, ItemVariation, Voucher
|
from pretix.base.models import (
|
||||||
|
AbstractPosition, Item, ItemAddOn, ItemVariation, Voucher,
|
||||||
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
|
|
||||||
|
|
||||||
def get_price(item: Item, variation: ItemVariation = None,
|
def get_price(item: Item, variation: ItemVariation = None,
|
||||||
voucher: Voucher = None, custom_price: Decimal = None,
|
voucher: Voucher = None, custom_price: Decimal = None,
|
||||||
subevent: SubEvent = None, custom_price_is_net: bool = False):
|
subevent: SubEvent = None, custom_price_is_net: bool = False,
|
||||||
|
addon_to: AbstractPosition = None):
|
||||||
|
if addon_to:
|
||||||
|
try:
|
||||||
|
iao = addon_to.item.addons.get(addon_category_id=item.category_id)
|
||||||
|
if iao.price_included:
|
||||||
|
return Decimal('0.00')
|
||||||
|
except ItemAddOn.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
price = item.default_price
|
price = item.default_price
|
||||||
if subevent and item.pk in subevent.item_price_overrides:
|
if subevent and item.pk in subevent.item_price_overrides:
|
||||||
price = subevent.item_price_overrides[item.pk]
|
price = subevent.item_price_overrides[item.pk]
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ class ItemAddOnForm(I18nModelForm):
|
|||||||
'addon_category',
|
'addon_category',
|
||||||
'min_count',
|
'min_count',
|
||||||
'max_count',
|
'max_count',
|
||||||
|
'price_included'
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'min_count': _('Be aware that setting a minimal number makes it impossible to buy this product if all '
|
'min_count': _('Be aware that setting a minimal number makes it impossible to buy this product if all '
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
{% bootstrap_field form.addon_category layout='horizontal' %}
|
{% bootstrap_field form.addon_category layout='horizontal' %}
|
||||||
{% bootstrap_field form.min_count layout='horizontal' %}
|
{% bootstrap_field form.min_count layout='horizontal' %}
|
||||||
{% bootstrap_field form.max_count layout='horizontal' %}
|
{% bootstrap_field form.max_count layout='horizontal' %}
|
||||||
|
{% bootstrap_field form.price_included layout='horizontal' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
{% bootstrap_field formset.empty_form.addon_category layout='horizontal' %}
|
{% bootstrap_field formset.empty_form.addon_category layout='horizontal' %}
|
||||||
{% bootstrap_field formset.empty_form.min_count layout='horizontal' %}
|
{% bootstrap_field formset.empty_form.min_count layout='horizontal' %}
|
||||||
{% bootstrap_field formset.empty_form.max_count layout='horizontal' %}
|
{% bootstrap_field formset.empty_form.max_count layout='horizontal' %}
|
||||||
|
{% bootstrap_field formset.empty_form.price_included layout='horizontal' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endescapescript %}
|
{% endescapescript %}
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
event=self.request.event,
|
event=self.request.event,
|
||||||
prefix='{}_{}'.format(cartpos.pk, iao.addon_category.pk),
|
prefix='{}_{}'.format(cartpos.pk, iao.addon_category.pk),
|
||||||
category=iao.addon_category,
|
category=iao.addon_category,
|
||||||
|
price_included=iao.price_included,
|
||||||
initial=current_addon_products,
|
initial=current_addon_products,
|
||||||
data=(self.request.POST if self.request.method == 'POST' else None),
|
data=(self.request.POST if self.request.method == 'POST' else None),
|
||||||
quota_cache=quota_cache,
|
quota_cache=quota_cache,
|
||||||
|
|||||||
@@ -293,6 +293,9 @@ class AddOnsForm(forms.Form):
|
|||||||
tax_value = round_decimal(price * (1 - 100 / (100 + item.tax_rate)))
|
tax_value = round_decimal(price * (1 - 100 / (100 + item.tax_rate)))
|
||||||
price_net = price - tax_value
|
price_net = price - tax_value
|
||||||
|
|
||||||
|
if self.price_included:
|
||||||
|
price = Decimal('0.00')
|
||||||
|
|
||||||
if not price:
|
if not price:
|
||||||
n = '{name}'.format(
|
n = '{name}'.format(
|
||||||
name=label
|
name=label
|
||||||
@@ -336,6 +339,7 @@ class AddOnsForm(forms.Form):
|
|||||||
current_addons = kwargs.pop('initial')
|
current_addons = kwargs.pop('initial')
|
||||||
quota_cache = kwargs.pop('quota_cache')
|
quota_cache = kwargs.pop('quota_cache')
|
||||||
item_cache = kwargs.pop('item_cache')
|
item_cache = kwargs.pop('item_cache')
|
||||||
|
self.price_included = kwargs.pop('price_included')
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -1306,6 +1306,26 @@ class CartAddonTest(CartTestMixin, TestCase):
|
|||||||
self.addon1 = ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat)
|
self.addon1 = ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat)
|
||||||
self.cm = CartManager(event=self.event, cart_id=self.session_key)
|
self.cm = CartManager(event=self.event, cart_id=self.session_key)
|
||||||
|
|
||||||
|
def test_cart_set_simple_addon_included(self):
|
||||||
|
self.addon1.price_included = True
|
||||||
|
self.addon1.save()
|
||||||
|
cp1 = CartPosition.objects.create(
|
||||||
|
expires=now() + timedelta(minutes=10), item=self.ticket, price=Decimal('23.00'),
|
||||||
|
event=self.event, cart_id=self.session_key
|
||||||
|
)
|
||||||
|
|
||||||
|
self.cm.set_addons([
|
||||||
|
{
|
||||||
|
'addon_to': cp1.pk,
|
||||||
|
'item': self.workshop1.pk,
|
||||||
|
'variation': None
|
||||||
|
}
|
||||||
|
])
|
||||||
|
self.cm.commit()
|
||||||
|
cp2 = cp1.addons.first()
|
||||||
|
assert cp2.item == self.workshop1
|
||||||
|
assert cp2.price == 0
|
||||||
|
|
||||||
def test_cart_set_simple_addon(self):
|
def test_cart_set_simple_addon(self):
|
||||||
cp1 = CartPosition.objects.create(
|
cp1 = CartPosition.objects.create(
|
||||||
expires=now() + timedelta(minutes=10), item=self.ticket, price=Decimal('23.00'),
|
expires=now() + timedelta(minutes=10), item=self.ticket, price=Decimal('23.00'),
|
||||||
@@ -1322,6 +1342,7 @@ class CartAddonTest(CartTestMixin, TestCase):
|
|||||||
self.cm.commit()
|
self.cm.commit()
|
||||||
cp2 = cp1.addons.first()
|
cp2 = cp1.addons.first()
|
||||||
assert cp2.item == self.workshop1
|
assert cp2.item == self.workshop1
|
||||||
|
assert cp2.price == 12
|
||||||
|
|
||||||
def test_cart_subevent_set_simple_addon(self):
|
def test_cart_subevent_set_simple_addon(self):
|
||||||
self.event.has_subevents = True
|
self.event.has_subevents = True
|
||||||
@@ -1345,6 +1366,7 @@ class CartAddonTest(CartTestMixin, TestCase):
|
|||||||
cp2 = cp1.addons.first()
|
cp2 = cp1.addons.first()
|
||||||
assert cp2.item == self.workshop1
|
assert cp2.item == self.workshop1
|
||||||
assert cp2.subevent == se
|
assert cp2.subevent == se
|
||||||
|
assert cp2.value == 12
|
||||||
|
|
||||||
def test_cart_subevent_set_addon_for_wrong_subevent(self):
|
def test_cart_subevent_set_addon_for_wrong_subevent(self):
|
||||||
self.event.has_subevents = True
|
self.event.has_subevents = True
|
||||||
|
|||||||
@@ -407,6 +407,25 @@ class CheckoutTestCase(TestCase):
|
|||||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||||
self.assertEqual(cr1.price, 24)
|
self.assertEqual(cr1.price, 24)
|
||||||
|
|
||||||
|
def test_addon_price_included(self):
|
||||||
|
ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat, min_count=1,
|
||||||
|
price_included=True)
|
||||||
|
cp1 = CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=23, expires=now() - timedelta(minutes=10)
|
||||||
|
)
|
||||||
|
CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.workshop1,
|
||||||
|
price=0, expires=now() - timedelta(minutes=10),
|
||||||
|
addon_to=cp1
|
||||||
|
)
|
||||||
|
|
||||||
|
self._set_session('payment', 'banktransfer')
|
||||||
|
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||||
|
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||||
|
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||||
|
self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).last().price, 0)
|
||||||
|
|
||||||
def test_confirm_price_changed(self):
|
def test_confirm_price_changed(self):
|
||||||
self.ticket.default_price = 24
|
self.ticket.default_price = 24
|
||||||
self.ticket.save()
|
self.ticket.save()
|
||||||
@@ -953,6 +972,23 @@ class CheckoutTestCase(TestCase):
|
|||||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug))
|
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug))
|
||||||
self.assertRedirects(response, '/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug),
|
self.assertRedirects(response, '/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug),
|
||||||
target_status_code=200)
|
target_status_code=200)
|
||||||
|
response = self.client.get('/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug))
|
||||||
|
assert 'Workshop 1' in response.rendered_content
|
||||||
|
assert 'EUR 12.00' in response.rendered_content
|
||||||
|
|
||||||
|
def test_set_addons_included(self):
|
||||||
|
ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat, min_count=1,
|
||||||
|
price_included=True)
|
||||||
|
CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=23, expires=now() - timedelta(minutes=10)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||||
|
self.assertRedirects(response, '/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug),
|
||||||
|
target_status_code=200)
|
||||||
|
assert 'Workshop 1' in response.rendered_content
|
||||||
|
assert 'EUR 12.00' not in response.rendered_content
|
||||||
|
|
||||||
def test_set_addons_subevent(self):
|
def test_set_addons_subevent(self):
|
||||||
self.event.has_subevents = True
|
self.event.has_subevents = True
|
||||||
|
|||||||
Reference in New Issue
Block a user