Compare commits

..

1 Commits

Author SHA1 Message Date
Richard Schreiber
f44765cc88 Fix log-display for team.invite.deleted 2026-03-13 09:53:30 +01:00
19 changed files with 445 additions and 1135 deletions

View File

@@ -117,8 +117,6 @@ cancellation_date datetime Time of order c
reliable for orders that have been cancelled,
reactivated and cancelled again.
plugin_data object Additional data added by plugins.
use_gift_cards list of strings List of unique gift card secrets that are used to pay
for this order.
===================================== ========================== =======================================================
@@ -158,10 +156,6 @@ use_gift_cards list of strings List of unique
The ``tax_rounding_mode`` attribute has been added.
.. versionchanged:: 2026.03
The ``use_gift_cards`` attribute has been added.
.. _order-position-resource:
Order position resource
@@ -993,6 +987,8 @@ Creating orders
* does not support file upload questions
* does not support redeeming gift cards
* does not support or validate memberships
@@ -1099,14 +1095,6 @@ Creating orders
whether these emails are enabled for certain sales channels. If set to ``null``, behavior will be controlled by pretix'
settings based on the sales channels (added in pretix 4.7). Defaults to ``false``.
Used to be ``send_mail`` before pretix 3.14.
* ``use_gift_cards`` (optional) The provided gift cards will be used to pay for this order. They will be debited and
all the necessary payment records for these transactions will be created. The gift cards will be used in sequence to
pay for the order. Processing of the gift cards stops as soon as the order is payed for. All gift card transactions
are listed under ``payments`` in the response.
This option can only be used with orders that are in the pending state.
The ``use_gift_cards`` attribute can not be combined with ``payment_info`` and ``payment_provider`` fields. If the
order isn't completely paid after its creation with ``use_gift_cards``, then a subsequent request to the payment
endpoint is needed.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
to incrementing integers starting with ``1``. Then, you can reference one of these

View File

@@ -73,7 +73,7 @@ dependencies = [
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.12.*",
"PyJWT==2.11.*",
"phonenumberslite==9.0.*",
"Pillow==12.1.*",
"pretix-plugin-build",

View File

@@ -53,7 +53,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.invoicing.transmission import get_transmission_types
from pretix.base.models import (
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
CachedFile, Checkin, Customer, Device, Invoice, InvoiceAddress,
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
Voucher,
@@ -62,7 +62,6 @@ from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
PrintLog, RevokedTicketSecret, Transaction,
)
from pretix.base.payment import GiftCardPayment, PaymentException
from pretix.base.pdf import get_images, get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
@@ -1201,7 +1200,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
)
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
use_gift_cards = serializers.ListField(child=serializers.CharField(required=False), required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1217,7 +1215,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode', 'use_gift_cards')
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode')
def validate_payment_provider(self, pp):
if pp is None:
@@ -1312,14 +1310,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False)
simulate = validated_data.pop('simulate', False)
gift_card_secrets = validated_data.pop('use_gift_cards') if 'use_gift_cards' in validated_data else []
if (payment_provider is not None or payment_info != '{}') and len(gift_card_secrets) > 0:
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is not compatible with payment_provider or payment_info']})
if validated_data.get('status') != Order.STATUS_PENDING and len(gift_card_secrets) > 0:
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is only supported for orders that are created as pending']})
if len(set(gift_card_secrets)) != len(gift_card_secrets):
raise ValidationError({"use_gift_cards": ['Multiple copies of the same gift card secret are not allowed']})
if not validated_data.get("sales_channel"):
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
@@ -1804,45 +1794,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if order.total != Decimal('0.00') and order.event.currency == "XXX":
raise ValidationError('Paid products not supported without a valid currency.')
for gift_card_secret in gift_card_secrets:
try:
if order.status != Order.STATUS_PAID:
gift_card_payment_provider = GiftCardPayment(event=order.event)
gc = order.event.organizer.accepted_gift_cards.get(
secret=gift_card_secret
)
payment = order.payments.create(
amount=min(order.pending_sum, gc.value),
provider=gift_card_payment_provider.identifier,
info_data={
'gift_card': gc.pk,
'gift_card_secret': gc.secret,
'retry': True
},
state=OrderPayment.PAYMENT_STATE_CREATED
)
gift_card_payment_provider.execute_payment(request=None, payment=payment, is_early_special_case=True)
if order.pending_sum <= Decimal('0.00'):
order.status = Order.STATUS_PAID
except PaymentException:
pass
except GiftCard.DoesNotExist as e:
payment = order.payments.create(
amount=order.pending_sum,
provider=GiftCardPayment.identifier,
info_data={
'gift_card_secret': gift_card_secret,
},
state=OrderPayment.PAYMENT_STATE_CREATED
)
payment.fail(info={**payment.info_data, 'error': str(e)},
send_mail=False)
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
order.status = Order.STATUS_PAID
order.save()

