GiftCard: Add more information to transactions (#3308)

This commit is contained in:
Raphael Michel
2023-05-12 09:38:35 +02:00
committed by GitHub
parent 916ee0697f
commit c0419518c3
19 changed files with 247 additions and 85 deletions

View File

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

View File

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

View File

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

View File

@@ -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(
'<a href="{}">{}</a>',
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(
'<em>{}:</em> {}',
_('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)

View File

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

View File

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

View File

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

View File

@@ -70,29 +70,32 @@
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Order" %}</th>
<th>{% trans "Information" %}</th>
<th class="text-right">{% trans "Value" %}</th>
</tr>
</thead>
<tbody>
{% for t in card.transactions.all %}
{% for t in transactions %}
<tr>
<td>{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>
{% if t.order %}
<a href="{% url "control:event.order" event=t.order.event.slug organizer=t.order.event.organizer.slug code=t.order.code %}">
{{ t.order.full_code }}
</a>
{% if t.refund and t.value > 0 and t.value <= card.value %}
<button type="submit" name="revert" value="{{ t.pk }}"
class="btn btn-default btn-xs" data-toggle="tooltip"
title="{% trans "Create a payment on the respective order that cancels out with this transaction. The order will then likely be overpaid." %}">
<span class="fa fa-repeat"></span>
{% trans "Revert" %}
</button>
{% endif %}
{% else %}
<em>{% trans "Manual transaction" %}{% if t.text %}: {{ t.text }}{% endif %}</em>
{{ t.display_backend }}
{% if t.refund and t.value > 0 and t.value <= card.value %}
<button type="submit" name="revert" value="{{ t.pk }}"
class="btn btn-default btn-xs" data-toggle="tooltip"
title="{% trans "Create a payment on the respective order that cancels out with this transaction. The order will then likely be overpaid." %}">
<span class="fa fa-repeat"></span>
{% trans "Revert" %}
</button>
{% endif %}
{% if staff_session and t.info %}
<pre><code>{{ t.info|pprint }}</code></pre>
{% endif %}
{% if t.acceptor and t.acceptor != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ t.acceptor }}
</span>
{% endif %}
</td>
<td class="text-right">

View File

@@ -1452,6 +1452,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
self.object.transactions.create(
value=value,
text=request.POST.get('text') or None,
acceptor=request.organizer,
)
self.object.log_action(
'pretix.giftcards.transaction.manual',
@@ -1471,6 +1472,16 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
))
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
return super().get_context_data(
**kwargs,
transactions=self.object.transactions.select_related(
'order', 'order__event', 'order__event__organizer', 'payment', 'refund'
).prefetch_related(
'acceptor'
)
)
class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/giftcard_create.html'
@@ -1497,6 +1508,7 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
form.instance.issuer = self.request.organizer
super().form_valid(form)
form.instance.transactions.create(
acceptor=self.request.organizer,
value=form.cleaned_data['value']
)
form.instance.log_action('pretix.giftcards.created', user=self.request.user, data={})

View File

@@ -4,7 +4,7 @@
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Order" %}</th>
<th>{% trans "Information" %}</th>
<th class="text-right">{% trans "Value" %}</th>
</tr>
</thead>
@@ -13,11 +13,7 @@
<tr>
<td>{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>
{% if t.order %}
{{ t.order.full_code }}
{% else %}
<em>{% if t.text %}{{ t.text }}{% else %}{% trans "Manual transaction" %}{% endif %}</em>
{% endif %}
{{ t.display_presale }}
</td>
<td class="text-right">
{{ t.value|money:giftcard.currency }}