Fix #1001 -- Add product bundles (#1041)

* Data model + Editor

* Cart and order management

* Rebase migrations

* Fix typos, add tests on cart handling

* Add tests for checkout and quotas

* Add API endpoints

* Validation of settings

* Front page tax display

* Voucher handling

* Widget foo

* Show correct net pricing

* Front page tests

* reverse charge foo

* Allow to require bundling

* Fix test failure on postgres
This commit is contained in:
Raphael Michel
2019-03-22 14:48:48 +00:00
committed by GitHub
parent c4b18a4c81
commit 90f881c48e
34 changed files with 2747 additions and 153 deletions

View File

@@ -196,7 +196,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
):
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])
found = len([1 for p in a if p.item.category_id == iao.addon_category_id and not p.is_bundled])
if found < iao.min_count or found > iao.max_count:
self._completed = False
return False
@@ -216,7 +216,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
'item__addons', 'item__addons__addon_category', 'addons', 'addons__variation',
).order_by('pk'):
current_addon_products = {
a.item_id: a.variation_id for a in cartpos.addons.all()
a.item_id: a.variation_id for a in cartpos.addons.all() if not a.is_bundled
}
formsetentry = {
'cartpos': cartpos,

View File

@@ -293,7 +293,13 @@
{% if item.original_price %}
</ins>
{% endif %}
{% if var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
@@ -397,7 +403,13 @@
{% if item.original_price %}
</ins>
{% endif %}
{% if item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>

View File

@@ -112,7 +112,13 @@
{% else %}
{{ var.display_price.gross|money:event.currency }}
{% endif %}
{% if var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
@@ -201,7 +207,13 @@
{% else %}
{{ item.display_price.gross|money:event.currency }}
{% endif %}
{% if item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>

View File

@@ -19,6 +19,7 @@ from django.views.generic import TemplateView
from pretix.base.models import ItemVariation, Quota
from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemBundle
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.ical import get_ical
from pretix.presale.views.organizer import (
@@ -54,6 +55,21 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web'):
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.filter(subevent=subevent)),
Prefetch('bundles',
queryset=ItemBundle.objects.prefetch_related(
Prefetch('bundled_item',
queryset=event.items.select_related('tax_rule').prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.filter(subevent=subevent)),
)),
Prefetch('bundled_variation',
queryset=ItemVariation.objects.select_related('item', 'item__tax_rule').filter(item__event=event).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.filter(subevent=subevent)),
)),
)),
Prefetch('variations', to_attr='available_variations',
queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).prefetch_related(
Prefetch('quotas',
@@ -94,7 +110,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web'):
)
else:
item.cached_availability = list(
item.check_quotas(subevent=subevent, _cache=quota_cache)
item.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
)
item.order_max = min(
@@ -106,7 +122,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web'):
price = item_price_override.get(item.pk, item.default_price)
if voucher:
price = voucher.calculate_price(price)
item.display_price = item.tax(price)
item.display_price = item.tax(price, currency=event.currency, include_bundled=True)
display_add_to_cart = display_add_to_cart or item.order_max > 0
else:
@@ -117,7 +133,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web'):
)
else:
var.cached_availability = list(
var.check_quotas(subevent=subevent, _cache=quota_cache)
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
)
var.order_max = min(
@@ -129,7 +145,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web'):
price = var_price_override.get(var.pk, var.price)
if voucher:
price = voucher.calculate_price(price)
var.display_price = var.tax(price)
var.display_price = var.tax(price, currency=event.currency, include_bundled=True)
display_add_to_cart = display_add_to_cart or var.order_max > 0

View File

@@ -149,13 +149,14 @@ def widget_js(request, lang, **kwargs):
return resp
def price_dict(price):
def price_dict(item, price):
return {
'gross': price.gross,
'net': price.net,
'tax': price.tax,
'rate': price.rate,
'name': str(price.name)
'name': str(price.name),
'includes_mixed_tax_rate': item.includes_mixed_tax_rate,
}
@@ -185,7 +186,7 @@ class WidgetAPIProductList(EventListMixin, View):
'require_voucher': item.require_voucher,
'order_min': item.min_per_order,
'order_max': item.order_max if not item.has_variations else None,
'price': price_dict(item.display_price) if not item.has_variations else None,
'price': price_dict(item, item.display_price) if not item.has_variations else None,
'min_price': item.min_price if item.has_variations else None,
'max_price': item.max_price if item.has_variations else None,
'free_price': item.free_price,
@@ -200,7 +201,7 @@ class WidgetAPIProductList(EventListMixin, View):
'value': str(var.value),
'order_max': var.order_max,
'description': str(rich_text(var.description, safelinks=False)) if var.description else None,
'price': price_dict(var.display_price),
'price': price_dict(item, var.display_price),
'avail': [
var.cached_availability[0],
var.cached_availability[1] if self.request.event.settings.show_quota_left else None