View File

@@ -381,15 +381,12 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
return FileResponse(
ct.file.file,
filename='{}-{}-{}{}'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
),
as_attachment=True,
content_type=ct.type
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
)
return resp
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
@@ -1304,17 +1301,14 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
raise NotFound()
ftype, ignored = mimetypes.guess_type(answer.file.name)
return FileResponse(
answer.file,
filename='{}-{}-{}-{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
),
as_attachment=True,
content_type=ftype or 'application/binary'
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
)
return resp
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
def printlog(self, request, **kwargs):
@@ -1369,18 +1363,15 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
if hasattr(image_file, 'seek'):
image_file.seek(0)
return FileResponse(
image_file,
filename='{}-{}-{}-{}.{}'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
),
as_attachment=True,
content_type=ftype or 'application/binary'
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
)
return resp
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
@@ -1406,15 +1397,12 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
return FileResponse(
ct.file.file,
filename='{}-{}-{}-{}{}'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
),
as_attachment=True,
content_type=ct.type
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
)
return resp
@action(detail=True, methods=['POST'])
def regenerate_secrets(self, request, **kwargs):
@@ -1991,12 +1979,9 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
if not invoice.file:
raise RetryException()
return FileResponse(
invoice.file.file,
filename='{}.pdf"'.format(invoice.number),
as_attachment=True,
content_type='application/pdf'
)
resp = FileResponse(invoice.file.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
return resp
@action(detail=True, methods=['POST'])
def transmit(self, request, **kwargs):

View File

@@ -1526,26 +1526,16 @@ class GiftCardPayment(BasePaymentProvider):
def payment_control_render(self, request, payment) -> str:
from .models import GiftCard
if any(key in payment.info_data for key in ('gift_card', 'error')):
if 'gift_card' in payment.info_data:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
template = get_template('pretixcontrol/giftcards/payment.html')
ctx = {
'request': request,
'event': self.event,
**({'error': payment.info_data[
'error']} if 'error' in payment.info_data else {}),
**({'gift_card_secret': payment.info_data[
'gift_card_secret']} if 'gift_card_secret' in payment.info_data else {})
'gc': gc,
}
try:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
ctx = {
'gc': gc,
}
except GiftCard.DoesNotExist:
pass
finally:
return template.render(ctx)
return template.render(ctx)
def payment_control_render_short(self, payment: OrderPayment) -> str:
d = payment.info_data
@@ -1560,16 +1550,12 @@ class GiftCardPayment(BasePaymentProvider):
try:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
except GiftCard.DoesNotExist:
return {
**({'error': payment.info_data[
'error']} if 'error' in payment.info_data else {})
}
return {}
return {
'gift_card': {
'id': gc.pk,
'secret': gc.secret,
'organizer': gc.issuer.slug,
** ({'error': payment.info_data['error']} if 'error' in payment.info_data else {})
'organizer': gc.issuer.slug
}
}
@@ -1641,8 +1627,6 @@ class GiftCardPayment(BasePaymentProvider):
raise PaymentException(_("This gift card does not support this currency."))
if not gc.accepted_by(self.event.organizer):
raise PaymentException(_("This gift card is not accepted by this event organizer."))
if gc.value <= Decimal("0.00"):
raise PaymentException(_("All credit on this gift card has been used."))
if payment.amount > gc.value:
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
if gc.testmode and not payment.order.testmode:
@@ -1672,7 +1656,7 @@ class GiftCardPayment(BasePaymentProvider):
}
)
except PaymentException as e:
payment.fail(info={**payment.info_data, 'error': str(e)}, send_mail=not is_early_special_case)
payment.fail(info={'error': str(e)})
raise e
def payment_is_valid_session(self, request: HttpRequest) -> bool:

