mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Add new gift card to orderposition relationship (#3291)
This commit is contained in:
@@ -201,6 +201,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('DELETE', 'api-v1:cartposition-detail'),
|
||||
('GET', 'api-v1:giftcard-list'),
|
||||
('POST', 'api-v1:giftcard-transact'),
|
||||
('PATCH', 'api-v1:giftcard-detail'),
|
||||
('GET', 'plugins:pretix_posbackend:posclosing-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posclosing-list'),
|
||||
|
||||
@@ -19,3 +19,30 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class AsymmetricField(serializers.Field):
|
||||
def __init__(self, read, write, **kwargs):
|
||||
self.read = read
|
||||
self.write = write
|
||||
super().__init__(
|
||||
required=self.write.required,
|
||||
default=self.write.default,
|
||||
initial=self.write.initial,
|
||||
source=self.write.source if self.write.source != self.write.field_name else None,
|
||||
label=self.write.label,
|
||||
allow_null=self.write.allow_null,
|
||||
error_messages=self.write.error_messages,
|
||||
validators=self.write.validators,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return self.write.to_internal_value(data)
|
||||
|
||||
def to_representation(self, value):
|
||||
return self.read.to_representation(value)
|
||||
|
||||
def run_validation(self, data=serializers.empty):
|
||||
return self.write.run_validation(data)
|
||||
|
||||
@@ -64,7 +64,9 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
||||
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True)
|
||||
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
|
||||
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
||||
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
|
||||
else:
|
||||
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
|
||||
required=False,
|
||||
|
||||
@@ -22,12 +22,14 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers import AsymmetricField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
@@ -35,8 +37,8 @@ from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||
User,
|
||||
MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan,
|
||||
Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -127,8 +129,51 @@ class MembershipSerializer(I18nAwareModelSerializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
|
||||
def to_internal_value(self, data):
|
||||
queryset = self.get_queryset()
|
||||
|
||||
if isinstance(data, int):
|
||||
try:
|
||||
return queryset.get(pk=data)
|
||||
except ObjectDoesNotExist:
|
||||
self.fail('does_not_exist', pk_value=data)
|
||||
|
||||
elif isinstance(data, str):
|
||||
try:
|
||||
return queryset.get(
|
||||
Q(secret=data)
|
||||
| Q(pseudonymization_id=data)
|
||||
| Q(pk__in=ReusableMedium.objects.filter(
|
||||
organizer=self.context['organizer'],
|
||||
type='barcode',
|
||||
identifier=data
|
||||
))
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
self.fail('does_not_exist', pk_value=data)
|
||||
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
|
||||
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())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['owner_ticket'].queryset = OrderPosition.objects.filter(order__event__organizer=self.context['organizer'])
|
||||
|
||||
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
||||
from pretix.api.serializers.media import (
|
||||
NestedOrderPositionSerializer,
|
||||
)
|
||||
|
||||
self.fields['owner_ticket'] = AsymmetricField(
|
||||
NestedOrderPositionSerializer(read_only=True, context=self.context),
|
||||
self.fields['owner_ticket'],
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -151,7 +196,7 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
|
||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket')
|
||||
|
||||
|
||||
class OrderEventSlugField(serializers.RelatedField):
|
||||
|
||||
@@ -179,18 +179,32 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
if 'include_accepted' in self.request.GET:
|
||||
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
|
||||
GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
||||
old_value = serializer.instance.value
|
||||
value = serializer.validated_data.pop('value')
|
||||
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
|
||||
testmode=serializer.instance.testmode)
|
||||
diff = value - old_value
|
||||
inst.transactions.create(value=diff)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': diff}
|
||||
)
|
||||
|
||||
value = serializer.validated_data.pop('value', None)
|
||||
|
||||
if any(k != 'value' for k in self.request.data):
|
||||
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
|
||||
testmode=serializer.instance.testmode)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.modified',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
else:
|
||||
inst = serializer.instance
|
||||
|
||||
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.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': diff}
|
||||
)
|
||||
|
||||
return inst
|
||||
|
||||
@action(detail=True, methods=["POST"])
|
||||
@@ -214,7 +228,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
auth=self.request.auth,
|
||||
data={'value': value, 'text': text}
|
||||
)
|
||||
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
|
||||
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
raise MethodNotAllowed("Gift cards cannot be deleted.")
|
||||
|
||||
19
src/pretix/base/migrations/0238_giftcard_owner_ticket.py
Normal file
19
src/pretix/base/migrations/0238_giftcard_owner_ticket.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.18 on 2023-05-04 12:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0237_question_valid_string_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='giftcard',
|
||||
name='owner_ticket',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='owned_gift_cards', to='pretixbase.orderposition'),
|
||||
),
|
||||
]
|
||||
@@ -66,6 +66,13 @@ class GiftCard(LoggedModel):
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
owner_ticket = models.ForeignKey(
|
||||
'OrderPosition',
|
||||
related_name='owned_gift_cards',
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True,
|
||||
verbose_name=_('Owned by ticket holder')
|
||||
)
|
||||
issuance = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
|
||||
@@ -1338,6 +1338,7 @@ class GiftCardFilterForm(FilterForm):
|
||||
Q(secret__icontains=query)
|
||||
| Q(transactions__text__icontains=query)
|
||||
| Q(transactions__order__code__icontains=query)
|
||||
| Q(owner_ticket__order__code__icontains=query)
|
||||
)
|
||||
if fdata.get('testmode') == 'yes':
|
||||
qs = qs.filter(testmode=True)
|
||||
|
||||
@@ -186,6 +186,15 @@ class OrganizerUpdateForm(OrganizerForm):
|
||||
return instance
|
||||
|
||||
|
||||
class SafeOrderPositionChoiceField(forms.ModelChoiceField):
|
||||
def __init__(self, queryset, **kwargs):
|
||||
queryset = queryset.model.all.none()
|
||||
super().__init__(queryset, **kwargs)
|
||||
|
||||
def label_from_instance(self, op):
|
||||
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
|
||||
|
||||
|
||||
class EventMetaPropertyForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = EventMetaProperty
|
||||
@@ -650,23 +659,32 @@ class GiftCardCreateForm(forms.ModelForm):
|
||||
class GiftCardUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ['expires', 'conditions']
|
||||
fields = ['expires', 'conditions', 'owner_ticket']
|
||||
field_classes = {
|
||||
'expires': SplitDateTimeField
|
||||
'expires': SplitDateTimeField,
|
||||
'owner_ticket': SafeOrderPositionChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'expires': SplitDateTimePickerWidget,
|
||||
'conditions': forms.Textarea(attrs={"rows": 2})
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
organizer = self.instance.issuer
|
||||
|
||||
class SafeOrderPositionChoiceField(forms.ModelChoiceField):
|
||||
def __init__(self, queryset, **kwargs):
|
||||
queryset = queryset.model.all.none()
|
||||
super().__init__(queryset, **kwargs)
|
||||
|
||||
def label_from_instance(self, op):
|
||||
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
|
||||
self.fields['owner_ticket'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
|
||||
self.fields['owner_ticket'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
|
||||
'organizer': organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('Ticket')
|
||||
}
|
||||
)
|
||||
self.fields['owner_ticket'].widget.choices = self.fields['owner_ticket'].choices
|
||||
self.fields['owner_ticket'].required = False
|
||||
|
||||
|
||||
class ReusableMediumUpdateForm(forms.ModelForm):
|
||||
|
||||
@@ -631,6 +631,12 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% for gc in line.owned_gift_cards.all %}
|
||||
<div class="product-row-giftcard">
|
||||
<span class="fa fa-credit-card" aria-hidden="true"></span>
|
||||
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">{{ gc.secret }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for fee in items.fees %}
|
||||
<div class="row-fluid product-row {% if fee.canceled %}pos-canceled{% endif %}">
|
||||
|
||||
@@ -46,6 +46,14 @@
|
||||
{{ card.issued_in.order.full_code }}</a>-{{ card.issued_in.positionid }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if card.owner_ticket %}
|
||||
<dt>{% trans "Owned by ticket holder" %}</dt>
|
||||
<dd>
|
||||
<span class="fa fa-ticket fa-fw"></span>
|
||||
<a href="{% url "control:event.order" event=card.owner_ticket.order.event.slug organizer=request.organizer.slug code=card.owner_ticket.order.code %}">
|
||||
{{ card.owner_ticket.order.code }}</a>-{{ card.owner_ticket.positionid }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.expires layout="control" %}
|
||||
{% bootstrap_field form.owner_ticket layout="control" %}
|
||||
{% bootstrap_field form.conditions layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -367,7 +367,7 @@ class OrderDetail(OrderView):
|
||||
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type',
|
||||
'discount',
|
||||
).prefetch_related(
|
||||
'item__questions', 'issued_gift_cards', 'linked_media',
|
||||
'item__questions', 'issued_gift_cards', 'owned_gift_cards', 'linked_media',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
|
||||
Prefetch('all_checkins', queryset=Checkin.all.select_related('list').order_by('datetime')),
|
||||
).order_by('positionid')
|
||||
|
||||
@@ -210,7 +210,7 @@ def giftcard_select2(request, **kwargs):
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@organizer_permission_required(("can_manage_reusable_media"))
|
||||
@organizer_permission_required(("can_manage_reusable_media", "can_manage_gift_cards"))
|
||||
def ticket_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
try:
|
||||
|
||||
@@ -369,6 +369,20 @@
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% for gc in line.owned_gift_cards.all %}
|
||||
<div class="cart-row-giftcard">
|
||||
<span class="fa fa-credit-card" aria-hidden="true"></span>
|
||||
<strong>{% trans "Gift card" %}</strong>
|
||||
<code>{{ gc.secret }}</code>
|
||||
<strong>{% trans "Current value:" %}</strong>
|
||||
{{ gc.value|money:gc.currency }}
|
||||
<a href="{% if position_page and line.addon_to %}{% eventurl event "presale:event.order.position.giftcard" secret=line.addon_to.web_secret order=order.code pk=gc.pk position=line.addon_to.positionid %}{% elif position_page %}{% eventurl event "presale:event.order.position.giftcard" secret=line.web_secret order=order.code pk=gc.pk position=line.positionid %}{% else %}{% eventurl event "presale:event.order.giftcard" secret=order.secret order=order.code pk=gc.pk %}{% endif %}"
|
||||
class="btn btn-default btn-sm">
|
||||
<span class="fa fa-eye" aria-hidden="true"></span>
|
||||
{% trans "Details" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for fee in cart.fees %}
|
||||
<div class="row cart-row">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{% load i18n %}
|
||||
{% load money %}
|
||||
<table class="panel-body table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Order" %}</th>
|
||||
<th class="text-right">{% trans "Value" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in transactions %}
|
||||
<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 %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ t.value|money:giftcard.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load money %}
|
||||
{% load eventurl %}
|
||||
{% load l10n %}
|
||||
{% load rich_text %}
|
||||
{% block title %}{% trans "Gift card" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% blocktrans trimmed with code=giftcard.secret %}
|
||||
Gift card: {{ code }}
|
||||
{% endblocktrans %}
|
||||
<a href="{% eventurl request.event "presale:event.order" secret=order.secret order=order.code %}"
|
||||
class="btn btn-default btn-sm btn-link">
|
||||
<span class="fa fa-caret-left"></span>
|
||||
{% trans "Back" %}
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
<strong>{% trans "Current value:" %} {{ giftcard.value|money:giftcard.currency }}</strong>
|
||||
</p>
|
||||
<h3>{% trans "Transaction history" %}</h3>
|
||||
{% include "pretixpresale/event/fragment_giftcard_history.html" %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load money %}
|
||||
{% load eventurl %}
|
||||
{% load l10n %}
|
||||
{% load rich_text %}
|
||||
{% block title %}{% trans "Gift card" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% blocktrans trimmed with code=giftcard.secret %}
|
||||
Gift card: {{ code }}
|
||||
{% endblocktrans %}
|
||||
<a href="{% eventurl request.event "presale:event.order.position" secret=position.web_secret order=order.code position=position.positionid %}"
|
||||
class="btn btn-default btn-sm btn-link">
|
||||
<span class="fa fa-caret-left"></span>
|
||||
{% trans "Back" %}
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
<strong>{% trans "Current value:" %} {{ giftcard.value|money:giftcard.currency }}</strong>
|
||||
</p>
|
||||
<h3>{% trans "Transaction history" %}</h3>
|
||||
{% include "pretixpresale/event/fragment_giftcard_history.html" %}
|
||||
{% endblock %}
|
||||
@@ -140,6 +140,9 @@ event_patterns = [
|
||||
re_path(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<position>[0-9]+)/(?P<output>[^/]+)$',
|
||||
pretix.presale.views.order.OrderDownload.as_view(),
|
||||
name='event.order.download'),
|
||||
re_path(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/giftcard/(?P<pk>[0-9]+)/$',
|
||||
pretix.presale.views.order.OrderGiftCardDetails.as_view(),
|
||||
name='event.order.giftcard'),
|
||||
re_path(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/invoice/(?P<invoice>[0-9]+)$',
|
||||
pretix.presale.views.order.InvoiceDownload.as_view(),
|
||||
name='event.invoice.download'),
|
||||
@@ -150,6 +153,9 @@ event_patterns = [
|
||||
re_path(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<pid>[0-9]+)/(?P<output>[^/]+)$',
|
||||
pretix.presale.views.order.OrderPositionDownload.as_view(),
|
||||
name='event.order.position.download'),
|
||||
re_path(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/giftcard/(?P<pk>[0-9]+)/$',
|
||||
pretix.presale.views.order.OrderPositionGiftCardDetails.as_view(),
|
||||
name='event.order.position.giftcard'),
|
||||
re_path(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/change$',
|
||||
pretix.presale.views.order.OrderPositionChange.as_view(),
|
||||
name='event.order.position.change'),
|
||||
|
||||
@@ -57,7 +57,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.generic import TemplateView, View
|
||||
from django.views.generic import ListView, TemplateView, View
|
||||
|
||||
from pretix.base.models import (
|
||||
CachedTicket, Checkin, GiftCard, Invoice, Order, OrderPosition, Quota,
|
||||
@@ -241,7 +241,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
qs = self.order.positions.prefetch_related('issued_gift_cards').select_related('tax_rule')
|
||||
qs = self.order.positions.prefetch_related('issued_gift_cards', 'owned_gift_cards').select_related('tax_rule')
|
||||
if self.request.event.settings.show_checkin_number_user:
|
||||
qs = qs.annotate(
|
||||
checkin_count=Subquery(
|
||||
@@ -1128,6 +1128,53 @@ class OrderDownload(OrderDownloadMixin, EventViewMixin, OrderDetailMixin, AsyncA
|
||||
return None
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderGiftCardDetails(EventViewMixin, OrderDetailMixin, ListView):
|
||||
template_name = 'pretixpresale/event/order_giftcard.html'
|
||||
context_object_name = 'transactions'
|
||||
paginate_by = 50
|
||||
|
||||
@cached_property
|
||||
def giftcard(self):
|
||||
return GiftCard.objects.filter(
|
||||
owner_ticket__order_id=self.order.pk
|
||||
).get(pk=self.kwargs['pk'])
|
||||
|
||||
def get_queryset(self):
|
||||
return self.giftcard.transactions.order_by('-datetime', '-pk')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(
|
||||
order=self.order,
|
||||
giftcard=self.giftcard,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderPositionGiftCardDetails(EventViewMixin, OrderPositionDetailMixin, ListView):
|
||||
template_name = 'pretixpresale/event/position_giftcard.html'
|
||||
context_object_name = 'transactions'
|
||||
paginate_by = 50
|
||||
|
||||
@cached_property
|
||||
def giftcard(self):
|
||||
return GiftCard.objects.filter(
|
||||
Q(owner_ticket_id=self.position.pk) | Q(owner_ticket__addon_to_id=self.position.pk)
|
||||
).get(pk=self.kwargs['pk'])
|
||||
|
||||
def get_queryset(self):
|
||||
return self.giftcard.transactions.order_by('-datetime', '-pk')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(
|
||||
order=self.order,
|
||||
position=self.position,
|
||||
giftcard=self.giftcard,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderPositionDownload(OrderDownloadMixin, EventViewMixin, OrderPositionDetailMixin, AsyncAction, View):
|
||||
task = generate
|
||||
|
||||
@@ -101,6 +101,16 @@ nav.navbar .danger, nav.navbar .danger:hover, nav.navbar .danger:active {
|
||||
}
|
||||
}
|
||||
|
||||
.product-row-giftcard {
|
||||
border-radius: $border-radius-base;
|
||||
padding: 10px 1.4em;
|
||||
margin: 0 0 5px 1.4em;
|
||||
background: $panel-footer-bg;
|
||||
code {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
h1 .btn-sm {
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
@@ -106,6 +106,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cart-row-giftcard {
|
||||
border-radius: $border-radius-base;
|
||||
padding: 10px 1.4em;
|
||||
margin: 0 0 5px 1.4em;
|
||||
background: $panel-footer-bg;
|
||||
code {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.cart .firstchild-in-panel .cart-row:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import GiftCard, Organizer
|
||||
from pretix.base.models import GiftCard, Order, Organizer
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -50,7 +52,8 @@ TEST_GC_RES = {
|
||||
"testmode": False,
|
||||
"expires": None,
|
||||
"conditions": None,
|
||||
"currency": "EUR"
|
||||
"currency": "EUR",
|
||||
"owner_ticket": None
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +96,63 @@ def test_giftcard_detail(token_client, organizer, event, giftcard):
|
||||
assert res == resp.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_giftcard_detail_expand(token_client, organizer, event, giftcard):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
|
||||
total=14, locale='en'
|
||||
)
|
||||
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
|
||||
personalized=True)
|
||||
op = o.positions.create(item=ticket, price=Decimal("14"))
|
||||
giftcard.owner_ticket = op
|
||||
giftcard.save()
|
||||
|
||||
res = dict(TEST_GC_RES)
|
||||
res["id"] = giftcard.pk
|
||||
res["issuance"] = giftcard.issuance.isoformat().replace('+00:00', 'Z')
|
||||
resp = token_client.get('/api/v1/organizers/{}/giftcards/{}/?expand=owner_ticket'.format(organizer.slug, giftcard.pk))
|
||||
assert resp.status_code == 200
|
||||
|
||||
assert resp.data["owner_ticket"] == {
|
||||
"id": op.pk,
|
||||
"order": {"code": "FOO", "event": "dummy"},
|
||||
"positionid": op.positionid,
|
||||
"item": ticket.pk,
|
||||
"variation": None,
|
||||
"price": "14.00",
|
||||
"attendee_name": None,
|
||||
"attendee_name_parts": {},
|
||||
"company": None,
|
||||
"street": None,
|
||||
"zipcode": None,
|
||||
"city": None,
|
||||
"country": None,
|
||||
"state": None,
|
||||
"discount": None,
|
||||
"attendee_email": None,
|
||||
"voucher": None,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
"secret": op.secret,
|
||||
"addon_to": None,
|
||||
"subevent": None,
|
||||
"checkins": [],
|
||||
"downloads": [],
|
||||
"answers": [],
|
||||
"tax_rule": None,
|
||||
"pseudonymization_id": op.pseudonymization_id,
|
||||
"pdf_data": {},
|
||||
"seat": None,
|
||||
"canceled": False,
|
||||
"valid_from": None,
|
||||
"valid_until": None,
|
||||
"blocked": None
|
||||
}
|
||||
|
||||
|
||||
TEST_GIFTCARD_CREATE_PAYLOAD = {
|
||||
"secret": "DEFABC",
|
||||
"value": "12.00",
|
||||
@@ -129,34 +189,51 @@ def test_giftcard_duplicate_secert(token_client, organizer, event, giftcard):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_giftcard_patch(token_client, organizer, event, giftcard):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/giftcards/{}/'.format(organizer.slug, giftcard.pk),
|
||||
{
|
||||
'secret': 'foo',
|
||||
'value': '10.00',
|
||||
'testmode': True,
|
||||
'currency': 'USD'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
giftcard.refresh_from_db()
|
||||
assert giftcard.value == Decimal('10.00')
|
||||
assert giftcard.secret == "ABCDEF"
|
||||
assert giftcard.currency == "EUR"
|
||||
assert not giftcard.testmode
|
||||
def test_giftcard_patch_owner_by_id(token_client, organizer, event, giftcard):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
|
||||
total=14, locale='en'
|
||||
)
|
||||
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
|
||||
personalized=True)
|
||||
op = o.positions.create(item=ticket, price=Decimal("14"))
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/giftcards/{}/'.format(organizer.slug, giftcard.pk),
|
||||
{
|
||||
'value': '9.00',
|
||||
'owner_ticket': op.pk,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
giftcard.refresh_from_db()
|
||||
assert giftcard.value == Decimal('9.00')
|
||||
assert giftcard.owner_ticket == op
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_giftcard_patch_owner_by_secret(token_client, organizer, event, giftcard):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
|
||||
total=14, locale='en'
|
||||
)
|
||||
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
|
||||
personalized=True)
|
||||
op = o.positions.create(item=ticket, price=Decimal("14"))
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/giftcards/{}/'.format(organizer.slug, giftcard.pk),
|
||||
{
|
||||
'owner_ticket': op.secret,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
giftcard.refresh_from_db()
|
||||
assert giftcard.owner_ticket == op
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -121,9 +121,14 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
|
||||
medium.linked_giftcard = giftcard
|
||||
medium.customer = customer
|
||||
medium.save()
|
||||
giftcard.owner_ticket = op
|
||||
giftcard.save()
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/reusablemedia/{}/?expand=linked_giftcard&expand=linked_orderposition&expand=customer'.format(
|
||||
organizer.slug, medium.pk))
|
||||
'/api/v1/organizers/{}/reusablemedia/{}/?expand=linked_giftcard&expand='
|
||||
'linked_giftcard.owner_ticket&expand=linked_orderposition&expand=customer'.format(
|
||||
organizer.slug, medium.pk
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
assert resp.data["customer"] == {
|
||||
@@ -183,7 +188,8 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
|
||||
"currency": "EUR",
|
||||
"testmode": False,
|
||||
"expires": None,
|
||||
"conditions": None
|
||||
"conditions": None,
|
||||
"owner_ticket": resp.data["linked_orderposition"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user