diff --git a/src/pretix/base/migrations/0135_auto_20190910_2020.py b/src/pretix/base/migrations/0135_auto_20190910_2020.py index 33d55371bc..8db9543e2a 100644 --- a/src/pretix/base/migrations/0135_auto_20190910_2020.py +++ b/src/pretix/base/migrations/0135_auto_20190910_2020.py @@ -1,7 +1,8 @@ # Generated by Django 2.2.1 on 2019-09-10 20:20 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models + import pretix.base.models.fields import pretix.base.models.giftcards diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index fd7ba9dba9..a40e1a9eac 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -888,6 +888,21 @@ class OffsettingProvider(BasePaymentProvider): return _('Balanced against orders: %s' % ', '.join(payment.info_data['orders'])) +class GiftCardPayment(BasePaymentProvider): + is_enabled = True + identifier = "giftcard" + verbose_name = _("Gift card") + is_implicit = True + + def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool: + return False + + def order_change_allowed(self, order: Order) -> bool: + return False + + # TODO: execute, refund, api, control render + + @receiver(register_payment_providers, dispatch_uid="payment_free") def register_payment_provider(sender, **kwargs): - return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment] + return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment] diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 767f895df0..8ccc0c0bde 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -14,8 +14,8 @@ from django_scopes import scopes_disabled from pretix.base.i18n import language from pretix.base.models import ( - CartPosition, Event, InvoiceAddress, Item, ItemBundle, ItemVariation, Seat, - SeatCategoryMapping, Voucher, + CartPosition, Event, GiftCard, InvoiceAddress, Item, ItemBundle, + ItemVariation, Seat, SeatCategoryMapping, Voucher, ) from pretix.base.models.event import SubEvent from pretix.base.models.orders import OrderFee @@ -958,14 +958,36 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress def get_fees(event, request, total, invoice_address, provider): - fees = [] + from pretix.presale.views.cart import cart_session + fees = [] for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address, total=total): if resp: fees += resp total = total + sum(f.value for f in fees) + + cs = cart_session(request) + if cs.get('gift_cards'): + gc_qs = GiftCard.objects.filter(pk__in=cs.get('gift_cards')) + summed = 0 + for gc in gc_qs: + fval = Decimal(gc.value) # TODO: don't require an extra query + fval = min(fval, total - summed) + if fval > 0: + total -= fval + summed += fval + fees.append(OrderFee( + fee_type=OrderFee.FEE_TYPE_GIFTCARD, + internal_type='giftcard', + description=gc.secret, + value=-1 * fval, + tax_rate=Decimal('0.00'), + tax_value=Decimal('0.00'), + tax_rule=TaxRule.zero() + )) + if provider and total != 0: provider = event.get_payment_providers().get(provider) if provider: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index b711f47060..5d91950dfe 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -20,8 +20,9 @@ from pretix.api.models import OAuthApplication from pretix.base.email import get_email_context from pretix.base.i18n import LazyLocaleException, language from pretix.base.models import ( - CartPosition, Device, Event, Item, ItemVariation, Order, OrderPayment, - OrderPosition, Quota, Seat, SeatCategoryMapping, User, Voucher, + CartPosition, Device, Event, GiftCard, Item, ItemVariation, Order, + OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User, + Voucher, ) from pretix.base.models.event import SubEvent from pretix.base.models.items import ItemBundle @@ -545,7 +546,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvider, address: InvoiceAddress, - meta_info: dict, event: Event): + meta_info: dict, event: Event, gift_cards: List[GiftCard]): fees = [] total = sum([c.price for c in positions]) @@ -553,8 +554,18 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid meta_info=meta_info, positions=positions): if resp: fees += resp - total += sum(f.value for f in fees) + + summed = 0 + gift_card_values = {} + for gc in gift_cards: + fval = Decimal(gc.value) # TODO: don't require an extra query + fval = min(fval, total - summed) + if fval > 0: + total -= fval + summed += fval + gift_card_values[gc] = fval + if payment_provider: payment_fee = payment_provider.calculate_fee(total) else: @@ -565,17 +576,24 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid internal_type=payment_provider.identifier) fees.append(pf) - return fees, pf + return fees, pf, gift_card_values def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime, payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None, - 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]) + meta_info: dict=None, sales_channel: str='web', gift_cards: list=None): p = None - with transaction.atomic(): + checked_gift_cards = [] + if gift_cards: + gc_qs = GiftCard.objects.select_for_update().filter(pk__in=gift_cards) # TODO: Make sure to prevent race conditions + for gc in gc_qs: + # TODO: Re-check acceptance + checked_gift_cards.append(gc) + + fees, pf, gift_card_values = _get_fees(positions, payment_provider, address, meta_info, event, checked_gift_cards) + total = pending_sum = sum([c.price for c in positions]) + sum([c.value for c in fees]) + order = Order( status=Order.STATUS_PENDING, event=event, @@ -606,11 +624,30 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d fee.tax_rule = None # TODO: deprecate fee.save() + for gc, val in gift_card_values.items(): + p = order.payments.create( + state=OrderPayment.PAYMENT_STATE_CONFIRMED, + provider='giftcard', + amount=val, + fee=pf + ) + trans = gc.transactions.create( + value=-1 * val, + order=order, + payment=p + ) + p.info_data = { + 'gift_card': gc.pk, + 'transaction_id': trans.pk, + } + p.save() + pending_sum -= val + if payment_provider and not order.require_approval: p = order.payments.create( state=OrderPayment.PAYMENT_STATE_CREATED, provider=payment_provider.identifier, - amount=total, + amount=pending_sum, fee=pf ) @@ -658,7 +695,8 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi def _perform_order(event: Event, payment_provider: str, position_ids: List[str], - email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web'): + email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web', + gift_cards: list=None): if payment_provider: pprov = event.get_payment_providers().get(payment_provider) if not pprov: @@ -707,9 +745,10 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str], raise OrderError(error_messages['internal']) _check_positions(event, now_dt, positions, address=addr) order, payment = _create_order(event, email, positions, now_dt, pprov, - locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel) + locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel, + gift_cards=gift_cards) - free_order_flow = payment and payment_provider == 'free' and order.total == Decimal('0.00') and not order.require_approval + free_order_flow = payment and payment_provider == 'free' and order.pending_sum == Decimal('0.00') and not order.require_approval if free_order_flow: try: payment.confirm(send_mail=False, lock=not locked) @@ -1466,12 +1505,12 @@ class OrderChangeManager: @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) def perform_order(self, event: Event, payment_provider: str, positions: List[str], email: str=None, locale: str=None, address: int=None, meta_info: dict=None, - sales_channel: str='web'): + sales_channel: str='web', gift_cards: list=None): with language(locale): try: try: return _perform_order(event, payment_provider, positions, email, locale, address, meta_info, - sales_channel) + sales_channel, gift_cards) except LockTimeoutException: self.retry() except (MaxRetriesExceededError, LockTimeoutException): diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 59ae609d89..1d94225461 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -15,7 +15,7 @@ from django.utils.translation import ( from django.views.generic.base import TemplateResponseMixin from django_scopes import scopes_disabled -from pretix.base.models import Order +from pretix.base.models import GiftCard, Order from pretix.base.models.orders import InvoiceAddress, OrderPayment from pretix.base.services.cart import ( get_fees, set_cart_addons, update_tax_rates, @@ -530,6 +530,24 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): def post(self, request): self.request = request + if request.POST.get("giftcard"): + # TODO: cross-organizer acceptance, check for valid money, … + try: + gc = GiftCard.objects.get( + issuer=request.organizer, + secret=request.POST.get("giftcard") + ) + if gc.currency != request.event.currency: + messages.error(self.request, _("This gift card does not support this currency.")) + return self.render() + if 'gift_cards' not in self.cart_session: + self.cart_session['gift_cards'] = [] + self.cart_session['gift_cards'] = self.cart_session['gift_cards'] + [gc.pk] + return self.render() + except GiftCard.DoesNotExist: + messages.error(self.request, _("This gift card is not known.")) + return self.render() + for p in self.provider_forms: if p['provider'].identifier == request.POST.get('payment', ''): self.cart_session['payment'] = p['provider'].identifier @@ -709,7 +727,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): return self.do(self.request.event.id, self.payment_provider.identifier if self.payment_provider else None, [p.id for p in self.positions], self.cart_session.get('email'), translation.get_language(), self.invoice_address.pk, meta_info, - request.sales_channel) + request.sales_channel, self.cart_session.get('gift_cards')) def get_success_message(self, value): create_empty_cart_id(self.request) diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html index 4df8de073f..58dcb81c6a 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html @@ -11,6 +11,8 @@