Vouchers: Allow to set all addons or bundles as included (#3322)

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2023-05-22 11:59:27 +02:00
committed by GitHub
co-authored by Richard Schreiber
parent 5eff9a86f4
commit c75c080c5c
19 changed files with 241 additions and 23 deletions
+29 -6
View File
@@ -47,6 +47,8 @@ tag string A string that i
comment string An internal comment on the voucher comment string An internal comment on the voucher
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``). subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``. show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
@@ -95,6 +97,9 @@ Endpoints
"comment": "", "comment": "",
"seat": null, "seat": null,
"subevent": null, "subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
} }
] ]
} }
@@ -161,7 +166,10 @@ Endpoints
"tag": "testvoucher", "tag": "testvoucher",
"comment": "", "comment": "",
"seat": null, "seat": null,
"subevent": null "subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
} }
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
@@ -198,7 +206,10 @@ Endpoints
"quota": null, "quota": null,
"tag": "testvoucher", "tag": "testvoucher",
"comment": "", "comment": "",
"subevent": null "subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
} }
**Example response**: **Example response**:
@@ -225,7 +236,10 @@ Endpoints
"tag": "testvoucher", "tag": "testvoucher",
"comment": "", "comment": "",
"seat": null, "seat": null,
"subevent": null "subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
} }
:param organizer: The ``slug`` field of the organizer to create a voucher for :param organizer: The ``slug`` field of the organizer to create a voucher for
@@ -264,7 +278,10 @@ Endpoints
"quota": null, "quota": null,
"tag": "testvoucher", "tag": "testvoucher",
"comment": "", "comment": "",
"subevent": null "subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}, },
{ {
"code": "ASDKLJCYXCASDASD", "code": "ASDKLJCYXCASDASD",
@@ -279,7 +296,10 @@ Endpoints
"quota": null, "quota": null,
"tag": "testvoucher", "tag": "testvoucher",
"comment": "", "comment": "",
"subevent": null "subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}, },
**Example response**: **Example response**:
@@ -353,7 +373,10 @@ Endpoints
"tag": "testvoucher", "tag": "testvoucher",
"comment": "", "comment": "",
"seat": null, "seat": null,
"subevent": null "subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
} }
:param organizer: The ``slug`` field of the organizer to modify :param organizer: The ``slug`` field of the organizer to modify
+2 -1
View File
@@ -63,7 +63,8 @@ class VoucherSerializer(I18nAwareModelSerializer):
model = Voucher model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota', fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota', 'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat') 'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
'all_bundles_included')
read_only_fields = ('id', 'redeemed') read_only_fields = ('id', 'redeemed')
list_serializer_class = VoucherListSerializer list_serializer_class = VoucherListSerializer
@@ -0,0 +1,23 @@
# Generated by Django 3.2.18 on 2023-05-16 11:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0239_giftcard_info'),
]
operations = [
migrations.AddField(
model_name='voucher',
name='all_addons_included',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='voucher',
name='all_bundles_included',
field=models.BooleanField(default=False),
),
]
+6 -2
View File
@@ -2832,8 +2832,12 @@ class CartPosition(AbstractPosition):
if self.is_bundled: if self.is_bundled:
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first() bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
if bundle: if bundle:
listed_price = bundle.designated_price if self.addon_to.voucher_id and self.addon_to.voucher.all_bundles_included:
price_after_voucher = bundle.designated_price listed_price = Decimal('0.00')
price_after_voucher = Decimal('0.00')
else:
listed_price = bundle.designated_price
price_after_voucher = bundle.designated_price
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher: if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
self.listed_price = listed_price self.listed_price = listed_price
+8
View File
@@ -296,6 +296,14 @@ class Voucher(LoggedModel):
verbose_name=_("Shows hidden products that match this voucher"), verbose_name=_("Shows hidden products that match this voucher"),
default=True default=True
) )
all_addons_included = models.BooleanField(
verbose_name=_("Offer all add-on products for free when redeeming this voucher"),
default=False
)
all_bundles_included = models.BooleanField(
verbose_name=_("Include all bundled products without a designated price when redeeming this voucher"),
default=False
)
objects = ScopedManager(organizer='event__organizer') objects = ScopedManager(organizer='event__organizer')
+11 -5
View File
@@ -512,7 +512,10 @@ class CartManager:
if cp.is_bundled: if cp.is_bundled:
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first() bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
if bundle: if bundle:
listed_price = bundle.designated_price or Decimal('0.00') if cp.addon_to.voucher_id and cp.addon_to.voucher.all_bundles_included:
listed_price = Decimal('0.00')
else:
listed_price = bundle.designated_price
else: else:
listed_price = cp.price listed_price = cp.price
price_after_voucher = listed_price price_after_voucher = listed_price
@@ -712,6 +715,11 @@ class CartManager:
else: else:
bundle_quotas = [] bundle_quotas = []
if voucher and voucher.all_bundles_included:
bundled_price = Decimal('0.00')
else:
bundled_price = bundle.designated_price
bop = self.AddOperation( bop = self.AddOperation(
count=bundle.count, count=bundle.count,
item=bitem, item=bitem,
@@ -722,8 +730,8 @@ class CartManager:
subevent=subevent, subevent=subevent,
bundled=[], bundled=[],
seat=None, seat=None,
listed_price=bundle.designated_price, listed_price=bundled_price,
price_after_voucher=bundle.designated_price, price_after_voucher=bundled_price,
custom_price_input=None, custom_price_input=None,
custom_price_input_is_net=False, custom_price_input_is_net=False,
voucher_ignored=False, voucher_ignored=False,
@@ -809,7 +817,6 @@ 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(
@@ -819,7 +826,6 @@ 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
for a in cp.addons.all(): for a in cp.addons.all():
if not a.is_bundled: if not a.is_bundled:
+1 -1
View File
@@ -1892,7 +1892,7 @@ class OrderChangeManager:
input_addons[op.id][a['item'], a['variation']] = a.get('count', 1) input_addons[op.id][a['item'], a['variation']] = a.get('count', 1)
selected_addons[op.id, item.category_id][a['item'], a['variation']] = a.get('count', 1) selected_addons[op.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
if price_included[op.pk].get(item.category_id): if price_included[op.pk].get(item.category_id) or (op.voucher_id and op.voucher.all_addons_included):
price = TAXED_ZERO price = TAXED_ZERO
else: else:
price = get_price( price = get_price(
+2
View File
@@ -110,6 +110,8 @@ def is_included_for_free(item: Item, addon_to: AbstractPosition):
return True return True
except ItemAddOn.DoesNotExist: except ItemAddOn.DoesNotExist:
pass pass
if addon_to.voucher_id and addon_to.voucher.all_addons_included:
return True
return False return False
+4 -2
View File
@@ -72,7 +72,8 @@ class VoucherForm(I18nModelForm):
localized_fields = '__all__' localized_fields = '__all__'
fields = [ fields = [
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget' 'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
'all_bundles_included', 'budget'
] ]
field_classes = { field_classes = {
'valid_until': SplitDateTimeField, 'valid_until': SplitDateTimeField,
@@ -308,7 +309,8 @@ class VoucherBulkForm(VoucherForm):
localized_fields = '__all__' localized_fields = '__all__'
fields = [ fields = [
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget' 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
'all_bundles_included', 'budget'
] ]
field_classes = { field_classes = {
'valid_until': SplitDateTimeField, 'valid_until': SplitDateTimeField,
@@ -78,6 +78,8 @@
{% bootstrap_field form.tag layout="control" %} {% bootstrap_field form.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %} {% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.show_hidden_items layout="control" %} {% bootstrap_field form.show_hidden_items layout="control" %}
{% bootstrap_field form.all_addons_included layout="control" %}
{% bootstrap_field form.all_bundles_included layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Send out emails" %}</legend> <legend>{% trans "Send out emails" %}</legend>
@@ -90,6 +90,8 @@
{% bootstrap_field form.tag layout="control" %} {% bootstrap_field form.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %} {% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.show_hidden_items layout="control" %} {% bootstrap_field form.show_hidden_items layout="control" %}
{% bootstrap_field form.all_addons_included layout="control" %}
{% bootstrap_field form.all_bundles_included layout="control" %}
</fieldset> </fieldset>
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %} {% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
</div> </div>
+1 -1
View File
@@ -585,7 +585,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
if items: if items:
formsetentry['categories'].append({ formsetentry['categories'].append({
'category': iao.addon_category, 'category': iao.addon_category,
'price_included': iao.price_included, 'price_included': iao.price_included or (cartpos.voucher_id and cartpos.voucher.all_addons_included),
'multi_allowed': iao.multi_allowed, 'multi_allowed': iao.multi_allowed,
'min_count': iao.min_count, 'min_count': iao.min_count,
'max_count': iao.max_count, 'max_count': iao.max_count,
@@ -149,7 +149,7 @@
from {{ minprice }} from {{ minprice }}
{% endblocktrans %} {% endblocktrans %}
{% elif not item.min_price and not item.max_price %} {% elif not item.min_price and not item.max_price %}
{% if not item.mandatory_priced_addons %} {% if not item.mandatory_priced_addons or voucher.all_addons_included %}
<span class="text-uppercase">{% trans "free" context "price" %}</span> <span class="text-uppercase">{% trans "free" context "price" %}</span>
{% endif %} {% endif %}
{% else %} {% else %}
@@ -207,7 +207,7 @@
</div> </div>
<p> <p>
{% elif not var.display_price.gross %} {% elif not var.display_price.gross %}
{% if not item.mandatory_priced_addons or var.original_price %} {% if not item.mandatory_priced_addons or var.original_price or voucher.all_addons_included %}
<span class="text-uppercase">{% trans "free" context "price" %}</span> <span class="text-uppercase">{% trans "free" context "price" %}</span>
{% endif %} {% endif %}
{% elif event.settings.display_net_prices %} {% elif event.settings.display_net_prices %}
@@ -349,7 +349,7 @@
</div> </div>
<p> <p>
{% elif not item.display_price.gross %} {% elif not item.display_price.gross %}
{% if not item.mandatory_priced_addons or item.original_price %} {% if not item.mandatory_priced_addons or item.original_price or voucher.all_addons_included %}
<span class="text-uppercase">{% trans "free" context "price" %}</span> <span class="text-uppercase">{% trans "free" context "price" %}</span>
{% endif %} {% endif %}
{% elif event.settings.display_net_prices %} {% elif event.settings.display_net_prices %}
+1 -1
View File
@@ -1361,7 +1361,7 @@ class OrderChangeMixin:
if items: if items:
p.addon_form['categories'].append({ p.addon_form['categories'].append({
'category': iao.addon_category, 'category': iao.addon_category,
'price_included': iao.price_included, 'price_included': iao.price_included or (p.voucher_id and p.voucher.all_addons_included),
'multi_allowed': iao.multi_allowed, 'multi_allowed': iao.multi_allowed,
'min_count': iao.min_count, 'min_count': iao.min_count,
'max_count': iao.max_count, 'max_count': iao.max_count,
+2
View File
@@ -79,6 +79,8 @@ TEST_VOUCHER_RES = {
'tag': 'Foo', 'tag': 'Foo',
'comment': '', 'comment': '',
'show_hidden_items': True, 'show_hidden_items': True,
'all_addons_included': False,
'all_bundles_included': False,
'subevent': None, 'subevent': None,
'seat': None, 'seat': None,
} }
+42 -1
View File
@@ -28,7 +28,7 @@ from django.utils.timezone import now
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.base.models import ( from pretix.base.models import (
CartPosition, Event, Item, OrderPosition, Organizer, Quota, CartPosition, Event, Item, OrderPosition, Organizer, Quota, Voucher,
) )
from pretix.base.services.orders import _perform_order from pretix.base.services.orders import _perform_order
from pretix.testutils.sessions import get_cart_session_key from pretix.testutils.sessions import get_cart_session_key
@@ -116,6 +116,47 @@ class BundlePricesTest(TestCase):
assert op2.item == self.food assert op2.item == self.food
assert op2.tax_rate == Decimal('7.00') assert op2.tax_rate == Decimal('7.00')
def test_voucher_includes_bundles(self):
with scopes_disabled():
v = Voucher.objects.create(item=self.ticket, value=Decimal('0.00'), event=self.event, price_mode='set',
all_bundles_included=True)
# Verify correct price displayed on event page
response = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertContains(response, '23.00')
# Verify correct price being added to cart
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'_voucher_code': v.code
}, follow=True)
with scopes_disabled():
cp1 = CartPosition.objects.get(is_bundled=False)
cp2 = CartPosition.objects.get(is_bundled=True)
assert cp1.price == Decimal('0.00')
assert cp1.item == self.ticket
assert cp2.price == Decimal('0.00')
assert cp2.item == self.food
# Make sure cart expires
cp1.expires = now() - datetime.timedelta(minutes=120)
cp1.save()
cp2.expires = now() - datetime.timedelta(minutes=120)
cp2.save()
# Verify price is kept if cart expires and order is sent
with scopes_disabled():
_perform_order(self.event, self._manual_payment(), [cp1.pk, cp2.pk], 'admin@example.org', 'en', None, {}, 'web')
op1 = OrderPosition.objects.get(is_bundled=False)
op2 = OrderPosition.objects.get(is_bundled=True)
assert op1.price == Decimal('0.00')
assert op1.item == self.ticket
assert op1.tax_rate == Decimal('19.00')
assert op2.price == Decimal('0.00')
assert op2.item == self.food
assert op2.tax_rate == Decimal('7.00')
def test_net_price_definitions(self): def test_net_price_definitions(self):
self.tr19.price_includes_tax = False self.tr19.price_includes_tax = False
self.tr19.save() self.tr19.save()
+21
View File
@@ -3119,6 +3119,27 @@ class CartBundleTest(CartTestMixin, TestCase):
assert a.item == self.trans assert a.item == self.trans
assert a.price == 1.5 assert a.price == 1.5
@classscope(attr='orga')
def test_simple_bundled_voucher_all_free(self):
v = Voucher.objects.create(item=self.ticket, value=Decimal('0.00'), event=self.event, price_mode='set',
all_bundles_included=True)
self.cm.add_new_items([
{
'item': self.ticket.pk,
'variation': None,
'voucher': v.code,
'count': 1
}
])
self.cm.commit()
cp = CartPosition.objects.get(addon_to__isnull=True)
assert cp.item == self.ticket
assert cp.price == 0
assert cp.addons.count() == 1
a = cp.addons.get()
assert a.item == self.trans
assert a.price == 0
@classscope(attr='orga') @classscope(attr='orga')
def test_voucher_on_base_product(self): def test_voucher_on_base_product(self):
v = self.event.vouchers.create(code="foo", item=self.ticket) v = self.event.vouchers.create(code="foo", item=self.ticket)
+44
View File
@@ -2263,6 +2263,29 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
with scopes_disabled(): with scopes_disabled():
self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).last().price, 0) self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).last().price, 0)
def test_addon_price_included_in_voucher(self):
with scopes_disabled():
v = Voucher.objects.create(item=self.ticket, value=Decimal('0.00'), event=self.event, price_mode='set',
valid_until=now() + timedelta(days=2), all_addons_included=True)
ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat, min_count=1,
price_included=False)
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=0, expires=now() - timedelta(minutes=10), voucher=v
)
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_payment()
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select(".thank-you")), 1)
with scopes_disabled():
self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).last().price, 0)
def test_confirm_price_changed_reverse_charge(self): def test_confirm_price_changed_reverse_charge(self):
self._enable_reverse_charge() self._enable_reverse_charge()
self.ticket.default_price = 24 self.ticket.default_price = 24
@@ -3970,6 +3993,27 @@ class CheckoutBundleTest(BaseCheckoutTestCase, TestCase):
assert a.item == self.trans assert a.item == self.trans
assert a.price == 1.5 assert a.price == 1.5
@classscope(attr='orga')
def test_expired_bundle_with_voucher_bundles_included(self):
v = Voucher.objects.create(item=self.ticket, value=Decimal('12.00'), event=self.event, price_mode='none',
valid_until=now() + timedelta(days=2), all_bundles_included=True)
self.cp1.voucher = v
self.cp1.price = 23
self.cp1.expires = now() - timedelta(minutes=10)
self.cp1.save()
self.bundled1.price = 0
self.bundled1.expires = now() - timedelta(minutes=10)
self.bundled1.save()
oid = _perform_order(self.event, self._manual_payment(), [self.cp1.pk, self.bundled1.pk], 'admin@example.org', 'en', None, {}, 'web')
o = Order.objects.get(pk=oid['order_id'])
cp = o.positions.get(addon_to__isnull=True)
assert cp.item == self.ticket
assert cp.price == 23
assert cp.addons.count() == 1
a = cp.addons.get()
assert a.item == self.trans
assert a.price == 0
@classscope(attr='orga') @classscope(attr='orga')
def test_expired_keep_price(self): def test_expired_keep_price(self):
self.cp1.expires = now() - timedelta(minutes=10) self.cp1.expires = now() - timedelta(minutes=10)
+37
View File
@@ -650,6 +650,43 @@ class OrderChangeAddonsTest(BaseOrdersTest):
self.order.refresh_from_db() self.order.refresh_from_db()
assert self.order.total == Decimal('35.00') assert self.order.total == Decimal('35.00')
def test_add_addon_included_in_voucher(self):
response = self.client.get(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
assert 'Workshop 1' in response.content.decode()
with scopes_disabled():
v = self.event.vouchers.create(item=self.ticket, all_addons_included=True)
self.ticket_pos.voucher = v
self.ticket_pos.save()
doc = BeautifulSoup(response.content.decode(), "lxml")
assert doc.select(f'input[name=cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}]')
response = self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
{
f'cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}': '1'
},
follow=True
)
doc = BeautifulSoup(response.content.decode(), "lxml")
form_data = extract_form_fields(doc.select('.main-box form')[0])
form_data['confirm'] = 'true'
response = self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), form_data, follow=True
)
assert 'alert-success' in response.content.decode()
with scopes_disabled():
new_pos = self.ticket_pos.addons.get()
assert new_pos.item == self.workshop1
assert new_pos.price == Decimal('0.00')
self.order.refresh_from_db()
assert self.order.total == Decimal('23.00')
def test_add_addon_free_price(self): def test_add_addon_free_price(self):
self.workshop1.free_price = True self.workshop1.free_price = True
self.workshop1.save() self.workshop1.save()