diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 3fc81c0408..013a8f25c6 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1159,7 +1159,7 @@ class OrderPayment(models.Model): self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth) order_paid.send(self.order.event, order=self.order) - def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text=''): + def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='', lock=True): """ Marks the payment as complete. If possible, this also marks the order as paid if no further payment is required @@ -1218,7 +1218,7 @@ class OrderPayment(models.Model): if payment_sum - refund_sum < self.order.total: return - if self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12): + if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock: # Performance optimization. In this case, there's really no reason to lock everything and an atomic # database transaction is more than enough. with transaction.atomic(): diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 722683f193..866ab7bda6 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -567,6 +567,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d meta_info: dict=None, sales_channel: str='web'): fees, pf = _get_fees(positions, payment_provider, address, meta_info, event) total = sum([c.price for c in positions]) + sum([c.value for c in fees]) + p = None with transaction.atomic(): order = Order( @@ -600,7 +601,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d fee.save() if payment_provider and not order.require_approval: - order.payments.create( + p = order.payments.create( state=OrderPayment.PAYMENT_STATE_CREATED, provider=payment_provider.identifier, amount=total, @@ -616,7 +617,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d order.log_action('pretix.event.order.consent', data={'msg': msg}) order_placed.send(event, order=order) - return order + return order, p def _perform_order(event: str, payment_provider: str, position_ids: List[str], @@ -648,8 +649,12 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str], if len(position_ids) != len(positions): raise OrderError(error_messages['internal']) _check_positions(event, now_dt, positions, address=addr) - order = _create_order(event, email, positions, now_dt, pprov, - locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel) + order, payment = _create_order(event, email, positions, now_dt, pprov, + locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel) + + free_order_flow = payment and payment_provider == 'free' and order.total == Decimal('0.00') + if free_order_flow: + payment.confirm(send_mail=False, lock=False) invoice = order.invoices.last() # Might be generated by plugin already if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order): @@ -664,7 +669,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str], if order.require_approval: email_template = event.settings.mail_text_order_placed_require_approval log_entry = 'pretix.event.order.email.order_placed_require_approval' - elif payment_provider == 'free': + elif free_order_flow: email_template = event.settings.mail_text_order_free log_entry = 'pretix.event.order.email.order_free' else: diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index caa1c8eca0..c6171b7217 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -14,7 +14,7 @@ from django.utils.translation import ( from django.views.generic.base import TemplateResponseMixin from pretix.base.models import Order -from pretix.base.models.orders import InvoiceAddress +from pretix.base.models.orders import InvoiceAddress, OrderPayment from pretix.base.services.cart import ( get_fees, set_cart_addons, update_tax_rates, ) @@ -721,7 +721,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): return self.get_step_url(self.request) def get_order_url(self, order): - payment = order.payments.first() + payment = order.payments.filter(state__in=[OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING]).first() if not payment: return eventreverse(self.request.event, 'presale:event.order', kwargs={ 'order': order.code, diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py index 77b8548168..59dba7fb18 100644 --- a/src/tests/base/test_orders.py +++ b/src/tests/base/test_orders.py @@ -41,7 +41,7 @@ def test_expiry_days(event): event.settings.set('payment_term_weekdays', False) order = _create_order(event, email='dummy@example.org', positions=[], now_dt=today, payment_provider=FreeOrderProvider(event), - locale='de') + locale='de')[0] assert (order.expires - today).days == 5 @@ -52,14 +52,14 @@ def test_expiry_weekdays(event): event.settings.set('payment_term_weekdays', True) order = _create_order(event, email='dummy@example.org', positions=[], now_dt=today, payment_provider=FreeOrderProvider(event), - locale='de') + locale='de')[0] assert (order.expires - today).days == 6 assert order.expires.weekday() == 0 today = make_aware(datetime(2016, 9, 19, 15, 0, 0, 0)) order = _create_order(event, email='dummy@example.org', positions=[], now_dt=today, payment_provider=FreeOrderProvider(event), - locale='de') + locale='de')[0] assert (order.expires - today).days == 7 assert order.expires.weekday() == 0 @@ -72,12 +72,12 @@ def test_expiry_last(event): event.settings.set('payment_term_last', now() + timedelta(days=3)) order = _create_order(event, email='dummy@example.org', positions=[], now_dt=today, payment_provider=FreeOrderProvider(event), - locale='de') + locale='de')[0] assert (order.expires - today).days == 3 event.settings.set('payment_term_last', now() + timedelta(days=7)) order = _create_order(event, email='dummy@example.org', positions=[], now_dt=today, payment_provider=FreeOrderProvider(event), - locale='de') + locale='de')[0] assert (order.expires - today).days == 5 @@ -93,7 +93,7 @@ def test_expiry_last_relative(event): )) order = _create_order(event, email='dummy@example.org', positions=[], now_dt=today, payment_provider=FreeOrderProvider(event), - locale='de') + locale='de')[0] assert (order.expires - today).days == 3 @@ -124,7 +124,7 @@ def test_expiry_last_relative_subevents(event): )) order = _create_order(event, email='dummy@example.org', positions=[cp1, cp2], now_dt=today, payment_provider=FreeOrderProvider(event), - locale='de') + locale='de')[0] assert (order.expires - today).days == 6 @@ -136,7 +136,7 @@ def test_expiry_dst(event): today = tz.localize(datetime(2016, 10, 29, 12, 0, 0)).astimezone(utc) order = _create_order(event, email='dummy@example.org', positions=[], now_dt=today, payment_provider=FreeOrderProvider(event), - locale='de') + locale='de')[0] localex = order.expires.astimezone(tz) assert (localex.hour, localex.minute) == (23, 59) diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index f6afcea89a..ce7107e733 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -907,6 +907,24 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): self.assertEqual(OrderPosition.objects.count(), 1) self.assertEqual(OrderPosition.objects.first().price, 42) + def test_free_order(self): + self.ticket.free_price = True + self.ticket.save() + cr1 = CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=0, expires=now() + timedelta(minutes=10) + ) + self._set_session('payment', 'free') + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertEqual(len(doc.select(".thank-you")), 1) + self.assertFalse(CartPosition.objects.filter(id=cr1.id).exists()) + self.assertEqual(Order.objects.count(), 1) + self.assertEqual(OrderPosition.objects.count(), 1) + self.assertEqual(OrderPosition.objects.first().price, 0) + self.assertEqual(Order.objects.first().status, Order.STATUS_PAID) + def test_confirm_in_time(self): cr1 = CartPosition.objects.create( event=self.event, cart_id=self.session_key, item=self.ticket, @@ -920,6 +938,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): self.assertFalse(CartPosition.objects.filter(id=cr1.id).exists()) self.assertEqual(Order.objects.count(), 1) self.assertEqual(OrderPosition.objects.count(), 1) + self.assertEqual(Order.objects.first().status, Order.STATUS_PENDING) def test_subevent_confirm_expired_available(self): self.event.has_subevents = True