View File

@@ -3,26 +3,16 @@
<dl class="dl-horizontal">
<dt>{% trans "Gift card code" %}</dt>
<dd>
{% if gc %}
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
{{ gc.secret }}
</a>
{% if gc.issuer.slug != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ gc.issuer }}
</span>
{% endif %}
{% elif gift_card_secret %}
{{ gift_card_secret }}
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
{{ gc.secret }}
</a>
{% if gc.issuer != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ gc.issuer }}
</span>
{% endif %}
</dd>
{% if gc %}
<dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd>
{% endif %}
{% if error %}
<dt>{% trans "Error" %}</dt>
<dd>{{ error }}</dd>
{% endif %}
<dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd>
</dl>

View File

@@ -743,7 +743,12 @@ class InvoicePreview(EventPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
fname, ftype, fcontent = build_preview_invoice_pdf(request.event)
resp = HttpResponse(fcontent, content_type=ftype)
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
if settings.DEBUG:
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
resp._csp_ignore = True
else:
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(fname)
return resp

View File

@@ -300,4 +300,5 @@ class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data)
resp['Content-Type'] = mime
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
resp._csp_ignore = True
return resp

View File

@@ -710,26 +710,22 @@ class OrderDownload(AsyncAction, OrderView):
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
return FileResponse(
value.file.file,
filename='{}-{}-{}-{}{}'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
),
content_type=value.type
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
)
return resp
elif isinstance(value, CachedCombinedTicket):
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
return FileResponse(
value.file.file,
filename='{}-{}-{}{}'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
),
content_type=value.type
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
)
return resp
else:
return redirect(self.get_self_url())
@@ -1835,15 +1831,15 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
return redirect(self.get_order_url())
try:
return FileResponse(
self.invoice.file.file,
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number)),
content_type='application/pdf'
)
resp = FileResponse(self.invoice.file.file, content_type='application/pdf')
except FileNotFoundError:
invoice_pdf_task.apply(args=(self.invoice.pk,))
return self.get(request, *args, **kwargs)
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number))
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
return resp
class OrderExtend(OrderView):
permission = 'can_change_orders'

View File

