Add add-on products

This commit is contained in:
Raphael Michel
2017-03-01 19:31:50 +01:00
parent 3f76be2287
commit 5f52963ce0
14 changed files with 772 additions and 25 deletions

View File

@@ -11,14 +11,17 @@ from django.views.generic.base import TemplateResponseMixin
from pretix.base.models import Order
from pretix.base.models.orders import InvoiceAddress
from pretix.base.services.cart import set_cart_addons
from pretix.base.services.orders import perform_order
from pretix.base.signals import register_payment_providers
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.checkout import ContactForm, InvoiceAddressForm
from pretix.presale.forms.checkout import (
AddOnsForm, ContactForm, InvoiceAddressForm,
)
from pretix.presale.signals import (
checkout_confirm_messages, checkout_flow_steps, order_meta_from_request,
)
from pretix.presale.views import CartMixin, get_cart_total
from pretix.presale.views import CartMixin, get_cart, get_cart_total
from pretix.presale.views.async import AsyncAction
from pretix.presale.views.questions import QuestionsViewMixin
@@ -126,6 +129,116 @@ class TemplateFlowStep(TemplateResponseMixin, BaseCheckoutFlowStep):
raise NotImplementedError()
class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
priority = 40
identifier = "addons"
template_name = "pretixpresale/event/checkout_addons.html"
task = set_cart_addons
known_errortypes = ['CartError']
def is_applicable(self, request):
return get_cart(request).filter(item__addons__isnull=False).exists()
def is_completed(self, request, warn=False):
for cartpos in get_cart(request).filter(addon_to__isnull=True).prefetch_related(
'item__addons', 'item__addons__addon_category', 'addons', 'addons__item'
):
a = cartpos.addons.all()
for iao in cartpos.item.addons.all():
found = len([1 for p in a if p.item.category_id == iao.addon_category_id])
if found < iao.min_count or found > iao.max_count:
return False
return True
@cached_property
def forms(self):
"""
A list of forms with one form for each cart position that has questions
the user can answer. All forms have a custom prefix, so that they can all be
submitted at once.
"""
formset = []
for cartpos in get_cart(self.request).filter(addon_to__isnull=True).prefetch_related(
'item__addons', 'item__addons__addon_category', 'addons', 'addons__variation'
):
current_addon_products = {
a.item_id: a.variation_id for a in cartpos.addons.all()
}
formsetentry = {
'cartpos': cartpos,
'item': cartpos.item,
'variation': cartpos.variation,
'categories': []
}
for iao in cartpos.item.addons.all():
category = {
'category': iao.addon_category,
'min_count': iao.min_count,
'max_count': iao.max_count,
'form': AddOnsForm(
event=self.request.event,
prefix='{}_{}'.format(cartpos.pk, iao.addon_category.pk),
category=iao.addon_category,
initial=current_addon_products,
data=(self.request.POST if self.request.method == 'POST' else None)
)
}
if len(category['form'].fields) > 0:
formsetentry['categories'].append(category)
formset.append(formsetentry)
return formset
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['forms'] = self.forms
return ctx
def get_success_message(self, value):
return None
def get_success_url(self, value):
return self.get_next_url(self.request)
def get_error_url(self):
return self.get_step_url()
def get(self, request):
self.request = request
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
return TemplateFlowStep.get(self, request)
def post(self, request, *args, **kwargs):
self.request = request
is_valid = True
data = []
for f in self.forms:
for c in f['categories']:
is_valid = is_valid and c['form'].is_valid()
if c['form'].is_valid():
for k, v in c['form'].cleaned_data.items():
itemid = int(k[5:])
if v is True:
data.append({
'addon_to': f['cartpos'].pk,
'item': itemid,
'variation': None
})
elif v:
data.append({
'addon_to': f['cartpos'].pk,
'item': itemid,
'variation': int(v)
})
if not is_valid:
return self.get(request, *args, **kwargs)
return self.do(self.request.event.id, data, self.request.session.session_key)
class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
priority = 50
identifier = "questions"
@@ -395,6 +508,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
DEFAULT_FLOW = (
AddOnsStep,
QuestionsStep,
PaymentStep,
ConfirmStep

View File

@@ -2,9 +2,12 @@ from decimal import Decimal
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count, Prefetch, Q
from django.utils.formats import number_format
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Question
from pretix.base.models import ItemVariation, Question
from pretix.base.models.orders import InvoiceAddress
@@ -141,3 +144,90 @@ class QuestionsForm(forms.Form):
# Cache the answer object for later use
field.answer = answers[0]
self.fields['question_%s' % q.id] = field
class AddOnsForm(forms.Form):
"""
This form class is responsible for selecting add-ons to a product in the cart.
"""
def _label(self, event, item_or_variation):
if isinstance(item_or_variation, ItemVariation):
variation = item_or_variation
item = item_or_variation.item
price = variation.price
price_net = variation.net_price
label = variation.value
else:
item = item_or_variation
price = item.default_price
price_net = item.default_price_net
label = item.name
if not item.tax_rate or not price:
return '{name} (+ {currency} {price})'.format(
name=label, currency=event.currency, price=number_format(price)
)
elif event.settings.display_net_prices:
return '{name} (+ {currency} {price} plus {taxes}% taxes)'.format(
name=label, currency=event.currency, price=number_format(price_net),
taxes=number_format(item.tax_rate)
)
else:
return '{name} (+ {currency} {price} incl. {taxes}% taxes)'.format(
name=label, currency=event.currency, price=number_format(price),
taxes=number_format(item.tax_rate)
)
def __init__(self, *args, **kwargs):
"""
Takes additional keyword arguments:
:param category: The category to choose from
:param event: The event this belongs to
"""
category = kwargs.pop('category')
event = kwargs.pop('event')
current_addons = kwargs.pop('initial')
super().__init__(*args, **kwargs)
items = category.items.filter(
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(hide_without_voucher=False)
).prefetch_related(
'variations__quotas', # for .availability()
Prefetch('quotas', queryset=event.quotas.all()),
Prefetch('variations', to_attr='available_variations',
queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).distinct()),
).annotate(
quotac=Count('quotas'),
has_variations=Count('variations')
).filter(
quotac__gt=0
).order_by('category__position', 'category_id', 'position', 'name')
for i in items:
if i.has_variations:
field = forms.ChoiceField(
choices=[('', '')] + [
(
v.pk,
self._label(event, v)
) for v in i.available_variations
],
label=i.name,
required=False,
widget=forms.RadioSelect,
initial=current_addons.get(i.pk)
)
else:
field = forms.BooleanField(
label=self._label(event, i),
required=False,
initial=i.pk in current_addons
)
self.fields['item_%s' % i.pk] = field

