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

@@ -8,8 +8,8 @@ from django_countries.fields import Country
from pytz import UTC
from pretix.base.models import (
CartPosition, InvoiceAddress, Item, ItemAddOn, ItemCategory, ItemVariation,
Order, OrderPosition, Question, QuestionOption, Quota,
CartPosition, InvoiceAddress, Item, ItemAddOn, ItemBundle, ItemCategory,
ItemVariation, Order, OrderPosition, Question, QuestionOption, Quota,
)
from pretix.base.models.orders import OrderFee
@@ -225,6 +225,7 @@ TEST_ITEM_RES = {
"picture": None,
"available_from": None,
"available_until": None,
"require_bundling": False,
"require_voucher": False,
"hide_without_voucher": False,
"allow_cancel": True,
@@ -235,6 +236,7 @@ TEST_ITEM_RES = {
"require_approval": False,
"variations": [],
"addons": [],
"bundles": [],
"original_price": None
}
@@ -344,6 +346,25 @@ def test_item_detail_addons(token_client, organizer, event, team, item, category
assert res == resp.data
@pytest.mark.django_db
def test_item_detail_bundles(token_client, organizer, event, team, item, category):
i = event.items.create(name="Included thing", default_price=2)
item.bundles.create(bundled_item=i, count=1, designated_price=2)
res = dict(TEST_ITEM_RES)
res["id"] = item.pk
res["bundles"] = [{
"bundled_item": i.pk,
"bundled_variation": None,
"count": 1,
"designated_price": '2.00',
}]
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.mark.django_db
def test_item_create(token_client, organizer, event, item, category, taxrule):
resp = token_client.post(
@@ -601,7 +622,134 @@ def test_item_create_with_addon(token_client, organizer, event, item, category,
@pytest.mark.django_db
def test_item_update(token_client, organizer, event, item, category, category2, taxrule2):
def test_item_create_with_bundle(token_client, organizer, event, item, category, item2, taxrule):
i = event.items.create(name="Included thing", default_price=2)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug),
{
"category": category.pk,
"name": {
"en": "Ticket"
},
"active": True,
"description": None,
"default_price": "23.00",
"free_price": False,
"tax_rate": "19.00",
"tax_rule": taxrule.pk,
"admission": True,
"position": 0,
"picture": None,
"available_from": None,
"available_until": None,
"require_voucher": False,
"hide_without_voucher": False,
"allow_cancel": True,
"min_per_order": None,
"max_per_order": None,
"checkin_attention": False,
"has_variations": True,
"bundles": [
{
"bundled_item": i.pk,
"bundled_variation": None,
"count": 2,
"designated_price": "3.00",
}
]
},
format='json'
)
assert resp.status_code == 201
item = Item.objects.get(pk=resp.data['id'])
b = item.bundles.first()
assert b.bundled_item == i
assert b.bundled_variation is None
assert b.count == 2
assert b.designated_price == 3
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug),
{
"category": category.pk,
"name": {
"en": "Ticket"
},
"active": True,
"description": None,
"default_price": "23.00",
"free_price": False,
"tax_rate": "19.00",
"tax_rule": taxrule.pk,
"admission": True,
"position": 0,
"picture": None,
"available_from": None,
"available_until": None,
"require_voucher": False,
"hide_without_voucher": False,
"allow_cancel": True,
"min_per_order": None,
"max_per_order": None,
"checkin_attention": False,
"has_variations": True,
"bundles": [
{
"bundled_item": item2.pk,
"bundled_variation": None,
"count": 2,
"designated_price": "3.00",
}
]
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"bundles":["The bundled item must belong to the same event as the item."]}'
v = item2.variations.create(value="foo")
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug),
{
"category": category.pk,
"name": {
"en": "Ticket"
},
"active": True,
"description": None,
"default_price": "23.00",
"free_price": False,
"tax_rate": "19.00",
"tax_rule": taxrule.pk,
"admission": True,
"position": 0,
"picture": None,
"available_from": None,
"available_until": None,
"require_voucher": False,
"hide_without_voucher": False,
"allow_cancel": True,
"min_per_order": None,
"max_per_order": None,
"checkin_attention": False,
"has_variations": True,
"bundles": [
{
"bundled_item": item.pk,
"bundled_variation": v.pk,
"count": 2,
"designated_price": "3.00",
}
]
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"bundles":["The chosen variation does not belong to this item."]}'
@pytest.mark.django_db
def test_item_update(token_client, organizer, event, item, category, item2, category2, taxrule2):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug, item.pk),
{
@@ -673,7 +821,25 @@ def test_item_update(token_client, organizer, event, item, category, category2,
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons or variations via PATCH/PUT is not supported. Please use ' \
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug, item.pk),
{
"bundles": [
{
"bundled_item": item2.pk,
"bundled_variation": None,
"count": 2,
"designated_price": "3.00",
}
]
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
@@ -699,7 +865,7 @@ def test_item_update_with_variation(token_client, organizer, event, item):
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons or variations via PATCH/PUT is not supported. Please use ' \
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
@@ -721,7 +887,7 @@ def test_item_update_with_addon(token_client, organizer, event, item, category):
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons or variations via PATCH/PUT is not supported. Please use ' \
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
@@ -922,6 +1088,123 @@ def test_only_variation_not_delete(token_client, organizer, event, item, variati
assert item.variations.filter(pk=variation.id).exists()
@pytest.fixture
def bundle(item, item3, category):
return item.bundles.create(bundled_item=item3, count=1, designated_price=2)
TEST_BUNDLE_RES = {
"bundled_item": 0,
"bundled_variation": None,
"count": 1,
"designated_price": "2.00"
}
@pytest.mark.django_db
def test_bundles_list(token_client, organizer, event, item, bundle, item3):
res = dict(TEST_BUNDLE_RES)
res["id"] = bundle.pk
res["bundled_item"] = item3.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/bundles/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res == resp.data['results'][0]
@pytest.mark.django_db
def test_bundles_detail(token_client, organizer, event, item, bundle, item3):
res = dict(TEST_BUNDLE_RES)
res["id"] = bundle.pk
res["bundled_item"] = item3.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/bundles/{}/'.format(organizer.slug, event.slug,
item.pk, bundle.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.mark.django_db
def test_bundles_create(token_client, organizer, event, item, item2, item3):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/bundles/'.format(organizer.slug, event.slug, item.pk),
{
"bundled_item": item3.pk,
"bundled_variation": None,
"count": 1,
"designated_price": "1.50",
},
format='json'
)
assert resp.status_code == 201
b = ItemBundle.objects.get(pk=resp.data['id'])
assert b.bundled_item == item3
assert b.bundled_variation is None
assert b.designated_price == 1.5
assert b.count == 1
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/bundles/'.format(organizer.slug, event.slug, item.pk),
{
"bundled_item": item2.pk,
"bundled_variation": None,
"count": 1,
"designated_price": "1.50",
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["The bundled item must belong to the same event as the item."]}'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/bundles/'.format(organizer.slug, event.slug, item.pk),
{
"bundled_item": item.pk,
"bundled_variation": None,
"count": 1,
"designated_price": "1.50",
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["The bundled item must not be the same item as the bundling one."]}'
item3.bundles.create(bundled_item=item, count=1, designated_price=3)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/bundles/'.format(organizer.slug, event.slug, item.pk),
{
"bundled_item": item3.pk,
"bundled_variation": None,
"count": 1,
"designated_price": "1.50",
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["The bundled item must not have bundles on its own."]}'
@pytest.mark.django_db
def test_bundles_update(token_client, organizer, event, item, bundle):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/bundles/{}/'.format(organizer.slug, event.slug, item.pk, bundle.pk),
{
"count": 3,
},
format='json'
)
assert resp.status_code == 200
a = ItemBundle.objects.get(pk=bundle.pk)
assert a.count == 3
@pytest.mark.django_db
def test_bundles_delete(token_client, organizer, event, item, bundle):
resp = token_client.delete('/api/v1/organizers/{}/events/{}/items/{}/bundles/{}/'.format(organizer.slug, event.slug,
item.pk, bundle.pk))
assert resp.status_code == 204
assert not item.bundles.filter(pk=bundle.id).exists()
@pytest.fixture
def addon(item, category):
return item.addons.create(addon_category=category, min_count=0, max_count=10, position=1)