Allow to mark add-ons as "included" in price

This commit is contained in:
Raphael Michel
2017-07-12 18:42:09 +02:00
parent 79562e7ad9
commit c1158c3175
11 changed files with 163 additions and 4 deletions

View 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'),
),
]

View File

@@ -477,6 +477,12 @@ class ItemAddOn(models.Model):
default=1,
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(
default=0,
verbose_name=_("Position")

View File

@@ -340,6 +340,7 @@ class CartManager:
quota_diff = Counter() # Quota -> Number of usages
operations = []
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(
addon_to__isnull=True
).prefetch_related(
@@ -349,6 +350,7 @@ class CartManager:
# Prefill some of the cache containers
for cp in toplevel_cp:
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
current_addons[cp] = {
(a.item_id, a.variation_id): a
@@ -392,7 +394,10 @@ class CartManager:
for quota in quotas:
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(
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,

View File

@@ -282,7 +282,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
# Other checks are not necessary
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:
err = err or error_messages['unavailable']

View File

@@ -1,13 +1,24 @@
from decimal import 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
def get_price(item: Item, variation: ItemVariation = 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
if subevent and item.pk in subevent.item_price_overrides:
price = subevent.item_price_overrides[item.pk]

View File

@@ -302,6 +302,7 @@ class ItemAddOnForm(I18nModelForm):
'addon_category',
'min_count',
'max_count',
'price_included'
]
help_texts = {
'min_count': _('Be aware that setting a minimal number makes it impossible to buy this product if all '

View File

@@ -47,6 +47,7 @@
{% bootstrap_field form.addon_category layout='horizontal' %}
{% bootstrap_field form.min_count layout='horizontal' %}
{% bootstrap_field form.max_count layout='horizontal' %}
{% bootstrap_field form.price_included layout='horizontal' %}
</div>
</div>
{% endfor %}
@@ -78,6 +79,7 @@
{% bootstrap_field formset.empty_form.addon_category 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.price_included layout='horizontal' %}
</div>
</div>
{% endescapescript %}

View File

@@ -183,6 +183,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
event=self.request.event,
prefix='{}_{}'.format(cartpos.pk, iao.addon_category.pk),
category=iao.addon_category,
price_included=iao.price_included,
initial=current_addon_products,
data=(self.request.POST if self.request.method == 'POST' else None),
quota_cache=quota_cache,

View File

@@ -293,6 +293,9 @@ class AddOnsForm(forms.Form):
tax_value = round_decimal(price * (1 - 100 / (100 + item.tax_rate)))
price_net = price - tax_value
if self.price_included:
price = Decimal('0.00')
if not price:
n = '{name}'.format(
name=label
@@ -336,6 +339,7 @@ class AddOnsForm(forms.Form):
current_addons = kwargs.pop('initial')
quota_cache = kwargs.pop('quota_cache')
item_cache = kwargs.pop('item_cache')
self.price_included = kwargs.pop('price_included')
super().__init__(*args, **kwargs)