Compare commits

...

1 Commits

Author SHA1 Message Date
Raphael Michel
d169958687 Add Transaction.source flag 2022-10-19 12:09:55 +02:00
14 changed files with 147 additions and 54 deletions

View File

@@ -38,7 +38,7 @@ Frontend
.. automodule:: pretix.presale.signals .. 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 Request flow
"""""""""""" """"""""""""

View File

@@ -1378,7 +1378,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
state=OrderPayment.PAYMENT_STATE_CREATED 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 return order

14
src/pretix/api/utils.py Normal file
View File

@@ -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

View File

@@ -61,6 +61,7 @@ from pretix.api.serializers.orderchange import (
OrderPositionCreateForExistingOrderSerializer, OrderPositionCreateForExistingOrderSerializer,
OrderPositionInfoPatchSerializer, OrderPositionInfoPatchSerializer,
) )
from pretix.api.utils import get_api_source
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue, CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
@@ -190,6 +191,7 @@ class OrderViewSet(viewsets.ModelViewSet):
ctx['event'] = self.request.event ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true' ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
ctx['exclude'] = self.request.query_params.getlist('exclude') ctx['exclude'] = self.request.query_params.getlist('exclude')
ctx['source'] = get_api_source(self.request)
return ctx return ctx
def get_queryset(self): def get_queryset(self):
@@ -390,7 +392,8 @@ class OrderViewSet(viewsets.ModelViewSet):
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None, oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
send_mail=send_mail, send_mail=send_mail,
email_comment=comment, email_comment=comment,
cancellation_fee=cancellation_fee cancellation_fee=cancellation_fee,
source=get_api_source(request),
) )
except OrderError as e: except OrderError as e:
return Response( return Response(
@@ -414,6 +417,7 @@ class OrderViewSet(viewsets.ModelViewSet):
order, order,
user=request.user if request.user.is_authenticated else None, user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None, auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
source=get_api_source(request),
) )
except OrderError as e: except OrderError as e:
return Response( return Response(
@@ -433,6 +437,7 @@ class OrderViewSet(viewsets.ModelViewSet):
user=request.user if request.user.is_authenticated else None, user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None, auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
send_mail=send_mail, send_mail=send_mail,
source=get_api_source(request),
) )
except Quota.QuotaExceededException as e: except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) 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, auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
send_mail=send_mail, send_mail=send_mail,
comment=comment, comment=comment,
source=get_api_source(request),
) )
except OrderError as e: except OrderError as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -491,6 +497,7 @@ class OrderViewSet(viewsets.ModelViewSet):
order, order,
user=request.user if request.user.is_authenticated else None, user=request.user if request.user.is_authenticated else None,
auth=request.auth, auth=request.auth,
source=get_api_source(request),
) )
return self.retrieve(request, [], **kwargs) return self.retrieve(request, [], **kwargs)
@@ -508,6 +515,7 @@ class OrderViewSet(viewsets.ModelViewSet):
order, order,
user=request.user if request.user.is_authenticated else None, user=request.user if request.user.is_authenticated else None,
auth=(request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken, Device)) else None), auth=(request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken, Device)) else None),
source=get_api_source(request),
) )
return self.retrieve(request, [], **kwargs) return self.retrieve(request, [], **kwargs)
@@ -1484,7 +1492,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
if mark_refunded: if mark_refunded:
mark_order_refunded(payment.order, mark_order_refunded(payment.order,
user=self.request.user if self.request.user.is_authenticated else None, 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: else:
payment.order.status = Order.STATUS_PENDING payment.order.status = Order.STATUS_PENDING
payment.order.set_expires( payment.order.set_expires(
@@ -1557,7 +1566,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
mark_refunded = request.data.get('mark_canceled', False) mark_refunded = request.data.get('mark_canceled', False)
if mark_refunded: if mark_refunded:
mark_order_refunded(refund.order, user=self.request.user if self.request.user.is_authenticated else None, 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): elif not (refund.order.status == Order.STATUS_PAID and refund.order.pending_sum <= 0):
refund.order.status = Order.STATUS_PENDING refund.order.status = Order.STATUS_PENDING
refund.order.set_expires( refund.order.set_expires(
@@ -1610,6 +1619,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
r.order, r.order,
user=request.user if request.user.is_authenticated else None, user=request.user if request.user.is_authenticated else None,
auth=(request.auth if request.auth else None), auth=(request.auth if request.auth else None),
source=get_api_source(self.request),
) )
except OrderError as e: except OrderError as e:
raise ValidationError(str(e)) raise ValidationError(str(e))

View File

@@ -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),
),
]

View File

@@ -1041,10 +1041,13 @@ class Order(LockModel, LoggedModel):
continue continue
yield op yield op
def create_transactions(self, is_new=False, positions=None, fees=None, dt_now=None, migrated=False, def create_transactions(self, *, source=None, is_new=False, positions=None, fees=None,
_backfill_before_cancellation=False, save=True): dt_now=None, migrated=False, _backfill_before_cancellation=False, save=True):
dt_now = dt_now or now() 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 # Count the transactions we already have
current_transaction_count = Counter() current_transaction_count = Counter()
if not is_new: if not is_new:
@@ -1089,6 +1092,8 @@ class Order(LockModel, LoggedModel):
tax_value=taxvalue, tax_value=taxvalue,
fee_type=feetype, fee_type=feetype,
internal_type=internaltype, 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)) create.sort(key=lambda t: (0 if t.count < 0 else 1, t.positionid or 0))
if save: if save:
@@ -1573,7 +1578,7 @@ class OrderPayment(models.Model):
return self.order.event.get_payment_providers(cached=True).get(self.provider) return self.order.event.get_payment_providers(cached=True).get(self.provider)
@transaction.atomic() @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 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) 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: 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) self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
order_paid.send(self.order.event, order=self.order) order_paid.send(self.order.event, order=self.order)
if status_change: 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): def fail(self, info=None, user=None, auth=None, log_data=None):
""" """
@@ -1630,7 +1637,7 @@ class OrderPayment(models.Model):
}, user=user, auth=auth) }, user=user, auth=auth)
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='',
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 Marks the payment as complete. If possible, this also marks the order as paid if no further
payment is required payment is required
@@ -1693,10 +1700,11 @@ class OrderPayment(models.Model):
)) ))
return 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='', 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 ( from pretix.base.services.invoices import (
generate_invoice, invoice_qualified, generate_invoice, invoice_qualified,
) )
@@ -1710,7 +1718,7 @@ class OrderPayment(models.Model):
with lockfn(): with lockfn():
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total, 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 invoice = None
if invoice_qualified(self.order): if invoice_qualified(self.order):
@@ -2483,6 +2491,8 @@ class Transaction(models.Model):
:param id: ID of the transaction :param id: ID of the transaction
:param order: Order the transaction belongs to :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 datetime: Date and time of the transaction
:param migrated: Whether this object was reconstructed because the order was created before transactions where introduced :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 :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', related_name='transactions',
on_delete=models.PROTECT 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( created = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
db_index=True, db_index=True,

View File

@@ -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 += min(p.price, Decimal(keep_fee_per_ticket))
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) 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 refund_amount = o.payment_refund_sum
try: try:

View File

@@ -195,7 +195,8 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
user=user, user=user,
data={'source': 'import'} 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) Transaction.objects.bulk_create(save_transactions)
for o in orders: for o in orders:

View File

@@ -148,7 +148,7 @@ def mark_order_paid(*args, **kwargs):
raise NotImplementedError("This method is no longer supported since pretix 1.17.") 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 Reactivates a canceled order. If ``force`` is not set to ``True``, this will fail if there is not
enough quota. 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(): for m in position.granted_memberships.all():
m.canceled = False m.canceled = False
m.save() m.save()
order.create_transactions() order.create_transactions(source=source)
else: else:
raise OrderError(is_available) raise OrderError(is_available)
@@ -202,7 +202,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
generate_invoice(order) 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 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 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() 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): if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order) generate_invoice(order)
order.create_transactions() order.create_transactions(source=source)
if order.status == Order.STATUS_PENDING: if order.status == Order.STATUS_PENDING:
change(was_expired=False) change(was_expired=False)
@@ -245,16 +245,17 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
@transaction.atomic @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 oautha = auth.pk if isinstance(auth, OAuthApplication) else None
device = auth.pk if isinstance(auth, Device) 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) api_token = (api_token.pk if api_token else None) or (auth if isinstance(auth, TeamAPIToken) else None)
return _cancel_order( 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. Mark this order as expired. This sets the payment status and returns the order object.
:param order: The order to change :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() i = order.invoices.filter(is_cancellation=False).last()
if i and not i.refered.exists(): if i and not i.refered.exists():
generate_cancellation(i) generate_cancellation(i)
order.create_transactions() order.create_transactions(source=source)
order_expired.send(order.event, order=order) order_expired.send(order.event, order=order)
return 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 Mark this order as approved
:param order: The order to change :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.require_approval = False
order.set_expires(now(), order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()])) 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.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) order.log_action('pretix.event.order.approved', user=user, auth=auth)
if order.total == Decimal('0.00'): 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 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 Mark this order as canceled
:param order: The order to change :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(): for position in order.positions.all():
if position.voucher: if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) 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) 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, 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 Mark this order as canceled
:param order: The order to change :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}) data={'cancellation_fee': cancellation_fee, 'comment': comment})
order.cancellation_requests.all().delete() order.cancellation_requests.all().delete()
order.create_transactions() order.create_transactions(source=source)
if send_mail: if send_mail:
email_template = order.event.settings.mail_text_order_canceled 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, def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None, payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None,
meta_info: dict=None, sales_channel: str='web', gift_cards: list=None, shown_total=None, meta_info: dict=None, sales_channel: str='web', gift_cards: list=None, shown_total=None,
customer=None): customer=None, source=None):
p = None p = None
sales_channel = get_all_sales_channels()[sales_channel] 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) 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') order.log_action('pretix.event.order.placed')
if order.require_approval: if order.require_approval:
order.log_action('pretix.event.order.placed.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], 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, shown_total=None, customer=None): gift_cards: list=None, shown_total=None, customer=None, source=None):
if payment_provider: if payment_provider:
pprov = event.get_payment_providers().get(payment_provider) pprov = event.get_payment_providers().get(payment_provider)
if not pprov: 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) _check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
order, payment = _create_order(event, email, positions, now_dt, pprov, 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, 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 free_order_flow = payment and payment_provider == 'free' and order.pending_sum == Decimal('0.00') and not order.require_approval
if free_order_flow: 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) expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool)
event_id = o.event_id event_id = o.event_id
if expire: if expire:
mark_order_expired(o) mark_order_expired(o, source=("pretix.periodic", None))
@receiver(signal=periodic_task) @receiver(signal=periodic_task)
@@ -1281,7 +1282,7 @@ class OrderChangeManager:
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff')) CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff'))
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',)) 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.order = order
self.user = user self.user = user
self.auth = auth self.auth = auth
@@ -1296,6 +1297,7 @@ class OrderChangeManager:
self.notify = notify self.notify = notify
self._invoice_dirty = False self._invoice_dirty = False
self._invoices = [] self._invoices = []
self.source = source
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]): 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): 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._reissue_invoice()
self._clear_tickets_cache() self._clear_tickets_cache()
self.order.touch() self.order.touch()
self.order.create_transactions() self.order.create_transactions(source=self.source)
if self.split_order: if self.split_order:
self.split_order.create_transactions() self.split_order.create_transactions(source=self.source)
if self.notify: if self.notify:
notify_user_changed_order( 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,)) @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], 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, 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): with language(locale):
try: try:
try: try:
return _perform_order(event, payment_provider, positions, email, locale, address, meta_info, 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: except LockTimeoutException:
self.retry() self.retry()
except (MaxRetriesExceededError, LockTimeoutException): except (MaxRetriesExceededError, LockTimeoutException):
@@ -2503,11 +2505,12 @@ def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial
@scopes_disabled() @scopes_disabled()
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None, 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, 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:
try: try:
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application, 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: if try_auto_refund:
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard, _try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
comment=refund_comment) 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, 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: if not get_connection().in_atomic_block:
raise Exception('change_payment_provider should only be called in atomic transaction!') 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_cancellation(i)
generate_invoice(order) generate_invoice(order)
order.create_transactions() order.create_transactions(source=source)
return old_fee, new_fee, fee, new_payment return old_fee, new_fee, fee, new_payment

