diff --git a/doc/api/resources/giftcards.rst b/doc/api/resources/giftcards.rst
index 6756be13f..5091affc9 100644
--- a/doc/api/resources/giftcards.rst
+++ b/doc/api/resources/giftcards.rst
@@ -24,6 +24,8 @@ owner_ticket integer Internal ID of
this gift card and can view all transactions. When setting
this field, you can also give the ``secret`` of an order
position.
+issuer string Organizer slug of the organizer who created this gift
+ card and is responsible for it.
===================================== ========================== =======================================================
The gift card transaction resource contains the following public fields:
@@ -39,8 +41,17 @@ value money (string) Transaction amo
event string Event slug, if the gift card was used in the web shop (or ``null``)
order string Order code, if the gift card was used in the web shop (or ``null``)
text string Custom text of the transaction (or ``null``)
+info object Additional data about the transaction (or ``null``)
+acceptor string Organizer slug of the organizer who created this transaction
+ (can be ``null`` for all transactions performed before
+ this field was added.)
===================================== ========================== =======================================================
+.. versionchanged:: 4.20
+
+ The ``owner_ticket`` and ``issuer`` attributes of the gift card and the ``info`` and ``acceptor`` attributes of the
+ gift card transaction resource have been added.
+
Endpoints
---------
@@ -77,6 +88,7 @@ Endpoints
"expires": null,
"conditions": null,
"owner_ticket": null,
+ "issuer": "bigevents",
"value": "13.37"
}
]
@@ -123,6 +135,7 @@ Endpoints
"expires": null,
"conditions": null,
"owner_ticket": null,
+ "issuer": "bigevents",
"value": "13.37"
}
@@ -168,6 +181,7 @@ Endpoints
"expires": null,
"conditions": null,
"owner_ticket": null,
+ "issuer": "bigevents",
"value": "13.37"
}
@@ -221,6 +235,7 @@ Endpoints
"expires": null,
"conditions": null,
"owner_ticket": null,
+ "issuer": "bigevents",
"value": "14.00"
}
@@ -267,6 +282,7 @@ Endpoints
"expires": null,
"conditions": null,
"owner_ticket": null,
+ "issuer": "bigevents",
"value": "15.37"
}
@@ -310,7 +326,11 @@ Endpoints
"value": "50.00",
"event": "democon",
"order": "FXQYW",
- "text": null
+ "text": null,
+ "acceptor": "bigevents",
+ "info": {
+ "created_by": "plugin1"
+ }
}
]
}
diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst
index 361527d10..aaee300a6 100644
--- a/doc/development/api/general.rst
+++ b/doc/development/api/general.rst
@@ -13,7 +13,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
- register_ticket_secret_generators
+ register_ticket_secret_generators, gift_card_transaction_display
Order events
""""""""""""
diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py
index 07a16623d..9e7413a43 100644
--- a/src/pretix/api/serializers/organizer.py
+++ b/src/pretix/api/serializers/organizer.py
@@ -160,6 +160,7 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
class GiftCardSerializer(I18nAwareModelSerializer):
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
+ issuer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -196,7 +197,8 @@ class GiftCardSerializer(I18nAwareModelSerializer):
class Meta:
model = GiftCard
- fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket')
+ fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
+ 'issuer')
class OrderEventSlugField(serializers.RelatedField):
@@ -207,11 +209,12 @@ class OrderEventSlugField(serializers.RelatedField):
class GiftCardTransactionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
+ acceptor = serializers.SlugRelatedField(slug_field='slug', read_only=True)
event = OrderEventSlugField(source='order', read_only=True)
class Meta:
model = GiftCardTransaction
- fields = ('id', 'datetime', 'value', 'event', 'order', 'text')
+ fields = ('id', 'datetime', 'value', 'event', 'order', 'text', 'info', 'acceptor')
class EventSlugField(serializers.SlugRelatedField):
diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py
index 81561afee..d71721ed3 100644
--- a/src/pretix/api/views/organizer.py
+++ b/src/pretix/api/views/organizer.py
@@ -155,7 +155,9 @@ class GiftCardViewSet(viewsets.ModelViewSet):
qs = self.request.organizer.accepted_gift_cards
else:
qs = self.request.organizer.issued_gift_cards.all()
- return qs
+ return qs.prefetch_related(
+ 'issuer'
+ )
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -166,7 +168,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
value = serializer.validated_data.pop('value')
inst = serializer.save(issuer=self.request.organizer)
- inst.transactions.create(value=value)
+ inst.transactions.create(value=value, acceptor=self.request.organizer)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
@@ -197,7 +199,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
if 'value' in self.request.data and value is not None:
old_value = serializer.instance.value
diff = value - old_value
- inst.transactions.create(value=diff)
+ inst.transactions.create(value=diff, acceptor=self.request.organizer)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
@@ -217,11 +219,14 @@ class GiftCardViewSet(viewsets.ModelViewSet):
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
request.data.get('text', '')
)
+ info = serializers.JSONField(required=False, allow_null=True).to_internal_value(
+ request.data.get('info', {})
+ )
if gc.value + value < Decimal('0.00'):
return Response({
'value': ['The gift card does not have sufficient credit for this operation.']
}, status=status.HTTP_409_CONFLICT)
- gc.transactions.create(value=value, text=text)
+ gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer)
gc.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
@@ -249,7 +254,7 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
return get_object_or_404(qs, pk=self.kwargs.get('giftcard'))
def get_queryset(self):
- return self.giftcard.transactions.select_related('order', 'order__event')
+ return self.giftcard.transactions.select_related('order', 'order__event').prefetch_related('acceptor')
class TeamViewSet(viewsets.ModelViewSet):
diff --git a/src/pretix/base/migrations/0239_giftcard_info.py b/src/pretix/base/migrations/0239_giftcard_info.py
new file mode 100644
index 000000000..a8822af28
--- /dev/null
+++ b/src/pretix/base/migrations/0239_giftcard_info.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.18 on 2023-05-11 11:02
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0238_giftcard_owner_ticket'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='giftcardtransaction',
+ name='acceptor',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='gift_card_transactions', to='pretixbase.organizer'),
+ ),
+ migrations.AddField(
+ model_name='giftcardtransaction',
+ name='info',
+ field=models.JSONField(null=True),
+ ),
+ ]
diff --git a/src/pretix/base/models/giftcards.py b/src/pretix/base/models/giftcards.py
index 470b7bb21..387ca6bf2 100644
--- a/src/pretix/base/models/giftcards.py
+++ b/src/pretix/base/models/giftcards.py
@@ -25,7 +25,9 @@ from django.conf import settings
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Sum
+from django.urls import reverse
from django.utils.crypto import get_random_string
+from django.utils.html import format_html
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
@@ -160,6 +162,61 @@ class GiftCardTransaction(models.Model):
on_delete=models.PROTECT
)
text = models.TextField(blank=True, null=True)
+ info = models.JSONField(
+ null=True, blank=True,
+ )
+ acceptor = models.ForeignKey(
+ 'Organizer',
+ related_name='gift_card_transactions',
+ on_delete=models.PROTECT,
+ null=True, blank=True
+ )
class Meta:
ordering = ("datetime",)
+
+ def save(self, *args, **kwargs):
+ if not self.pk and not self.acceptor:
+ raise ValueError("`acceptor` should be set on all new gift card transactions.")
+ super().save(*args, **kwargs)
+
+ def display(self, customer_facing=True):
+ from ..signals import gift_card_transaction_display
+
+ for receiver, response in gift_card_transaction_display.send(self, transaction=self, customer_facing=customer_facing):
+ if response:
+ return response
+
+ if self.order_id:
+ if not self.text:
+ if not customer_facing:
+ return format_html(
+ '{}',
+ reverse(
+ "control:event.order",
+ kwargs={
+ "event": self.order.event.slug,
+ "organizer": self.order.event.organizer.slug,
+ "code": self.order.code,
+ }
+ ),
+ self.order.full_code
+ )
+ return self.order.full_code
+ else:
+ return self.text
+ else:
+ if self.text:
+ return format_html(
+ '{}: {}',
+ _('Manual transaction'),
+ self.text,
+ )
+ else:
+ return _('Manual transaction')
+
+ def display_backend(self):
+ return self.display(customer_facing=False)
+
+ def display_presale(self):
+ return self.display(customer_facing=True)
diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py
index 39f2753ee..6e82227f0 100644
--- a/src/pretix/base/payment.py
+++ b/src/pretix/base/payment.py
@@ -1469,7 +1469,8 @@ class GiftCardPayment(BasePaymentProvider):
trans = gc.transactions.create(
value=-1 * payment.amount,
order=payment.order,
- payment=payment
+ payment=payment,
+ acceptor=self.event.organizer,
)
payment.info_data = {
'gift_card': gc.pk,
@@ -1490,7 +1491,8 @@ class GiftCardPayment(BasePaymentProvider):
trans = gc.transactions.create(
value=refund.amount,
order=refund.order,
- refund=refund
+ refund=refund,
+ acceptor=self.event.organizer,
)
refund.info_data = {
'gift_card': gc.pk,
diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py
index 301c4e135..8c7fb7751 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -235,7 +235,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
for gc in position.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
- gc.transactions.create(value=position.price, order=order)
+ gc.transactions.create(value=position.price, order=order, acceptor=order.event.organizer)
break
for m in position.granted_memberships.all():
@@ -513,7 +513,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
)
)
else:
- gc.transactions.create(value=-position.price, order=order)
+ gc.transactions.create(value=-position.price, order=order, acceptor=order.event.organizer)
for m in position.granted_memberships.all():
m.canceled = True
@@ -2186,7 +2186,7 @@ class OrderChangeManager:
card=gc.secret
))
else:
- gc.transactions.create(value=-op.position.price, order=self.order)
+ gc.transactions.create(value=-op.position.price, order=self.order, acceptor=self.order.event.organizer)
for m in op.position.granted_memberships.with_usages().all():
m.canceled = True
@@ -2202,7 +2202,7 @@ class OrderChangeManager:
card=gc.secret
))
else:
- gc.transactions.create(value=-opa.position.price, order=self.order)
+ gc.transactions.create(value=-opa.position.price, order=self.order, acceptor=self.order.event.organizer)
for m in opa.granted_memberships.with_usages().all():
m.canceled = True
@@ -2918,7 +2918,7 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
currency=sender.currency, issued_in=p, testmode=order.testmode,
expires=sender.organizer.default_gift_card_expiry,
)
- gc.transactions.create(value=p.price - issued, order=order)
+ gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer)
any_giftcards = True
p.secret = gc.secret
p.save(update_fields=['secret'])
diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py
index 859799f0d..67d4020db 100644
--- a/src/pretix/base/signals.py
+++ b/src/pretix/base/signals.py
@@ -578,6 +578,20 @@ All plugins that are installed may send fields for the global settings form, as
an OrderedDict of (setting name, form field).
"""
+gift_card_transaction_display = django.dispatch.Signal()
+"""
+Arguments: ``transaction``, ``customer_facing``
+
+To display an instance of the ``GiftCardTransaction`` model to a human user,
+``pretix.base.signals.gift_card_transaction_display`` will be sent out with a ``transaction`` argument.
+The ``customer_facing`` argument specifies whether the HTML will be shown to an end-user or if it is being
+used in the backend.
+
+The first received response that is not ``None`` will be used to display the log entry
+to the user. The receivers are expected to return a string (that might be marked with ``mark_safe`` from Django if
+it contains HTML).
+"""
+
order_fee_calculation = EventPluginSignal()
"""
Arguments: ``positions``, ``invoice_address``, ``meta_info``, ``total``, ``gift_cards``, ``payment_requests``
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html b/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html
index 032d65f70..13aa41507 100644
--- a/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html
+++ b/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html
@@ -70,29 +70,32 @@
{% trans "Date" %}
- {% trans "Order" %}
+ {% trans "Information" %}
{% trans "Value" %}
{{ t.info|pprint }}
+ {% endif %}
+ {% if t.acceptor and t.acceptor != request.organizer %}
+
+