View File

@@ -0,0 +1,70 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Checkout" %}{% endblock %}
{% block content %}
<h2>{% trans "Checkout" %}</h2>
<p>
{% trans "For some of the products in your cart, you can choose additional options before you continue." %}
</p>
<form class="form-horizontal" method="post" data-asynctask>
{% csrf_token %}
<div class="panel-group" id="questions_group">
{% for form in forms %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#cp{{ form.pos.id }}">
<strong>{{ form.item.name }}</strong>
{% if form.variation %}
{{ form.variation }}
{% endif %}
</a>
</h4>
</div>
<div id="cp{{ form.pos.id }}"
class="panel-collapse collapsed in">
<div class="panel-body">
{% for c in form.categories %}
<fieldset>
<legend>{{ c.category.name }}</legend>
<p>
{% if c.min_count == c.max_count %}
{% blocktrans trimmed with min_count=c.min_count %}
You need to choose {{ min_count }} options from this category.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %}
You can choose between {{ min_count }} and {{ max_count }} options from
this category.
{% endblocktrans %}
{% endif %}
</p>
{% bootstrap_form c.form layout="horizontal" %}
</fieldset>
{% empty %}
<em>
{% trans "There are no add-ons available for this product." %}
</em>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row checkout-button-row">
<div class="col-md-4">
<a class="btn btn-block btn-default btn-lg"
href="{{ prev_url }}">
{% trans "Go back" %}
</a>
</div>
<div class="col-md-4 col-md-offset-4">
<button class="btn btn-block btn-primary btn-lg" type="submit">
{% trans "Continue" %}
</button>
</div>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}