View File

@@ -557,7 +557,7 @@ class OrderApprove(OrderView):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
if self.order.require_approval: if self.order.require_approval:
try: 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: except OrderError as e:
messages.error(self.request, str(e)) messages.error(self.request, str(e))
else: else:
@@ -608,7 +608,8 @@ class OrderDeny(OrderView):
try: try:
deny_order(self.order, user=self.request.user, deny_order(self.order, user=self.request.user,
comment=self.request.POST.get('comment'), 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: except OrderError as e:
messages.error(self.request, str(e)) messages.error(self.request, str(e))
else: else:
@@ -703,7 +704,7 @@ class OrderRefundProcess(OrderView):
if self.order.status != Order.STATUS_CANCELED and self.order.positions.exists(): if self.order.status != Order.STATUS_CANCELED and self.order.positions.exists():
if self.request.POST.get("action") == "r": 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): elif not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
self.order.status = Order.STATUS_PENDING self.order.status = Order.STATUS_PENDING
self.order.set_expires( self.order.set_expires(
@@ -1071,7 +1072,7 @@ class OrderRefundView(OrderView):
if any_success: if any_success:
if self.start_form.cleaned_data.get('action') == 'mark_refunded': if self.start_form.cleaned_data.get('action') == 'mark_refunded':
if self.order.cancel_allowed(): 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': elif self.start_form.cleaned_data.get('action') == 'mark_pending':
if not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0): if not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
self.order.status = Order.STATUS_PENDING self.order.status = Order.STATUS_PENDING
@@ -1274,7 +1275,8 @@ class OrderTransition(OrderView):
email_comment=self.mark_canceled_form.cleaned_data['comment'], email_comment=self.mark_canceled_form.cleaned_data['comment'],
send_mail=self.mark_canceled_form.cleaned_data['send_email'], send_mail=self.mark_canceled_form.cleaned_data['send_email'],
cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True), 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: except OrderError as e:
messages.error(self.request, str(e)) messages.error(self.request, str(e))
else: else:
@@ -1297,7 +1299,7 @@ class OrderTransition(OrderView):
else: else:
return self.get(self.request, *args, **kwargs) return self.get(self.request, *args, **kwargs)
elif self.order.status == Order.STATUS_PENDING and to == 'e': 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.')) messages.success(self.request, _('The order has been marked as expired.'))
return redirect(self.get_order_url()) return redirect(self.get_order_url())
@@ -1574,7 +1576,8 @@ class OrderReactivate(OrderView):
reactivate_order( reactivate_order(
self.order, self.order,
user=self.request.user, 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.')) messages.success(self.request, _('The order has been reactivated.'))
except OrderError as e: except OrderError as e:

View File

@@ -194,7 +194,7 @@ def _handle_transaction(trans: BankTransaction, matches: tuple, event: Event = N
if created: if created:
# We're perform a payment method switching on-demand here # 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, 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: if fee:
p.fee = fee p.fee = fee
p.save(update_fields=['fee']) p.save(update_fields=['fee'])

View File

@@ -73,7 +73,7 @@ from pretix.presale.forms.customer import AuthenticationForm, RegistrationForm
from pretix.presale.signals import ( from pretix.presale.signals import (
checkout_all_optional, checkout_confirm_messages, checkout_flow_steps, checkout_all_optional, checkout_confirm_messages, checkout_flow_steps,
contact_form_fields, contact_form_fields_overrides, 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, question_form_fields_overrides,
) )
from pretix.presale.utils import customer_login 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): for receiver, response in order_meta_from_request.send(sender=request.event, request=request):
meta_info.update(response) 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( return self.do(
self.request.event.id, self.request.event.id,
payment_provider=self.payment_provider.identifier if self.payment_provider else None, 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'), gift_cards=self.cart_session.get('gift_cards'),
shown_total=self.cart_session.get('shown_total'), shown_total=self.cart_session.get('shown_total'),
customer=self.cart_session.get('customer'), customer=self.cart_session.get('customer'),
source=source,
) )
def get_success_message(self, value): def get_success_message(self, value):

View File

@@ -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. 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() checkout_confirm_page_content = EventPluginSignal()
""" """
Arguments: 'request' Arguments: 'request'

View File

@@ -669,7 +669,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
request.session['payment_change_{}'.format(self.order.pk)] = '1' request.session['payment_change_{}'.format(self.order.pk)] = '1'
with transaction.atomic(): 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) resp = p['provider'].payment_prepare(request, newpayment)
if isinstance(resp, str): if isinstance(resp, str):
@@ -954,7 +954,7 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
else: else:
comment = gettext('Canceled by customer') comment = gettext('Canceled by customer')
return self.do(self.order.pk, cancellation_fee=fee, try_auto_refund=auto_refund, refund_as_giftcard=giftcard, 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): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)