forked from CGM_Public/pretix_original
GiftCard: Add more information to transactions (#3308)
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
24
src/pretix/base/migrations/0239_giftcard_info.py
Normal file
24
src/pretix/base/migrations/0239_giftcard_info.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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``
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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={})
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user