@@ -263,7 +263,12 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
if settings.DEBUG:
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
resp._csp_ignore = True
else:
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
return resp
elif "data" in request.POST:
if cf:
@@ -304,5 +309,6 @@ class FontsCSSView(TemplateView):
class PdfView(TemplateView):
def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
resp = FileResponse(cf.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
return resp

View File

@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-14 22:00+0000\n"
"PO-Revision-Date: 2026-03-03 20:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -27734,7 +27734,7 @@ msgstr "Toon accountgeschiedenis"
#: pretix/control/templates/pretixcontrol/user/staff_session_edit.html:4
msgid "Staff session"
msgstr "Beheerderssessie"
msgstr "Personeelssessie"
#: pretix/control/templates/pretixcontrol/user/staff_session_edit.html:6
msgid "Session notes"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-14 22:00+0000\n"
"PO-Revision-Date: 2026-03-02 10:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_Informal/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -27796,7 +27796,7 @@ msgstr "Toon accountgeschiedenis"
#: pretix/control/templates/pretixcontrol/user/staff_session_edit.html:4
msgid "Staff session"
msgstr "Beheerderssessie"
msgstr "Personeelssessie"
#: pretix/control/templates/pretixcontrol/user/staff_session_edit.html:6
msgid "Session notes"

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-11 00:00+0000\n"
"Last-Translator: Demir Kaya <demir@indieturk.org>\n"
"PO-Revision-Date: 2025-01-04 01:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Turkish <https://translate.pretix.eu/projects/pretix/pretix/"
"tr/>\n"
"Language: tr\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.9.2\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -37,7 +37,7 @@ msgstr "Arapça"
#: pretix/_base_settings.py:91
msgid "Basque"
msgstr "Baskça"
msgstr ""
#: pretix/_base_settings.py:92
msgid "Catalan"
@@ -57,7 +57,7 @@ msgstr "Çekce"
#: pretix/_base_settings.py:96
msgid "Croatian"
msgstr "Hırvatça"
msgstr ""
#: pretix/_base_settings.py:97
msgid "Danish"
@@ -89,7 +89,7 @@ msgstr "Yunanca"
#: pretix/_base_settings.py:104
msgid "Hebrew"
msgstr "İbranice"
msgstr ""
#: pretix/_base_settings.py:105
msgid "Indonesian"
@@ -101,7 +101,7 @@ msgstr "İtalyanca"
#: pretix/_base_settings.py:107
msgid "Japanese"
msgstr "Japonca"
msgstr ""
#: pretix/_base_settings.py:108
msgid "Latvian"
@@ -109,7 +109,7 @@ msgstr "Letonca"
#: pretix/_base_settings.py:109
msgid "Norwegian Bokmål"
msgstr "Norveççe"
msgstr ""
#: pretix/_base_settings.py:110
msgid "Polish"
@@ -145,7 +145,7 @@ msgstr "İspanyolca"
#: pretix/_base_settings.py:118
msgid "Spanish (Latin America)"
msgstr "İspanyolca (Latin Amerikan)"
msgstr ""
#: pretix/_base_settings.py:119
msgid "Turkish"
@@ -203,8 +203,10 @@ msgid "Client secret"
msgstr "Müşteri sırrı"
#: pretix/api/models.py:116
#, fuzzy
#| msgid "Enable"
msgid "Enable webhook"
msgstr "Webhook etkinleştir"
msgstr "Etkinleştirme"
#: pretix/api/models.py:117
#: pretix/control/templates/pretixcontrol/organizers/webhooks.html:36
@@ -275,9 +277,10 @@ msgid "Unknown plugin: '{name}'."
msgstr "Bilinmeyen eklenti: '{name}'."
#: pretix/api/serializers/event.py:286 pretix/api/serializers/organizer.py:88
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid "Unknown plugin: '{name}'."
msgid "Restricted plugin: '{name}'."
msgstr "sınırlandırılmış eklenti: '{name}'."
msgstr "Bilinmeyen eklenti: '{name}'."
#: pretix/api/serializers/item.py:87 pretix/api/serializers/item.py:149
#: pretix/api/serializers/item.py:405
@@ -609,8 +612,10 @@ msgid ""
msgstr ""
#: pretix/api/webhooks.py:413
#, fuzzy
#| msgid "Shop not live"
msgid "Shop taken live"
msgstr "Mağaza çevrimdışı"
msgstr "Mağaza kapalı"
#: pretix/api/webhooks.py:417
#, fuzzy
@@ -619,12 +624,16 @@ msgid "Shop taken offline"
msgstr "Mağaza çevrimdışı duruma getirildi."
#: pretix/api/webhooks.py:421
#, fuzzy
#| msgid "The order has been created."
msgid "Test-Mode of shop has been activated"
msgstr "Mağazanın Test-Modu aktive oldu"
msgstr "Sipariş oluşturuldu."
#: pretix/api/webhooks.py:425
#, fuzzy
#| msgid "The order has been created."
msgid "Test-Mode of shop has been deactivated"
msgstr "Mağazanın Test-Modu deaktif oldu"
msgstr "Sipariş oluşturuldu."
#: pretix/api/webhooks.py:429
#, fuzzy
@@ -651,8 +660,10 @@ msgid "Waiting list entry received voucher"
msgstr "Liste girdileri bekleniyor"
#: pretix/api/webhooks.py:445
#, fuzzy
#| msgid "Voucher code"
msgid "Voucher added"
msgstr "Kupon kodu eklendi"
msgstr "Kupon kodu"
#: pretix/api/webhooks.py:449
#, fuzzy

View File

@@ -841,13 +841,9 @@ class AnswerDownload(EventViewMixin, View):
return Http404()
ftype, _ = mimetypes.guess_type(answer.file.name)
filename = '{}-cart-{}'.format(
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-cart-{}"'.format(
self.request.event.slug.upper(),
os.path.basename(answer.file.name).split('.', 1)[1]
).encode("ascii", "ignore")
resp = FileResponse(
answer.file,
filename=filename,
content_type=ftype or 'application/binary'
)
return resp

View File

@@ -1220,25 +1220,30 @@ class OrderDownloadMixin:
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
name_parts = (
self.request.event.slug.upper(),
self.order.code,
str(self.order_position.positionid),
self.order_position.subevent.date_from.strftime('%Y_%m_%d') if self.order_position.subevent else None,
self.output.identifier
)
filename = "-".join(filter(None, name_parts)) + value.extension
return FileResponse(value.file.file, filename=filename, content_type=value.type)
resp = FileResponse(value.file.file, content_type=value.type)
if self.order_position.subevent:
# Subevent date in filename improves accessibility e.g. for screen reader users
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.order_position.subevent.date_from.strftime('%Y_%m_%d'),
self.output.identifier, value.extension
)
else:
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
)
return resp
elif isinstance(value, CachedCombinedTicket):
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
return FileResponse(
value.file.file,
filename="-".join(self.request.event.slug.upper(), self.order.code, self.output.identifier) + value.extension,
content_type=value.type
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
)
return resp
else:
return redirect(self.get_self_url())
@@ -1378,14 +1383,13 @@ class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
return redirect(self.get_order_url())
try:
return FileResponse(
invoice.file.file,
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", invoice.number)),
content_type='application/pdf'
)
resp = FileResponse(invoice.file.file, content_type='application/pdf')
except FileNotFoundError:
invoice_pdf_task.apply(args=(invoice.pk,))
return self.get(request, *args, **kwargs)
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", invoice.number))
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
return resp
class OrderChangeMixin:

View File

@@ -174,7 +174,7 @@ $(function () {
const fill_peppol_id = function () {
const vatId = dependents.vat_id.val();
if (vatId && vatId.startsWith("BE") && dependents.transmission_type.val() === "peppol") {
dependents.transmission_peppol_participant_id.val("0208:" + vatId.substring(2).replaceAll(".", ""))
dependents.transmission_peppol_participant_id.val("0208:" + vatId.substring(2))
}
}
dependents.vat_id.add(dependents.transmission_type).on("change", fill_peppol_id);

View File

@@ -35,8 +35,8 @@ from django_scopes import scopes_disabled
from tests.const import SAMPLE_PNG
from pretix.base.models import (
GiftCard, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
Organizer, Question, SeatingPlan,
InvoiceAddress, Item, Order, OrderPosition, Organizer, Question,
SeatingPlan,
)
from pretix.base.models.orders import CartPosition, OrderFee, QuestionAnswer
@@ -3371,251 +3371,3 @@ def test_order_create_rounding_default_pretixpos_fallback(device, device_client,
assert resp.data["total"] == "500.00"
assert resp.data["positions"][0]["price"] == "100.00"
assert resp.data["positions"][-1]["price"] == "100.00"
@pytest.mark.parametrize(
"order_status,status_code",
[
(
Order.STATUS_PENDING, 201
),
(
Order.STATUS_PAID, 400
),
],
)
@pytest.mark.django_db
def test_order_create_use_gift_cards_only_pending(token_client, organizer, event, item, quota, question, order_status, status_code):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
with scopes_disabled():
customer = organizer.customers.create()
res['customer'] = customer.identifier
res['api_meta'] = {
'test': 1
}
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
res['status'] = order_status
del res['payment_provider']
res['use_gift_cards'] = [gc.secret]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == status_code
if status_code != 201:
assert resp.data == {'use_gift_cards': ['The attribute use_gift_cards is only supported for orders that are created as pending']}
@pytest.mark.django_db
@pytest.mark.parametrize(
"send_mail,mail_amount",
[
(
False, 0
),
(
True, 2
),
],
)
def test_order_create_use_gift_card(token_client, organizer, event, item, quota, question, send_mail, mail_amount):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
with scopes_disabled():
customer = organizer.customers.create()
res['customer'] = customer.identifier
res['api_meta'] = {
'test': 1
}
if send_mail:
res['send_email'] = True
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
del res['payment_provider']
res['use_gift_cards'] = [gc.secret]
djmail.outbox = []
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
assert o.status == Order.STATUS_PAID
assert gc.transactions.count() == 2
assert -gc.transactions.last().value == o.total
assert len(djmail.outbox) == mail_amount
@pytest.mark.django_db
def test_order_create_use_multiple_gift_cards(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
with scopes_disabled():
customer = organizer.customers.create()
res['customer'] = customer.identifier
res['api_meta'] = {
'test': 1
}
del res['payment_provider']
gc_one_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_one_eur.transactions.create(value=Decimal("1.00"), acceptor=organizer).save()
gc_empty = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_wrong_currency = GiftCard.objects.create(issuer=organizer, currency='USD')
gc_wrong_currency.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_enough_eur.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
res['use_gift_cards'] = [gc_one_eur.secret, gc_empty.secret, gc_wrong_currency.secret, gc_enough_eur.secret]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
# order has a payment entry per giftcard
assert o.status == Order.STATUS_PAID
assert o.payments.count() == 4
assert gc_one_eur.transactions.count() == 2 # +1€ charge and -1€ payment
assert o.payments.all()[0].state == OrderPayment.PAYMENT_STATE_CONFIRMED
assert Decimal(-1.00) == gc_one_eur.transactions.last().value
assert gc_empty.transactions.count() == 0 # no charge and no payment transaction
assert o.payments.all()[1].state == OrderPayment.PAYMENT_STATE_FAILED
assert gc_wrong_currency.transactions.count() == 1 # charge transaction
assert o.payments.all()[2].state == OrderPayment.PAYMENT_STATE_FAILED
assert gc_enough_eur.transactions.count() == 2 # +100€ charge and -remainder € payment
assert o.payments.all()[3].state == OrderPayment.PAYMENT_STATE_CONFIRMED
assert -(o.total - Decimal(1.00)) == gc_enough_eur.transactions.last().value
@pytest.mark.django_db
def test_order_create_use_gift_card_exclusive_with_payment_provider(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
with scopes_disabled():
customer = organizer.customers.create()
res['customer'] = customer.identifier
res['api_meta'] = {
'test': 1
}
gc_value = Decimal("1.00")
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc.transactions.create(value=gc_value, acceptor=organizer).save()
res['use_gift_cards'] = [gc.secret]
res_with_payment_provider = copy.deepcopy(res)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res_with_payment_provider
)
assert resp.status_code == 400
assert resp.json() == {"use_gift_cards": ["The attribute use_gift_cards is not compatible with payment_provider or payment_info"]}
res_with_payment_info = copy.deepcopy(res)
res_with_payment_info['payment_info'] = {"a": "b"}
del res_with_payment_info['payment_provider']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res_with_payment_info
)
assert resp.status_code == 400
assert resp.json() == {"use_gift_cards": ["The attribute use_gift_cards is not compatible with payment_provider or payment_info"]}
@pytest.mark.django_db
def test_order_create_use_gift_card_repeated(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
with scopes_disabled():
customer = organizer.customers.create()
res['customer'] = customer.identifier
res['api_meta'] = {
'test': 1
}
del res['payment_provider']
gc_one_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_one_eur.transactions.create(value=Decimal("1.00"), acceptor=organizer).save()
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_enough_eur.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
res['use_gift_cards'] = [gc_one_eur.secret, gc_one_eur.secret, gc_enough_eur.secret]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.json() == {'use_gift_cards': ['Multiple copies of the same gift card secret are not allowed']}
@pytest.mark.django_db
def test_order_create_use_gift_card_invalid_secret(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
with scopes_disabled():
customer = organizer.customers.create()
res['customer'] = customer.identifier
res['api_meta'] = {
'test': 1
}
del res['payment_provider']
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_enough_eur.transactions.create(value=Decimal("100.00"),
acceptor=organizer).save()
res['use_gift_cards'] = ["INVALID", gc_enough_eur.secret]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
assert o.status == Order.STATUS_PAID
assert o.payments.count() == 2
assert o.payments.all()[0].state == OrderPayment.PAYMENT_STATE_FAILED
assert o.payments.all()[1].state == OrderPayment.PAYMENT_STATE_CONFIRMED