diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index 359bd2588b..ef42883231 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -38,7 +38,7 @@ Frontend .. automodule:: pretix.presale.signals - :members: order_info, order_info_top, order_meta_from_request + :members: order_info, order_info_top, order_meta_from_request, order_source_from_request Request flow """""""""""" diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 81af4a1c96..0dab27e09b 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -1378,7 +1378,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer): state=OrderPayment.PAYMENT_STATE_CREATED ) - order.create_transactions(is_new=True, fees=fees, positions=pos_map.values()) + order.create_transactions(is_new=True, fees=fees, positions=pos_map.values(), source=self.context['source']) return order diff --git a/src/pretix/api/utils.py b/src/pretix/api/utils.py new file mode 100644 index 0000000000..7b0281ed3d --- /dev/null +++ b/src/pretix/api/utils.py @@ -0,0 +1,14 @@ +from pretix.api.models import OAuthAccessToken +from pretix.base.models import Device, TeamAPIToken + + +def get_api_source(request): + if isinstance(request.auth, Device): + return "pretix.api", f"device:{request.auth.pk}" + elif isinstance(request.auth, TeamAPIToken): + return "pretix.api", f"token:{request.auth.pk}" + elif isinstance(request.auth, OAuthAccessToken): + return "pretix.api", f"oauth.app:{request.auth.application.pk}" + elif request.user.is_authenticated: + return "pretix.api", f"user:{request.user.pk}" + return "pretix.api", None diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 5135fb1e65..8fd7ec57c2 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -61,6 +61,7 @@ from pretix.api.serializers.orderchange import ( OrderPositionCreateForExistingOrderSerializer, OrderPositionInfoPatchSerializer, ) +from pretix.api.utils import get_api_source from pretix.base.i18n import language from pretix.base.models import ( CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue, @@ -190,6 +191,7 @@ class OrderViewSet(viewsets.ModelViewSet): ctx['event'] = self.request.event ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true' ctx['exclude'] = self.request.query_params.getlist('exclude') + ctx['source'] = get_api_source(self.request) return ctx def get_queryset(self): @@ -390,7 +392,8 @@ class OrderViewSet(viewsets.ModelViewSet): oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None, send_mail=send_mail, email_comment=comment, - cancellation_fee=cancellation_fee + cancellation_fee=cancellation_fee, + source=get_api_source(request), ) except OrderError as e: return Response( @@ -414,6 +417,7 @@ class OrderViewSet(viewsets.ModelViewSet): order, user=request.user if request.user.is_authenticated else None, auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None, + source=get_api_source(request), ) except OrderError as e: return Response( @@ -433,6 +437,7 @@ class OrderViewSet(viewsets.ModelViewSet): user=request.user if request.user.is_authenticated else None, auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None, send_mail=send_mail, + source=get_api_source(request), ) except Quota.QuotaExceededException as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -453,6 +458,7 @@ class OrderViewSet(viewsets.ModelViewSet): auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None, send_mail=send_mail, comment=comment, + source=get_api_source(request), ) except OrderError as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -491,6 +497,7 @@ class OrderViewSet(viewsets.ModelViewSet): order, user=request.user if request.user.is_authenticated else None, auth=request.auth, + source=get_api_source(request), ) return self.retrieve(request, [], **kwargs) @@ -508,6 +515,7 @@ class OrderViewSet(viewsets.ModelViewSet): order, user=request.user if request.user.is_authenticated else None, auth=(request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken, Device)) else None), + source=get_api_source(request), ) return self.retrieve(request, [], **kwargs) @@ -1484,7 +1492,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): if mark_refunded: mark_order_refunded(payment.order, user=self.request.user if self.request.user.is_authenticated else None, - auth=self.request.auth) + auth=self.request.auth, + source=get_api_source(self.request)) else: payment.order.status = Order.STATUS_PENDING payment.order.set_expires( @@ -1557,7 +1566,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): mark_refunded = request.data.get('mark_canceled', False) if mark_refunded: mark_order_refunded(refund.order, user=self.request.user if self.request.user.is_authenticated else None, - auth=self.request.auth) + auth=self.request.auth, source=get_api_source(self.request)) elif not (refund.order.status == Order.STATUS_PAID and refund.order.pending_sum <= 0): refund.order.status = Order.STATUS_PENDING refund.order.set_expires( @@ -1610,6 +1619,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): r.order, user=request.user if request.user.is_authenticated else None, auth=(request.auth if request.auth else None), + source=get_api_source(self.request), ) except OrderError as e: raise ValidationError(str(e)) diff --git a/src/pretix/base/migrations/0223_auto_20221019_0950.py b/src/pretix/base/migrations/0223_auto_20221019_0950.py new file mode 100644 index 0000000000..b3259896f5 --- /dev/null +++ b/src/pretix/base/migrations/0223_auto_20221019_0950.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.2 on 2022-10-19 09:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0222_alter_question_unique_together'), + ] + + operations = [ + migrations.AddField( + model_name='transaction', + name='source_identifier', + field=models.CharField(db_index=True, max_length=190, null=True), + ), + migrations.AddField( + model_name='transaction', + name='source_type', + field=models.CharField(db_index=True, max_length=190, null=True), + ), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 89198448cc..5a432fec89 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1041,10 +1041,13 @@ class Order(LockModel, LoggedModel): continue yield op - def create_transactions(self, is_new=False, positions=None, fees=None, dt_now=None, migrated=False, - _backfill_before_cancellation=False, save=True): + def create_transactions(self, *, source=None, is_new=False, positions=None, fees=None, + dt_now=None, migrated=False, _backfill_before_cancellation=False, save=True): dt_now = dt_now or now() + if source is not None and (not isinstance(source, tuple) or len(source) != 2 or not all(isinstance(a, str) or a is None for a in source)): + return ValueError("source needs to be a 2-tuple of (source_type(str), source_identifier(str))") + # Count the transactions we already have current_transaction_count = Counter() if not is_new: @@ -1089,6 +1092,8 @@ class Order(LockModel, LoggedModel): tax_value=taxvalue, fee_type=feetype, internal_type=internaltype, + source_type=source[0] if source else None, + source_identifier=source[1] if source else None, )) create.sort(key=lambda t: (0 if t.count < 0 else 1, t.positionid or 0)) if save: @@ -1573,7 +1578,7 @@ class OrderPayment(models.Model): return self.order.event.get_payment_providers(cached=True).get(self.provider) @transaction.atomic() - def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False): + def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False, source=None): from pretix.base.signals import order_paid can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force) if can_be_paid is not True: @@ -1596,7 +1601,9 @@ 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) if status_change: - self.order.create_transactions() + self.order.create_transactions( + source=source or ('pretix.payment', None), + ) def fail(self, info=None, user=None, auth=None, log_data=None): """ @@ -1630,7 +1637,7 @@ class OrderPayment(models.Model): }, user=user, auth=auth) def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='', - ignore_date=False, lock=True, payment_date=None): + ignore_date=False, lock=True, payment_date=None, source=None): """ Marks the payment as complete. If possible, this also marks the order as paid if no further payment is required @@ -1693,10 +1700,11 @@ class OrderPayment(models.Model): )) return - self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum) + self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum, + source) def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='', - ignore_date=False, lock=True, payment_refund_sum=0): + ignore_date=False, lock=True, payment_refund_sum=0, source=None): from pretix.base.services.invoices import ( generate_invoice, invoice_qualified, ) @@ -1710,7 +1718,7 @@ class OrderPayment(models.Model): with lockfn(): self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total, - ignore_date=ignore_date) + ignore_date=ignore_date, source=source) invoice = None if invoice_qualified(self.order): @@ -2483,6 +2491,8 @@ class Transaction(models.Model): :param id: ID of the transaction :param order: Order the transaction belongs to + :param source_type: Functionality that caused the transaction to be created, usually the name of a module or plugin + :param source_identifier: Identifier of the entity that caused the transaction to be created, as defined by the module or plugin noted in ``source_type``. :param datetime: Date and time of the transaction :param migrated: Whether this object was reconstructed because the order was created before transactions where introduced :param positionid: Affected Position ID, in case this transaction represents a change in an order position @@ -2505,6 +2515,12 @@ class Transaction(models.Model): related_name='transactions', on_delete=models.PROTECT ) + source_type = models.CharField( + max_length=190, db_index=True, null=True, blank=True + ) + source_identifier = models.CharField( + max_length=190, db_index=True, null=True, blank=True + ) created = models.DateTimeField( auto_now_add=True, db_index=True, diff --git a/src/pretix/base/services/cancelevent.py b/src/pretix/base/services/cancelevent.py index 58d2527d3a..ff94b9be50 100644 --- a/src/pretix/base/services/cancelevent.py +++ b/src/pretix/base/services/cancelevent.py @@ -210,7 +210,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, fee += min(p.price, Decimal(keep_fee_per_ticket)) fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) - _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects) + _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects, + source=("pretix.cancelevent", None)) refund_amount = o.payment_refund_sum try: diff --git a/src/pretix/base/services/orderimport.py b/src/pretix/base/services/orderimport.py index 65773c9241..3e8c140ace 100644 --- a/src/pretix/base/services/orderimport.py +++ b/src/pretix/base/services/orderimport.py @@ -195,7 +195,8 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user) user=user, data={'source': 'import'} ) - save_transactions += o.create_transactions(is_new=True, fees=[], positions=o._positions, save=False) + save_transactions += o.create_transactions(is_new=True, fees=[], positions=o._positions, save=False, + source=('pretix.orderimport', None)) Transaction.objects.bulk_create(save_transactions) for o in orders: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index be4a1418f5..40d235bebf 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -148,7 +148,7 @@ def mark_order_paid(*args, **kwargs): raise NotImplementedError("This method is no longer supported since pretix 1.17.") -def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None): +def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None, source=None): """ Reactivates a canceled order. If ``force`` is not set to ``True``, this will fail if there is not enough quota. @@ -189,7 +189,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None for m in position.granted_memberships.all(): m.canceled = False m.save() - order.create_transactions() + order.create_transactions(source=source) else: raise OrderError(is_available) @@ -202,7 +202,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None generate_invoice(order) -def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None): +def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None, source=None): """ Extends the deadline of an order. If the order is already expired, the quota will be checked to see if this is actually still possible. If ``force`` is set to ``True``, the result of this check @@ -231,7 +231,7 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User num_invoices = order.invoices.filter(is_cancellation=False).count() if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order): generate_invoice(order) - order.create_transactions() + order.create_transactions(source=source) if order.status == Order.STATUS_PENDING: change(was_expired=False) @@ -245,16 +245,17 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User @transaction.atomic -def mark_order_refunded(order, user=None, auth=None, api_token=None): +def mark_order_refunded(order, user=None, auth=None, api_token=None, source=None): oautha = auth.pk if isinstance(auth, OAuthApplication) else None device = auth.pk if isinstance(auth, Device) else None api_token = (api_token.pk if api_token else None) or (auth if isinstance(auth, TeamAPIToken) else None) return _cancel_order( - order.pk, user.pk if user else None, send_mail=False, api_token=api_token, device=device, oauth_application=oautha + order.pk, user.pk if user else None, send_mail=False, api_token=api_token, device=device, oauth_application=oautha, + source=source ) -def mark_order_expired(order, user=None, auth=None): +def mark_order_expired(order, user=None, auth=None, source=None): """ Mark this order as expired. This sets the payment status and returns the order object. :param order: The order to change @@ -273,13 +274,13 @@ def mark_order_expired(order, user=None, auth=None): i = order.invoices.filter(is_cancellation=False).last() if i and not i.refered.exists(): generate_cancellation(i) - order.create_transactions() + order.create_transactions(source=source) order_expired.send(order.event, order=order) return order -def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False): +def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False, source=None): """ Mark this order as approved :param order: The order to change @@ -292,7 +293,7 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False order.require_approval = False order.set_expires(now(), order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()])) order.save(update_fields=['require_approval', 'expires']) - order.create_transactions() + order.create_transactions(source=source) order.log_action('pretix.event.order.approved', user=user, auth=auth) if order.total == Decimal('0.00'): @@ -341,7 +342,7 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False return order.pk -def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None): +def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None, source=None): """ Mark this order as canceled :param order: The order to change @@ -365,7 +366,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None): for position in order.positions.all(): if position.voucher: Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) - order.create_transactions() + order.create_transactions(source=source) order_denied.send(order.event, order=order) @@ -386,7 +387,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None): def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None, - cancellation_fee=None, keep_fees=None, cancel_invoice=True, comment=None): + cancellation_fee=None, keep_fees=None, cancel_invoice=True, comment=None, source=None): """ Mark this order as canceled :param order: The order to change @@ -486,7 +487,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device data={'cancellation_fee': cancellation_fee, 'comment': comment}) order.cancellation_requests.all().delete() - order.create_transactions() + order.create_transactions(source=source) if send_mail: email_template = order.event.settings.mail_text_order_canceled @@ -813,7 +814,7 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid 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', gift_cards: list=None, shown_total=None, - customer=None): + customer=None, source=None): p = None sales_channel = get_all_sales_channels()[sales_channel] @@ -915,7 +916,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d ) orderpositions = OrderPosition.transform_cart_positions(positions, order) - order.create_transactions(positions=orderpositions, fees=fees, is_new=True) + order.create_transactions(positions=orderpositions, fees=fees, is_new=True, source=source, dt_now=now_dt) order.log_action('pretix.event.order.placed') if order.require_approval: order.log_action('pretix.event.order.placed.require_approval') @@ -967,7 +968,7 @@ 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', - gift_cards: list=None, shown_total=None, customer=None): + gift_cards: list=None, shown_total=None, customer=None, source=None): if payment_provider: pprov = event.get_payment_providers().get(payment_provider) if not pprov: @@ -1026,7 +1027,7 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str], _check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer) order, payment = _create_order(event, email, positions, now_dt, pprov, locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel, - gift_cards=gift_cards, shown_total=shown_total, customer=customer) + gift_cards=gift_cards, shown_total=shown_total, customer=customer, source=source) free_order_flow = payment and payment_provider == 'free' and order.pending_sum == Decimal('0.00') and not order.require_approval if free_order_flow: @@ -1088,7 +1089,7 @@ def expire_orders(sender, **kwargs): expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool) event_id = o.event_id if expire: - mark_order_expired(o) + mark_order_expired(o, source=("pretix.periodic", None)) @receiver(signal=periodic_task) @@ -1281,7 +1282,7 @@ class OrderChangeManager: CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff')) RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',)) - def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True): + def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, source=None): self.order = order self.user = user self.auth = auth @@ -1296,6 +1297,7 @@ class OrderChangeManager: self.notify = notify self._invoice_dirty = False self._invoices = [] + self.source = source def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]): if (not variation and item.has_variations) or (variation and variation.item_id != item.pk): @@ -2331,9 +2333,9 @@ class OrderChangeManager: self._reissue_invoice() self._clear_tickets_cache() self.order.touch() - self.order.create_transactions() + self.order.create_transactions(source=self.source) if self.split_order: - self.split_order.create_transactions() + self.split_order.create_transactions(source=self.source) if self.notify: notify_user_changed_order( @@ -2368,12 +2370,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', gift_cards: list=None, shown_total=None, customer=None): + sales_channel: str='web', gift_cards: list=None, shown_total=None, customer=None, source=None): with language(locale): try: try: return _perform_order(event, payment_provider, positions, email, locale, address, meta_info, - sales_channel, gift_cards, shown_total, customer) + sales_channel, gift_cards, shown_total, customer, source=source) except LockTimeoutException: self.retry() except (MaxRetriesExceededError, LockTimeoutException): @@ -2503,11 +2505,12 @@ def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial @scopes_disabled() def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None, device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, - email_comment=None, refund_comment=None, cancel_invoice=True): + email_comment=None, refund_comment=None, cancel_invoice=True, source=None): try: try: ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application, - cancellation_fee, cancel_invoice=cancel_invoice, comment=email_comment) + cancellation_fee, cancel_invoice=cancel_invoice, comment=email_comment, + source=source) if try_auto_refund: _try_auto_refund(order, refund_as_giftcard=refund_as_giftcard, comment=refund_comment) @@ -2519,7 +2522,7 @@ def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_tok def change_payment_provider(order: Order, payment_provider, amount=None, new_payment=None, create_log=True, - recreate_invoices=True): + recreate_invoices=True, source=None): if not get_connection().in_atomic_block: raise Exception('change_payment_provider should only be called in atomic transaction!') @@ -2607,7 +2610,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay generate_cancellation(i) generate_invoice(order) - order.create_transactions() + order.create_transactions(source=source) return old_fee, new_fee, fee, new_payment diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 1f1f22842b..d68850a82d 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -557,7 +557,7 @@ class OrderApprove(OrderView): def post(self, *args, **kwargs): if self.order.require_approval: try: - approve_order(self.order, user=self.request.user) + approve_order(self.order, user=self.request.user, source=("pretix.control", f"user:{self.request.user.pk}")) except OrderError as e: messages.error(self.request, str(e)) else: @@ -608,7 +608,8 @@ class OrderDeny(OrderView): try: deny_order(self.order, user=self.request.user, comment=self.request.POST.get('comment'), - send_mail=self.request.POST.get('send_email') == 'on') + send_mail=self.request.POST.get('send_email') == 'on', + source=("pretix.control", f"user:{self.request.user.pk}")) except OrderError as e: messages.error(self.request, str(e)) else: @@ -703,7 +704,7 @@ class OrderRefundProcess(OrderView): if self.order.status != Order.STATUS_CANCELED and self.order.positions.exists(): if self.request.POST.get("action") == "r": - mark_order_refunded(self.order, user=self.request.user) + mark_order_refunded(self.order, user=self.request.user, source=("pretix.control", f"user:{self.request.user.pk}")) elif not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0): self.order.status = Order.STATUS_PENDING self.order.set_expires( @@ -1071,7 +1072,7 @@ class OrderRefundView(OrderView): if any_success: if self.start_form.cleaned_data.get('action') == 'mark_refunded': if self.order.cancel_allowed(): - mark_order_refunded(self.order, user=self.request.user) + mark_order_refunded(self.order, user=self.request.user, source=("pretix.control", f"user:{self.request.user.pk}")) elif self.start_form.cleaned_data.get('action') == 'mark_pending': if not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0): self.order.status = Order.STATUS_PENDING @@ -1274,7 +1275,8 @@ class OrderTransition(OrderView): email_comment=self.mark_canceled_form.cleaned_data['comment'], send_mail=self.mark_canceled_form.cleaned_data['send_email'], cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True), - cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee')) + cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'), + source=("pretix.control", f"user:{self.request.user.pk}")) except OrderError as e: messages.error(self.request, str(e)) else: @@ -1297,7 +1299,7 @@ class OrderTransition(OrderView): else: return self.get(self.request, *args, **kwargs) elif self.order.status == Order.STATUS_PENDING and to == 'e': - mark_order_expired(self.order, user=self.request.user) + mark_order_expired(self.order, user=self.request.user, source=("pretix.control", f"user:{self.request.user.pk}")) messages.success(self.request, _('The order has been marked as expired.')) return redirect(self.get_order_url()) @@ -1574,7 +1576,8 @@ class OrderReactivate(OrderView): reactivate_order( self.order, user=self.request.user, - force=self.reactivate_form.cleaned_data.get('force', False) + force=self.reactivate_form.cleaned_data.get('force', False), + source=("pretix.control", f"user:{self.request.user.pk}"), ) messages.success(self.request, _('The order has been reactivated.')) except OrderError as e: diff --git a/src/pretix/plugins/banktransfer/tasks.py b/src/pretix/plugins/banktransfer/tasks.py index ed4f09fb4b..53de7aa324 100644 --- a/src/pretix/plugins/banktransfer/tasks.py +++ b/src/pretix/plugins/banktransfer/tasks.py @@ -194,7 +194,7 @@ def _handle_transaction(trans: BankTransaction, matches: tuple, event: Event = N if created: # We're perform a payment method switching on-demand here old_fee, new_fee, fee, p = change_payment_provider(order, p.payment_provider, p.amount, - new_payment=p, create_log=False) # noqa + new_payment=p, create_log=False, source=("pretix.plugins.banktransfer", None)) # noqa if fee: p.fee = fee p.save(update_fields=['fee']) diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 1a5fc79b29..be56e7f281 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -73,7 +73,7 @@ from pretix.presale.forms.customer import AuthenticationForm, RegistrationForm from pretix.presale.signals import ( checkout_all_optional, checkout_confirm_messages, checkout_flow_steps, contact_form_fields, contact_form_fields_overrides, - order_meta_from_request, question_form_fields, + order_meta_from_request, order_source_from_request, question_form_fields, question_form_fields_overrides, ) from pretix.presale.utils import customer_login @@ -1319,6 +1319,11 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): for receiver, response in order_meta_from_request.send(sender=request.event, request=request): meta_info.update(response) + source = ("pretix.presale", request.sales_channel.identifier) + for receiver, response in order_source_from_request.send(sender=request.event, request=request): + if response: + source = response + return self.do( self.request.event.id, payment_provider=self.payment_provider.identifier if self.payment_provider else None, @@ -1331,6 +1336,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): gift_cards=self.cart_session.get('gift_cards'), shown_total=self.cart_session.get('shown_total'), customer=self.cart_session.get('customer'), + source=source, ) def get_success_message(self, value): diff --git a/src/pretix/presale/signals.py b/src/pretix/presale/signals.py index 49e7acced3..0ce6935060 100644 --- a/src/pretix/presale/signals.py +++ b/src/pretix/presale/signals.py @@ -184,6 +184,22 @@ You will receive the request triggering the order creation as the ``request`` ke As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ + + +order_source_from_request = EventPluginSignal() +""" +Arguments: ``request`` + +This signal is sent before an order is created through the pretixpresale frontend. It allows you +to return a tuple that will be used as the ``source_type, source_identifier`` information on the +created transactions. If multiple plugins reply with a value that is not ``None``, there is no +guarantee of preference so please only respond with a tuple if you're really sure that this plugin +is the only one that cares about this order. +You will receive the request triggering the order creation as the ``request`` keyword argument. + +As with all event-plugin signals, the ``sender`` keyword argument will contain the event. +""" + checkout_confirm_page_content = EventPluginSignal() """ Arguments: 'request' diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index c3dc6f8c6d..608001cb83 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -669,7 +669,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView): request.session['payment_change_{}'.format(self.order.pk)] = '1' with transaction.atomic(): - old_fee, new_fee, fee, newpayment = change_payment_provider(self.order, p['provider'], None) + old_fee, new_fee, fee, newpayment = change_payment_provider(self.order, p['provider'], None, source=("pretix.presale", None)) resp = p['provider'].payment_prepare(request, newpayment) if isinstance(resp, str): @@ -954,7 +954,7 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View): else: comment = gettext('Canceled by customer') return self.do(self.order.pk, cancellation_fee=fee, try_auto_refund=auto_refund, refund_as_giftcard=giftcard, - refund_comment=comment) + refund_comment=comment, source=("pretix.presale", None)) def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs)