Compare commits

..

13 Commits

Author SHA1 Message Date
Mira Weller
1263f4359f Remove unused percentage result field 2024-10-15 11:46:10 +02:00
Raphael Michel
645df63e66 Fix failing test 2024-10-14 17:35:57 +02:00
Raphael Michel
3416366763 Tests and docs 2024-10-14 15:37:27 +02:00
Raphael Michel
bbefb59036 Add ticket renderer RPC API 2024-10-11 18:58:55 +02:00
Raphael Michel
8f0a277c7b Fix tax rule calculation of negative amounts (PRETIXEU-ANN) 2024-10-11 15:28:07 +02:00
George Hickman
9dc38e42d8 Add device_changed signal (#4412)
This provides both the original and updated version of the Device so
subscribers can see the changes.
2024-10-11 11:08:23 +02:00
Raphael Michel
bfd88d1496 Docs: Fix wrong field name in example 2024-10-10 13:51:33 +02:00
Patrick Chilton
be6bd501bd Translations: Update Hungarian
Currently translated at 10.5% (608 of 5745 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/hu/

powered by weblate
2024-10-10 09:16:51 +02:00
Raphael Michel
d160c9fd67 Fix crash in checkin list action view (PRETIXEU-AN8) 2024-10-09 17:11:10 +02:00
Raphael Michel
221f14cc21 API: Fix crash PRETIXEU-AN5 2024-10-09 12:25:36 +02:00
Felix Schäfer
1dda2eb4fb Fix reauth loops with redirect style authentication plugins (#4512)
* Test reauth with redirect style auth #4498

* Fix reauth loops with redirect style auth #4498
2024-10-09 09:24:49 +02:00
Davide Manzella
30f2e99020 Translations: Update Italian
Currently translated at 22.2% (1279 of 5745 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/it/

powered by weblate
2024-10-09 09:16:52 +02:00
Raphael Michel
8efe276ed0 Fix negative prices in bundles when tax rate is 0 (#4513) 2024-10-09 08:16:01 +02:00
18 changed files with 692 additions and 159 deletions

View File

@@ -51,7 +51,7 @@ Endpoints
"results": [
{
"identifier": "web",
"name": {
"label": {
"en": "Online shop"
},
"type": "web",
@@ -88,7 +88,7 @@ Endpoints
{
"identifier": "web",
"name": {
"label": {
"en": "Online shop"
},
"type": "web",
@@ -116,7 +116,7 @@ Endpoints
{
"identifier": "api.custom",
"name": {
"label": {
"en": "Custom integration"
},
"type": "api",
@@ -133,7 +133,7 @@ Endpoints
{
"identifier": "api.custom",
"name": {
"label": {
"en": "Custom integration"
},
"type": "api",
@@ -178,7 +178,7 @@ Endpoints
{
"identifier": "web",
"name": {
"label": {
"en": "Online shop"
},
"type": "web",

View File

@@ -14,7 +14,7 @@ Core
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display,
register_text_placeholders, register_mail_placeholders
register_text_placeholders, register_mail_placeholders, device_info_updated
Order events
""""""""""""

View File

@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
===================================== ========================== =======================================================
Endpoints
---------
Layout endpoints
----------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
@@ -268,5 +268,75 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
Ticket rendering endpoint
-----------------------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketpdfrenderer/render_batch/
With this API call, you can instruct the system to render a set of tickets into one combined PDF file. To specify
which tickets to render, you need to submit a list of "parts". For every part, the following fields are supported:
* ``orderposition`` (``integer``, required): The ID of the order position to render.
* ``override_channel`` (``string``, optional): The sales channel ID to be used for layout selection instead of the
original channel of the order.
* ``override_layout`` (``integer``, optional): The ticket layout ID to be used instead of the auto-selected one.
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
yield one of the following status codes:
* ``200 OK`` The export succeeded. The body will be your resulting file. Might be large!
* ``409 Conflict`` Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
* ``410 Gone`` Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
* ``404 Not Found`` The export does not exist / is expired.
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
.. note:: To avoid performance issues, a maximum number of 1000 parts is currently allowed.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/render_batch/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"parts": [
{
"orderposition": 55412
},
{
"orderposition": 55412,
"override_channel": "web"
},
{
"orderposition": 55412,
"override_layout": 56
}
]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 202: no error
:statuscode 400: Invalid input options
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json

View File

@@ -88,16 +88,20 @@ class SalesChannelMigrationMixin:
}
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
raise ValidationError(
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the list of all sales channels."
)
raise ValidationError({
"limit_sales_channels": [
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the list of all sales channels."
]
})
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
raise ValidationError(
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the same list."
)
raise ValidationError({
"limit_sales_channels": [
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the same list."
]
})
if data["sales_channels"] == all_channels:
data["all_sales_channels"] = True

View File

@@ -200,6 +200,11 @@ class UpdateView(APIView):
device.save()
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
from ...base.signals import device_info_updated
device_info_updated.send(
sender=Device, old_device=request.auth, new_device=device
)
serializer = DeviceSerializer(device)
return Response(serializer.data)

View File

@@ -304,10 +304,21 @@ class TaxRule(LoggedModel):
subtract_from_gross = Decimal('0.00')
rate = adjust_rate
def _limit_subtract(base_price, subtract_from_gross):
if not subtract_from_gross:
return base_price
if base_price >= Decimal('0.00'):
# For positive prices, make sure they don't go negative because of bundles
return max(Decimal('0.00'), base_price - subtract_from_gross)
else:
# If the price is already negative, we don't really care any more
return base_price - subtract_from_gross
if rate == Decimal('0.00'):
gross = _limit_subtract(base_price, subtract_from_gross)
return TaxedPrice(
net=max(Decimal('0.00'), base_price - subtract_from_gross),
gross=max(Decimal('0.00'), base_price - subtract_from_gross),
net=gross,
gross=gross,
tax=Decimal('0.00'),
rate=rate,
name=self.name,
@@ -320,19 +331,14 @@ class TaxRule(LoggedModel):
base_price_is = 'net'
if base_price_is == 'gross':
if base_price >= Decimal('0.00'):
# For positive prices, make sure they don't go negative because of bundles
gross = max(Decimal('0.00'), base_price - subtract_from_gross)
else:
# If the price is already negative, we don't really care any more
gross = base_price - subtract_from_gross
gross = _limit_subtract(base_price, subtract_from_gross)
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
elif base_price_is == 'net':
net = base_price
gross = round_decimal((net * (1 + rate / 100)), currency)
if subtract_from_gross:
gross -= subtract_from_gross
gross = _limit_subtract(gross, subtract_from_gross)
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
else:

View File

@@ -838,3 +838,12 @@ is given as the first argument.
The ``sender`` keyword argument will contain the organizer.
"""
device_info_updated = django.dispatch.Signal()
"""
Arguments: ``old_device``, ``new_device``
This signal is sent out each time the information for a Device is modified.
Both the original and updated versions of the Device are included to allow
receivers to see what has been updated.
"""

View File

@@ -94,7 +94,9 @@ def process_login(request, user, keep_logged_in):
pretix_successful_logins.inc(1)
handle_login_source(user, request)
auth_login(request, user)
request.session['pretix_auth_login_time'] = int(time.time())
t = int(time.time())
request.session['pretix_auth_login_time'] = t
request.session['pretix_auth_last_used'] = t
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
return redirect_to_url(next_url)
return redirect('control:index')

View File

@@ -39,7 +39,7 @@ from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Exists, Max, OuterRef, Prefetch, Q, Subquery
from django.http import Http404, HttpResponseRedirect
from django.http import Http404, HttpResponseNotAllowed, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.functional import cached_property
@@ -193,6 +193,9 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
class CheckInListBulkRevertConfirmView(CheckInListQueryMixin, EventPermissionRequiredMixin, TemplateView):
template_name = "pretixcontrol/checkin/bulk_revert_confirm.html"
def get(self, request, *args, **kwargs):
return HttpResponseNotAllowed(permitted_methods=["POST"])
def post(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
return super().get(request, *args, **kwargs)

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-26 11:22+0000\n"
"PO-Revision-Date: 2024-10-02 17:00+0000\n"
"PO-Revision-Date: 2024-10-10 07:00+0000\n"
"Last-Translator: Patrick Chilton <chpatrick@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix/"
"hu/>\n"
@@ -3048,7 +3048,7 @@ msgstr ""
#: pretix/base/forms/questions.py:1144
msgid "You need to provide a company name."
msgstr ""
msgstr "Meg kell adnod céget."
#: pretix/base/forms/questions.py:1146
msgid "You need to provide your name."
@@ -3149,7 +3149,7 @@ msgstr ""
#: pretix/base/forms/widgets.py:234 pretix/base/forms/widgets.py:239
msgid "Business or institutional customer"
msgstr ""
msgstr "Cég"
#: pretix/base/forms/widgets.py:238
msgid "Individual customer"
@@ -3942,7 +3942,7 @@ msgstr ""
#: pretix/base/models/customers.py:299 pretix/base/models/orders.py:1513
#: pretix/base/models/orders.py:3175 pretix/base/settings.py:1096
msgid "Company name"
msgstr ""
msgstr "Cég"
#: pretix/base/models/customers.py:303 pretix/base/models/orders.py:1517
#: pretix/base/models/orders.py:3182 pretix/base/settings.py:81
@@ -5734,7 +5734,7 @@ msgstr ""
#: pretix/base/models/orders.py:3190
msgid "This reference will be printed on your invoice for your convenience."
msgstr ""
msgstr "Ezt a megjegyzést feltüntetjük neked a számlán."
#: pretix/base/models/organizer.py:79
msgid ""
@@ -7972,7 +7972,7 @@ msgstr ""
#: pretix/base/services/orders.py:561 pretix/control/forms/orders.py:205
msgid ""
"The cancellation fee cannot be higher than the total amount of this order."
msgstr ""
msgstr "A lemondási díj nem lehet magasabb a rendelés áránál."
#: pretix/base/services/orders.py:955
#, fuzzy
@@ -16058,7 +16058,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:30
msgctxt "terminal_zvt"
msgid "Card type"
msgstr ""
msgstr "Kártytípus"
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:37
#, fuzzy
@@ -16095,7 +16095,7 @@ msgstr ""
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_card.html:31
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html:14
msgid "Card number"
msgstr ""
msgstr "Kártyaszám"
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:65
msgid "Client Transaction Code"
@@ -26626,6 +26626,8 @@ msgid ""
"The total amount listed above will be withdrawn from your PayPal account "
"after the confirmation of your purchase."
msgstr ""
"A teljes összeg levonásra kerül a PayPal fiókodról miután megerősítjük a "
"rendelésedet."
#: pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_form.html:3
msgid ""
@@ -28457,11 +28459,11 @@ msgstr ""
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_card.html:29
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html:12
msgid "Card type"
msgstr ""
msgstr "Kártyatípus"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:14
msgid "The total amount will be withdrawn from your bank account."
msgstr ""
msgstr "A teljes összeg levonásra kerül a bankszámládról."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:18
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:23
@@ -28510,6 +28512,8 @@ msgid ""
"Your payment will be processed by Stripe, Inc. Your credit card data will be "
"transmitted directly to Stripe and never touches our servers."
msgstr ""
"A fizetésedet a Stripe fogja feldolgozni. A kártyaadataid egyenesen a Stripe-"
"hoz mennek és nem érintik a szervereinket."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:13
msgid "For a SEPA Debit payment, please turn on JavaScript."
@@ -28874,7 +28878,7 @@ msgstr ""
#: pretix/presale/checkoutflow.py:717
msgctxt "checkoutflow"
msgid "Your information"
msgstr ""
msgstr "Az adataid"
#: pretix/presale/checkoutflow.py:943
msgid ""
@@ -28906,7 +28910,7 @@ msgstr ""
#: pretix/presale/checkoutflow.py:1202
msgctxt "checkoutflow"
msgid "Payment"
msgstr ""
msgstr "Fizetés"
#: pretix/presale/checkoutflow.py:1315
msgid ""
@@ -28931,7 +28935,7 @@ msgstr ""
#: pretix/presale/checkoutflow.py:1442
msgctxt "checkoutflow"
msgid "Review order"
msgstr ""
msgstr "Rendelés ellenőrzése"
#: pretix/presale/checkoutflow.py:1536
msgid "You need to check all checkboxes on the bottom of the page."
@@ -30630,7 +30634,7 @@ msgstr "Számlaigénylés"
#: pretix/presale/templates/pretixpresale/event/order.html:286
msgid "Your information"
msgstr "Személyes adatok"
msgstr "Az adataid"
#: pretix/presale/templates/pretixpresale/event/order.html:289
msgid "Change your information"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-26 11:22+0000\n"
"PO-Revision-Date: 2024-09-30 05:00+0000\n"
"Last-Translator: Rosariocastellana <rosariocastellana@gmail.com>\n"
"PO-Revision-Date: 2024-10-08 18:00+0000\n"
"Last-Translator: Davide Manzella <manzella.davide97@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix/"
"it/>\n"
"Language: it\n"
@@ -178,10 +178,8 @@ msgid "Allowed URIs list, space separated"
msgstr "Lista delle URI autorizzate, separate da spazio"
#: pretix/api/models.py:47
#, fuzzy
#| msgid "Allowed URIs list, space separated"
msgid "Allowed Post Logout URIs list, space separated"
msgstr "Lista delle URI autorizzate, separate da spazio"
msgstr "Lista delle URI Dopo Logout autorizzate, separate da spazio"
#: pretix/api/models.py:51 pretix/base/models/customers.py:395
#: pretix/plugins/paypal/payment.py:113 pretix/plugins/paypal2/payment.py:110
@@ -264,10 +262,9 @@ msgid "Unknown plugin: '{name}'."
msgstr "Plugin sconosciuto: '{name}'."
#: pretix/api/serializers/event.py:295
#, fuzzy, python-brace-format
#| msgid "Unknown plugin: '{name}'."
#, python-brace-format
msgid "Restricted plugin: '{name}'."
msgstr "Plugin sconosciuto: '{name}'."
msgstr "Plugin a cui è limitato l'accesso: '{name}'."
#: pretix/api/serializers/item.py:86 pretix/api/serializers/item.py:148
#: pretix/api/serializers/item.py:359
@@ -332,8 +329,6 @@ msgid "This type of question cannot be asked during check-in."
msgstr "Questo tipo di domanda non può essere fatta durante il check-in."
#: pretix/api/serializers/item.py:531 pretix/control/forms/item.py:142
#, fuzzy
#| msgid "This type of question cannot be asked during check-in."
msgid "This type of question cannot be shown during check-in."
msgstr "Questo tipo di domanda non può essere fatta durante il check-in."
@@ -402,7 +397,7 @@ msgstr ""
#: pretix/api/views/checkin.py:604 pretix/api/views/checkin.py:611
msgid "Medium connected to other event"
msgstr ""
msgstr "Medium connesso ad un altro evento"
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:475
#, python-brace-format
@@ -515,10 +510,8 @@ msgid "Order denied"
msgstr "Ordine rifiutato"
#: pretix/api/webhooks.py:313
#, fuzzy
#| msgid "Order denied"
msgid "Order deleted"
msgstr "Ordine rifiutato"
msgstr "Ordine cancellato"
#: pretix/api/webhooks.py:317
msgid "Ticket checked in"
@@ -580,9 +573,8 @@ msgid "Test-Mode of shop has been deactivated"
msgstr "La modalità test del negozio è stata disattivata"
#: pretix/api/webhooks.py:370
#, fuzzy
msgid "Waiting list entry added"
msgstr "Lista d'attesa"
msgstr "Inserito correttamente nella lista d'attesa"
#: pretix/api/webhooks.py:374
msgid "Waiting list entry changed"
@@ -659,7 +651,7 @@ msgstr "Password"
#: pretix/base/auth.py:176 pretix/base/auth.py:183
msgid "Your password must contain both numeric and alphabetic characters."
msgstr ""
msgstr "La password deve contenere sia caratteri numerici che alfabetici."
#: pretix/base/auth.py:202 pretix/base/auth.py:212
#, python-format
@@ -667,8 +659,9 @@ msgid "Your password may not be the same as your previous password."
msgid_plural ""
"Your password may not be the same as one of your %(history_length)s previous "
"passwords."
msgstr[0] ""
msgstr[0] "La password non può essere uguale a quella che si vuole cambiare."
msgstr[1] ""
"La password non può essere uguale ad una delle %(history_length)s precedenti."
#: pretix/base/channels.py:168
msgid "Online shop"
@@ -676,13 +669,15 @@ msgstr "Vendite online"
#: pretix/base/channels.py:174
msgid "API"
msgstr ""
msgstr "API"
#: pretix/base/channels.py:175
msgid ""
"API sales channels come with no built-in functionality, but may be used for "
"custom integrations."
msgstr ""
"API per i canali di vendita senza funzionalità incorporate, ma può essere "
"usata per integrazioni personalizzate."
#: pretix/base/context.py:45
#, python-brace-format
@@ -746,6 +741,8 @@ msgid ""
"No supported Token Endpoint Auth Methods supported: "
"{token_endpoint_auth_methods_supported}"
msgstr ""
"Nessun metodo di autenticazione di tipo Token per Endpoint supportatp: "
"{token_endpoint_auth_methods_supported}"
#: pretix/base/customersso/oidc.py:203 pretix/base/customersso/oidc.py:210
#: pretix/base/customersso/oidc.py:229 pretix/base/customersso/oidc.py:246
@@ -1089,7 +1086,6 @@ msgid "No"
msgstr "No"
#: pretix/base/exporters/dekodi.py:42 pretix/base/exporters/invoices.py:66
#, fuzzy
msgctxt "export_category"
msgid "Invoices"
msgstr "Fatture"
@@ -1616,7 +1612,6 @@ msgid "Product data"
msgstr "Dati del prodotto"
#: pretix/base/exporters/items.py:50 pretix/base/exporters/orderlist.py:1128
#, fuzzy
msgctxt "export_category"
msgid "Product data"
msgstr "Dati del prodotto"
@@ -1800,9 +1795,8 @@ msgstr "Richiede particolare attenzione"
#: pretix/base/modelimport_orders.py:617 pretix/base/models/items.py:614
#: pretix/base/models/items.py:1197 pretix/base/models/orders.py:287
#: pretix/plugins/checkinlists/exporters.py:522
#, fuzzy
msgid "Check-in text"
msgstr "Checkout"
msgstr "Testo di Check-in"
#: pretix/base/exporters/items.py:91 pretix/base/models/items.py:619
#: pretix/base/models/items.py:1117
@@ -2319,10 +2313,8 @@ msgid "Order comment"
msgstr "Commento all'ordine"
#: pretix/base/exporters/orderlist.py:622
#, fuzzy
#| msgid "Position ID"
msgid "Add-on to position ID"
msgstr "ID Posizione"
msgstr "Add-on al ID Posizione"
#: pretix/base/exporters/orderlist.py:650 pretix/base/pdf.py:340
msgid "Invoice address street"
@@ -2353,7 +2345,7 @@ msgstr "Nazione di fatturazione"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:162
#: pretix/plugins/checkinlists/apps.py:44
msgid "Check-in lists"
msgstr ""
msgstr "Liste de Check-in"
#: pretix/base/exporters/orderlist.py:822
msgid "Order transaction data"
@@ -2553,9 +2545,8 @@ msgid "Payment method"
msgstr "Metodo di pagamento"
#: pretix/base/exporters/orderlist.py:1077
#, fuzzy
msgid "Matching ID"
msgstr "ID Pagamento"
msgstr "ID Combaciato"
#: pretix/base/exporters/orderlist.py:1077
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:38
@@ -2620,7 +2611,6 @@ msgstr "Transazioni con carta regalo"
#: pretix/base/exporters/orderlist.py:1183
#: pretix/base/exporters/orderlist.py:1288
#, fuzzy
msgctxt "export_category"
msgid "Gift cards"
msgstr "Carte regalo"
@@ -2822,34 +2812,29 @@ msgstr "Ultima data di fatturazione dell'ordine"
#: pretix/control/templates/pretixcontrol/organizers/reusable_media.html:6
#: pretix/control/templates/pretixcontrol/organizers/reusable_media.html:9
msgid "Reusable media"
msgstr ""
msgstr "Media Riutilizzabile"
#: pretix/base/exporters/reusablemedia.py:35
msgctxt "export_category"
msgid "Reusable media"
msgstr ""
msgstr "Media Riutilizzabile"
#: pretix/base/exporters/reusablemedia.py:36
#, fuzzy
#| msgid ""
#| "Download a spreadsheet with information on all events in this organizer "
#| "account."
msgid ""
"Download a spread sheet with the data of all reusable medias on your account."
msgstr ""
"Scarica un foglio di calcolo con le informazioni su tutti gli eventi di "
"questo account organizzatore."
"Scarica un foglio di calcolo con le informazioni con i dati di tutti i media "
"riutlizzabili dell' account."
#: pretix/base/exporters/reusablemedia.py:46 pretix/base/models/media.py:67
#, fuzzy
msgctxt "reusable_medium"
msgid "Media type"
msgstr "Tipo di tariffa"
msgstr "Tipo di Media"
#: pretix/base/exporters/reusablemedia.py:47 pretix/base/models/media.py:73
msgctxt "reusable_medium"
msgid "Identifier"
msgstr ""
msgstr "Identificativo"
#: pretix/base/exporters/reusablemedia.py:49 pretix/base/models/media.py:81
#: pretix/base/models/orders.py:263 pretix/base/models/orders.py:3014
@@ -2861,22 +2846,18 @@ msgstr "Data di scadenza"
#: pretix/base/exporters/reusablemedia.py:50 pretix/base/models/media.py:90
#: pretix/control/templates/pretixcontrol/order/index.html:215
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:132
#, fuzzy
msgid "Customer account"
msgstr "Domande"
msgstr "Account del cliente"
#: pretix/base/exporters/reusablemedia.py:51 pretix/base/models/media.py:97
#, fuzzy
msgid "Linked ticket"
msgstr "Testo footer aggiuntivo"
msgstr "Ticket collegato"
#: pretix/base/exporters/reusablemedia.py:52 pretix/base/models/media.py:104
#, fuzzy
msgid "Linked gift card"
msgstr "Pagato con carta regalo"
msgstr "Carta regalo collegata"
#: pretix/base/exporters/waitinglist.py:42
#, fuzzy
msgctxt "export_category"
msgid "Waiting list"
msgstr "Lista d'attesa"
@@ -2955,7 +2936,7 @@ msgstr "Codice del voucher"
#: pretix/base/forms/__init__.py:118
#, python-brace-format
msgid "You can use {markup_name} in this field."
msgstr ""
msgstr "Puoi usare {markup_name} in questo campo."
#: pretix/base/forms/__init__.py:178
#, python-format
@@ -3169,12 +3150,15 @@ msgid ""
"up. Please note: to use literal \"{\" or \"}\", you need to double them as "
"\"{{\" and \"}}\"."
msgstr ""
"è presente un errore nella sintassi dei placeholder. Controllare che le "
"graffe di apertura \"{\" e di chiusura \"}\" sui placeholder match up. Fare "
"attenzione: perutlizzare \"{\" oppure \"}\", è necessario raddoppiarli \"{{ "
"e \"}}\"."
#: pretix/base/forms/validators.py:72 pretix/control/views/event.py:763
#, fuzzy, python-format
#| msgid "Invalid placeholder(s): %(value)s"
#, python-format
msgid "Invalid placeholder: {%(value)s}"
msgstr "Placeholder(s) non valido: %(value)s"
msgstr "Placeholder non valido: %(value)s"
#: pretix/base/forms/widgets.py:68
#, python-format
@@ -3191,7 +3175,7 @@ msgstr "Segnalibri disponibili: {list}"
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_create.html:40
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_update.html:54
msgid "Time"
msgstr ""
msgstr "Orario"
#: pretix/base/forms/widgets.py:234 pretix/base/forms/widgets.py:239
msgid "Business or institutional customer"
@@ -3420,7 +3404,7 @@ msgstr ""
#: pretix/base/invoice.py:858
msgid "Default invoice renderer (European-style letter)"
msgstr ""
msgstr "Renderizzatore di fatture di default (Stile Europeo)"
#: pretix/base/invoice.py:947
msgctxt "invoice"
@@ -3429,15 +3413,13 @@ msgstr "(Per cortesia citare sempre.)"
#: pretix/base/invoice.py:994
msgid "Simplified invoice renderer"
msgstr ""
msgstr "Renderizzatore di fatture semplificato"
#: pretix/base/invoice.py:1013
#, fuzzy, python-brace-format
#| msgctxt "subevent"
#| msgid "Event series date changed"
#, python-brace-format
msgctxt "invoice"
msgid "Event date: {date_range}"
msgstr "Serie di date modificate"
msgstr "Data evento: {date_range}"
#: pretix/base/media.py:61
msgid "Barcode / QR-Code"
@@ -3455,58 +3437,57 @@ msgstr "Lista predefinita"
#: pretix/base/modelimport.py:112
msgid "Keep empty"
msgstr ""
msgstr "Lasciare vuoto"
#: pretix/base/modelimport.py:139
#, python-brace-format
msgid "Invalid setting for column \"{header}\"."
msgstr ""
msgstr "Settaggio invalido per colonna \"{header}\"."
#: pretix/base/modelimport.py:199
#, python-brace-format
msgid "Could not parse {value} as a yes/no value."
msgstr ""
msgstr "Non è stato possibile leggere {value} come valore di sì/no."
#: pretix/base/modelimport.py:222
#, python-brace-format
msgid "Could not parse {value} as a date and time."
msgstr ""
msgstr "Non è stato possibile leggere {value} come data e ora."
#: pretix/base/modelimport.py:232 pretix/control/views/orders.py:1161
#: pretix/control/views/orders.py:1190 pretix/control/views/orders.py:1234
#: pretix/control/views/orders.py:1269 pretix/control/views/orders.py:1292
msgid "You entered an invalid number."
msgstr ""
msgstr "Hai inserito un numero invalido."
#: pretix/base/modelimport.py:276 pretix/base/modelimport.py:288
msgctxt "subevent"
msgid "No matching date was found."
msgstr ""
msgstr "Nessuna data combaciante è stata trovata."
#: pretix/base/modelimport.py:278 pretix/base/modelimport.py:290
msgctxt "subevent"
msgid "Multiple matching dates were found."
msgstr ""
msgstr "Molteplici date che combaciano sono state trovate."
#: pretix/base/modelimport_orders.py:85
#, fuzzy
msgid "Enter a valid phone number."
msgstr "Numero di telefono"
msgstr "Inserire un numero di telefono valido."
#: pretix/base/modelimport_orders.py:100 pretix/presale/views/waiting.py:118
msgctxt "subevent"
msgid "You need to select a date."
msgstr ""
msgstr "Devi selezionare una data."
#: pretix/base/modelimport_orders.py:128
#: pretix/base/modelimport_vouchers.py:194
msgid "No matching product was found."
msgstr ""
msgstr "Nessun prodotto combaciante è stato trovato."
#: pretix/base/modelimport_orders.py:130
#: pretix/base/modelimport_vouchers.py:196
msgid "Multiple matching products were found."
msgstr ""
msgstr "Molteplici prodotti combacianti sono stati trovati."
#: pretix/base/modelimport_orders.py:139
#: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1205
@@ -3518,13 +3499,13 @@ msgstr "Variante prodotto"
#: pretix/base/modelimport_vouchers.py:225
#: pretix/base/modelimport_vouchers.py:259
msgid "No matching variation was found."
msgstr ""
msgstr "Nessuna variazione combaciante trovata."
#: pretix/base/modelimport_orders.py:161
#: pretix/base/modelimport_vouchers.py:227
#: pretix/base/modelimport_vouchers.py:261
msgid "Multiple matching variations were found."
msgstr ""
msgstr "Multiple variazioni combacianti trovate."
#: pretix/base/modelimport_orders.py:164
msgid "You need to select a variation for this product."
@@ -3543,15 +3524,15 @@ msgstr "Indirizzo di fatturazione"
#: pretix/base/modelimport_orders.py:251 pretix/base/modelimport_orders.py:397
msgid "Please enter a valid country code."
msgstr ""
msgstr "Inserire un codice prefisso del paese valido."
#: pretix/base/modelimport_orders.py:268 pretix/base/modelimport_orders.py:414
msgid "States are not supported for this country."
msgstr ""
msgstr "Stati non sono supportati per questa nazione."
#: pretix/base/modelimport_orders.py:276 pretix/base/modelimport_orders.py:422
msgid "Please enter a valid state."
msgstr ""
msgstr "Inserire uno stato valido."
#: pretix/base/modelimport_orders.py:325 pretix/control/forms/filter.py:651
msgid "Attendee e-mail address"
@@ -3575,7 +3556,7 @@ msgstr "Stato"
#: pretix/base/modelimport_orders.py:432
msgid "Calculate from product"
msgstr ""
msgstr "Calculare dal prodotto"
#: pretix/base/modelimport_orders.py:449
#: pretix/control/templates/pretixcontrol/checkin/index.html:111
@@ -3585,29 +3566,29 @@ msgstr "Codice biglietto"
#: pretix/base/modelimport_orders.py:450
msgid "Generate automatically"
msgstr ""
msgstr "Generare automaticamente"
#: pretix/base/modelimport_orders.py:459
msgid "You cannot assign a position secret that already exists."
msgstr ""
msgstr "Non è possibile assegnare un segreto di posizione preesistente."
#: pretix/base/modelimport_orders.py:490
msgid "Please enter a valid language code."
msgstr ""
msgstr "Inserire un codice linguaggio valido."
#: pretix/base/modelimport_orders.py:558 pretix/base/modelimport_orders.py:560
msgid "Please enter a valid sales channel."
msgstr ""
msgstr "Inserire un valido canale di vendita."
#: pretix/base/modelimport_orders.py:584
#: pretix/base/modelimport_vouchers.py:291
msgid "Multiple matching seats were found."
msgstr ""
msgstr "Molteplici posti a sedere trovati."
#: pretix/base/modelimport_orders.py:586
#: pretix/base/modelimport_vouchers.py:293
msgid "No matching seat was found."
msgstr ""
msgstr "Nessun posto a sedere trovato."
#: pretix/base/modelimport_orders.py:589
#: pretix/base/modelimport_vouchers.py:296 pretix/base/services/cart.py:212
@@ -3616,10 +3597,12 @@ msgstr ""
msgid ""
"The seat you selected has already been taken. Please select a different seat."
msgstr ""
"Il posto a sedere da lei selezionato è stato già preso. Per piacere "
"selezionare un posto a sedere differente."
#: pretix/base/modelimport_orders.py:592 pretix/base/services/cart.py:209
msgid "You need to select a specific seat."
msgstr ""
msgstr "è necessario selezionare un posto a sedere specifico."
#: pretix/base/modelimport_orders.py:646 pretix/base/models/items.py:1618
#: pretix/base/models/items.py:1713 pretix/control/forms/item.py:91
@@ -3627,16 +3610,16 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/items/question_edit.html:17
#: pretix/control/templates/pretixcontrol/items/questions.html:21
msgid "Question"
msgstr ""
msgstr "Domanda"
#: pretix/base/modelimport_orders.py:656 pretix/base/modelimport_orders.py:664
#: pretix/base/models/items.py:1777 pretix/base/models/items.py:1795
msgid "Invalid option selected."
msgstr ""
msgstr "L' opzione selezionata è invalida."
#: pretix/base/modelimport_orders.py:658 pretix/base/modelimport_orders.py:666
msgid "Ambiguous option selected."
msgstr ""
msgstr "L'opzione selezionata è ambigua."
#: pretix/base/modelimport_orders.py:697 pretix/base/models/orders.py:237
#: pretix/control/forms/orders.py:643 pretix/control/forms/organizer.py:784
@@ -3645,48 +3628,49 @@ msgstr "Cliente"
#: pretix/base/modelimport_orders.py:710
msgid "No matching customer was found."
msgstr ""
msgstr "Nessun cliente ciìombaciante è stato trovato."
#: pretix/base/modelimport_vouchers.py:50 pretix/base/models/vouchers.py:488
msgid "A voucher with this code already exists."
msgstr ""
msgstr "Un voucher con questo codice esiste già."
#: pretix/base/modelimport_vouchers.py:68 pretix/base/models/memberships.py:57
#: pretix/base/models/vouchers.py:196 pretix/control/views/vouchers.py:120
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:28
msgid "Maximum usages"
msgstr ""
msgstr "Massimi utlizzi"
#: pretix/base/modelimport_vouchers.py:79
msgid "The maximum number of usages must be set."
msgstr ""
msgstr "Il massimo numero di ultizzi deve essere configurato."
#: pretix/base/modelimport_vouchers.py:88 pretix/base/models/vouchers.py:205
#, fuzzy
msgid "Minimum usages"
msgstr "Utilizzi massimi per voucher"
msgstr "Mini utlizzi"
#: pretix/base/modelimport_vouchers.py:103 pretix/base/models/vouchers.py:213
msgid "Maximum discount budget"
msgstr ""
msgstr "Massimo discount budget"
#: pretix/base/modelimport_vouchers.py:119 pretix/base/models/vouchers.py:225
#: pretix/control/forms/filter.py:2106
msgid "Reserve ticket from quota"
msgstr ""
msgstr "Prenotare biglietto dalla quota disponibile"
#: pretix/base/modelimport_vouchers.py:127 pretix/base/models/vouchers.py:233
msgid "Allow to bypass quota"
msgstr ""
msgstr "Permettere di superare la quota"
#: pretix/base/modelimport_vouchers.py:135 pretix/base/models/vouchers.py:239
msgid "Price mode"
msgstr ""
msgstr "Modalità prezzo"
#: pretix/base/modelimport_vouchers.py:150
#, python-brace-format
msgid "Could not parse {value} as a price mode, use one of {options}."
msgstr ""
"Non è possible leggere {value} come modalità prezzo, usare una delle "
"{options}."
#: pretix/base/modelimport_vouchers.py:160 pretix/base/models/vouchers.py:245
msgid "Voucher value"
@@ -3694,56 +3678,64 @@ msgstr "Valore Voucher"
#: pretix/base/modelimport_vouchers.py:165
msgid "It is pointless to set a value without a price mode."
msgstr ""
msgstr "è inutile settare un valore senza modalità prezzo."
#: pretix/base/modelimport_vouchers.py:237 pretix/base/models/items.py:2040
#: pretix/base/models/vouchers.py:272
#: pretix/control/templates/pretixcontrol/items/quota_edit.html:8
#: pretix/control/templates/pretixcontrol/items/quota_edit.html:15
msgid "Quota"
msgstr ""
msgstr "Quota"
#: pretix/base/modelimport_vouchers.py:253
msgid "You cannot specify a quota if you specified a product."
msgstr ""
msgstr "Non è possiblile specificare una quota se hai specificato un prodotto."
#: pretix/base/modelimport_vouchers.py:282 pretix/base/models/vouchers.py:495
msgid "You need to choose a date if you select a seat."
msgstr ""
msgstr "è necessario scegliere una data se si seleziona un posto a sedere."
#: pretix/base/modelimport_vouchers.py:299 pretix/base/models/vouchers.py:513
msgid "You need to choose a specific product if you select a seat."
msgstr ""
"è necessario scegliere un prodotto specifico se si seleziona un posto a "
"sedere."
#: pretix/base/modelimport_vouchers.py:302 pretix/base/models/vouchers.py:516
msgid "Seat-specific vouchers can only be used once."
msgstr ""
"Voucher specifici per un posto a sedere possono essere usati solo una volta "
"sola."
#: pretix/base/modelimport_vouchers.py:306 pretix/base/models/vouchers.py:519
#, python-brace-format
#, fuzzy, python-brace-format
msgid "You need to choose the product \"{prod}\" for this seat."
msgstr ""
"è necessario sceglliere il prodotto \"[prod}\" per questo posto a sedere."
#: pretix/base/modelimport_vouchers.py:318 pretix/base/models/vouchers.py:285
#: pretix/control/templates/pretixcontrol/vouchers/index.html:129
#: pretix/control/templates/pretixcontrol/vouchers/tags.html:42
#: pretix/control/views/vouchers.py:120
msgid "Tag"
msgstr ""
msgstr "Etichetta"
#: pretix/base/modelimport_vouchers.py:334 pretix/base/models/vouchers.py:297
msgid "Shows hidden products that match this voucher"
msgstr ""
msgstr "Mostrare i prodotti nascoscti che combaciano con questo voucher"
#: pretix/base/modelimport_vouchers.py:343 pretix/base/models/vouchers.py:301
msgid "Offer all add-on products for free when redeeming this voucher"
msgstr ""
"Offrire tutti i prodotti add-on gratuitamente riscattando questo voucher"
#: pretix/base/modelimport_vouchers.py:351 pretix/base/models/vouchers.py:305
msgid ""
"Include all bundled products without a designated price when redeeming this "
"voucher"
msgstr ""
"Includere tutti i prodotti inclusi nel pacchetto senza un prezzo quando "
"riscatti questo voucher"
#: pretix/base/models/auth.py:248
msgid "Is active"
@@ -3824,18 +3816,21 @@ msgstr ""
#: pretix/base/models/checkin.py:65
msgctxt "checkin"
msgid "Ignore check-ins on this list in statistics"
msgstr ""
msgstr "Ignora check-in presenti in questa lista, per le statistiche"
#: pretix/base/models/checkin.py:69
msgctxt "checkin"
msgid "Tickets with a check-in on this list should be considered \"used\""
msgstr ""
"Biglietti con un check-in in questa lista sono da considerarsi \"usati\""
#: pretix/base/models/checkin.py:70
msgid ""
"This is relevant in various situations, e.g. for deciding if a ticket can "
"still be canceled by the customer."
msgstr ""
"Questo è rilevante in varie situazioni, ad esempio per devidere se un "
"biglietto può ancora essere cancellato dal cliente."
#: pretix/base/models/checkin.py:74
msgctxt "checkin"
@@ -3908,6 +3903,10 @@ msgid ""
"replacement, our new plugin \"Auto check-in\" can be used. When we remove "
"this option, we will automatically migrate your event to use the new plugin."
msgstr ""
"Questa opzione è deprecata e verra rimossa nei mesi seguenti. Come "
"rimpiazzo, il nuovo plugin \"Auto check-in\" può essere usato. Quando verra "
"rimossa questa opzione, migreremo automaticamente il tuo evento nell' "
"utilizzo del nuovo plugin."
#: pretix/base/models/checkin.py:340
msgid "Entry"
@@ -4293,15 +4292,18 @@ msgid ""
"Optional. No products will be sold after this date. If you do not set this "
"value, the presale will end after the end date of your event."
msgstr ""
"Opzionale. nessun prodotto verrà venduto oltre questa data. Se questo campo "
"non viene settato, il periodo di prevendita finirà dopo la data di fine "
"evento."
#: pretix/base/models/event.py:599 pretix/base/models/event.py:1480
#: pretix/control/forms/subevents.py:94
msgid "Optional. No products will be sold before this date."
msgstr ""
msgstr "Opzionale. Nessun prodotto verrà venduto rima di questa data."
#: pretix/base/models/event.py:624 pretix/control/navigation.py:65
msgid "Plugins"
msgstr ""
msgstr "Plugin"
#: pretix/base/models/event.py:631 pretix/base/pdf.py:229
#: pretix/control/forms/event.py:260 pretix/control/forms/filter.py:1677
@@ -4311,11 +4313,11 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/organizers/index.html:90
#: pretix/presale/views/widget.py:682
msgid "Event series"
msgstr ""
msgstr "Serie di eventi"
#: pretix/base/models/event.py:635 pretix/base/models/event.py:1508
msgid "Seating plan"
msgstr ""
msgstr "Piani dei posti a sedere"
#: pretix/base/models/event.py:642 pretix/base/models/items.py:626
#, fuzzy

View File

@@ -19,21 +19,33 @@
# 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 datetime import timedelta
from celery.result import AsyncResult
from django.conf import settings
from django.db import transaction
from django.db.models import QuerySet
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.functional import lazy
from rest_framework import serializers, viewsets
from django.utils.timezone import now
from django_scopes import scopes_disabled
from rest_framework import serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_framework.reverse import reverse
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from ...api.serializers.fields import UploadedFileField
from ...base.models import SalesChannel
from ...base.models import CachedFile, OrderPosition, SalesChannel
from ...base.pdf import PdfLayoutValidator
from ...helpers.http import ChunkBasedFileResponse
from ...multidomain.utils import static_absolute
from .models import TicketLayout, TicketLayoutItem
from .tasks import bulk_render
class ItemAssignmentSerializer(I18nAwareModelSerializer):
@@ -156,3 +168,123 @@ class TicketLayoutItemViewSet(viewsets.ReadOnlyModelViewSet):
**super().get_serializer_context(),
'event': self.request.event,
}
with scopes_disabled():
class RenderJobPartSerializer(serializers.Serializer):
orderposition = serializers.PrimaryKeyRelatedField(
queryset=OrderPosition.objects.none(),
required=True,
allow_null=False,
)
override_layout = serializers.PrimaryKeyRelatedField(
queryset=TicketLayout.objects.none(),
required=False,
allow_null=True,
)
override_channel = serializers.SlugRelatedField(
queryset=SalesChannel.objects.none(),
slug_field='identifier',
required=False,
allow_null=True,
)
class RenderJobSerializer(serializers.Serializer):
parts = RenderJobPartSerializer(many=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['parts'].child.fields['orderposition'].queryset = OrderPosition.objects.filter(order__event=self.context['event'])
self.fields['parts'].child.fields['override_layout'].queryset = self.context['event'].ticket_layouts.all()
self.fields['parts'].child.fields['override_channel'].queryset = self.context['event'].organizer.sales_channels.all()
def validate(self, attrs):
if len(attrs["parts"]) > 1000:
raise ValidationError({"parts": ["Please do not submit more than 1000 parts."]})
return super().validate(attrs)
class TicketRendererViewSet(viewsets.ViewSet):
permission = 'can_view_orders'
def get_serializer_kwargs(self):
return {}
def list(self, request, *args, **kwargs):
raise Http404()
def retrieve(self, request, *args, **kwargs):
raise Http404()
def update(self, request, *args, **kwargs):
raise Http404()
def partial_update(self, request, *args, **kwargs):
raise Http404()
def destroy(self, request, *args, **kwargs):
raise Http404()
@action(detail=False, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
def download(self, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
if cf.file:
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
return resp
elif not settings.HAS_CELERY:
return Response(
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
status=status.HTTP_410_GONE
)
res = AsyncResult(kwargs['asyncid'])
if res.failed():
if isinstance(res.info, dict) and res.info['exc_type'] == 'ExportError':
msg = res.info['exc_message']
else:
msg = 'Internal error'
return Response(
{'status': 'failed', 'message': msg},
status=status.HTTP_410_GONE
)
return Response(
{
'status': 'running' if res.state in ('PROGRESS', 'STARTED', 'SUCCESS') else 'waiting',
},
status=status.HTTP_409_CONFLICT
)
@action(detail=False, methods=['POST'])
def render_batch(self, *args, **kwargs):
serializer = RenderJobSerializer(data=self.request.data, context={
"event": self.request.event,
})
serializer.is_valid(raise_exception=True)
cf = CachedFile(web_download=False)
cf.date = now()
cf.expires = now() + timedelta(hours=24)
cf.save()
async_result = bulk_render.apply_async(args=(
self.request.event.id,
str(cf.id),
[
{
"orderposition": r["orderposition"].id,
"override_layout": r["override_layout"].id if r.get("override_layout") else None,
"override_channel": r["override_channel"].id if r.get("override_channel") else None,
} for r in serializer.validated_data["parts"]
]
))
url_kwargs = {
'asyncid': str(async_result.id),
'cfid': str(cf.id),
}
url_kwargs.update(self.kwargs)
return Response({
'download': reverse('api-v1:ticketpdfrenderer-download', kwargs=url_kwargs, request=self.request)
}, status=status.HTTP_202_ACCEPTED)

View File

@@ -19,18 +19,26 @@
# 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/>.
#
import json
import logging
from io import BytesIO
from django.core.files.base import ContentFile
from django.db.models import Prefetch, prefetch_related_objects
from pypdf import PdfWriter
from pretix.base.models import (
CachedFile, Event, OrderPosition, cachedfile_name,
CachedFile, Checkin, Event, EventMetaValue, ItemMetaValue,
ItemVariationMetaValue, OrderPosition, SalesChannel, SubEventMetaValue,
cachedfile_name,
)
from pretix.base.services.orders import OrderError
from pretix.base.services.tasks import EventTask
from pretix.celery_app import app
from ...base.i18n import language
from ...base.services.export import ExportError
from .models import TicketLayout
from .ticketoutput import PdfTicketOutput
logger = logging.getLogger(__name__)
@@ -46,3 +54,93 @@ def tickets_create_pdf(event: Event, fileid: int, position: int, channel) -> int
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
file.save()
return file.pk
@app.task(base=EventTask, throws=(OrderError, ExportError,))
def bulk_render(event: Event, fileid: int, parts: list) -> int:
file = CachedFile.objects.get(id=fileid)
channels = SalesChannel.objects.in_bulk([p["override_channel"] for p in parts if p.get("override_channel")])
layouts = TicketLayout.objects.in_bulk([p["override_layout"] for p in parts if p.get("override_layout")])
positions = OrderPosition.objects.all()
prefetch_related_objects([event.organizer], 'meta_properties')
prefetch_related_objects(
[event],
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'),
'questions',
'item_meta_properties',
)
positions = positions.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('item', queryset=event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
)),
'variation',
'answers', 'answers__options', 'answers__question',
'item__category',
'addon_to__answers', 'addon_to__answers__options', 'addon_to__answers__question',
Prefetch('addons', positions.select_related('item', 'variation')),
Prefetch('subevent', queryset=event.subevents.prefetch_related(
Prefetch('meta_values', to_attr='meta_values_cached',
queryset=SubEventMetaValue.objects.select_related('property'))
)),
'linked_media',
Prefetch('order', event.orders.select_related('invoice_address').prefetch_related(
Prefetch(
'positions',
positions.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('item', queryset=event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
)),
Prefetch('variation', queryset=event.items.prefetch_related(
Prefetch('meta_values', ItemVariationMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
)),
'answers', 'answers__options', 'answers__question',
'item__category',
Prefetch('subevent', queryset=event.subevents.prefetch_related(
Prefetch('meta_values', to_attr='meta_values_cached',
queryset=SubEventMetaValue.objects.select_related('property'))
)),
Prefetch('addons', positions.select_related('item', 'variation', 'seat'))
).select_related('addon_to', 'seat', 'addon_to__seat')
)
))
).select_related(
'addon_to', 'seat', 'addon_to__seat'
)
positions = positions.in_bulk([p["orderposition"] for p in parts])
merger = PdfWriter()
for part in parts:
p = positions[part["orderposition"]]
p.order.event = event # performance optimization
with (language(p.order.locale)):
kwargs = {}
if part.get("override_channel"):
kwargs["override_channel"] = channels[part["override_channel"]].identifier
if part.get("override_layout"):
l = layouts[part["override_layout"]]
kwargs["override_layout"] = json.loads(l.layout)
kwargs["override_background"] = l.background
prov = PdfTicketOutput(
event,
**kwargs,
)
filename, ctype, data = prov.generate(p)
merger.append(ContentFile(data))
outbuffer = BytesIO()
merger.write(outbuffer)
merger.close()
outbuffer.seek(0)
file.type = "application/pdf"
file.file.save(cachedfile_name(file, file.filename), ContentFile(outbuffer.getvalue()))
file.save()
return file.pk

View File

@@ -23,7 +23,7 @@ from django.urls import re_path
from pretix.api.urls import event_router
from pretix.plugins.ticketoutputpdf.api import (
TicketLayoutItemViewSet, TicketLayoutViewSet,
TicketLayoutItemViewSet, TicketLayoutViewSet, TicketRendererViewSet,
)
from pretix.plugins.ticketoutputpdf.views import (
LayoutCreate, LayoutDelete, LayoutEditorView, LayoutGetDefault,
@@ -48,3 +48,4 @@ urlpatterns = [
]
event_router.register('ticketlayouts', TicketLayoutViewSet)
event_router.register('ticketlayoutitems', TicketLayoutItemViewSet)
event_router.register('ticketpdfrenderer', TicketRendererViewSet, basename='ticketpdfrenderer')

View File

@@ -807,6 +807,19 @@ def test_event_update_plugins_validation(token_client, organizer, event, item, m
)
assert resp.status_code == 200
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
{
"all_sales_channels": False,
"limit_sales_channels": ["web"],
"sales_channels": ["bar"],
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == ('{"limit_sales_channels":["If \'limit_sales_channels\' is set, the legacy '
'attribute \'sales_channels\' must not be set or set to the same list."]}')
@pytest.mark.django_db
def test_event_test_mode(token_client, organizer, event):

View File

@@ -782,3 +782,59 @@ def test_custom_rules_country_rate_subtract_from_gross(event):
rate=Decimal('100.00'),
name='',
)
@pytest.mark.django_db
def test_no_negative_due_to_subtract_from_gross(event):
tr = TaxRule(
event=event,
rate=Decimal("19.00"),
price_includes_tax=True,
)
assert tr.tax(Decimal('100.00'), subtract_from_gross=Decimal('200.00')).gross == Decimal("0.00")
tr = TaxRule(
event=event,
rate=Decimal("0.00"),
price_includes_tax=True,
)
assert tr.tax(Decimal('100.00'), subtract_from_gross=Decimal('200.00')).gross == Decimal("0.00")
tr = TaxRule(
event=event,
rate=Decimal("19.00"),
price_includes_tax=False,
)
assert tr.tax(Decimal('100.00'), subtract_from_gross=Decimal('200.00')).gross == Decimal("0.00")
tr = TaxRule(
event=event,
rate=Decimal("19.00"),
price_includes_tax=True,
)
assert tr.tax(Decimal('100.00'), subtract_from_gross=Decimal('200.00')).gross == Decimal("0.00")
@pytest.mark.django_db
def test_allow_negative(event):
tr = TaxRule(
event=event,
rate=Decimal("19.00"),
price_includes_tax=True,
)
assert tr.tax(Decimal('-100.00')).gross == Decimal("-100.00")
tr = TaxRule(
event=event,
rate=Decimal("0.00"),
price_includes_tax=True,
)
assert tr.tax(Decimal('-100.00')).gross == Decimal("-100.00")
tr = TaxRule(
event=event,
rate=Decimal("19.00"),
price_includes_tax=False,
)
assert tr.tax(Decimal('-100.00')).gross == Decimal("-119.00")
tr = TaxRule(
event=event,
rate=Decimal("19.00"),
price_includes_tax=True,
)
assert tr.tax(Decimal('-100.00')).gross == Decimal("-100.00")

View File

@@ -41,7 +41,7 @@ from django.contrib.auth.tokens import (
PasswordResetTokenGenerator, default_token_generator,
)
from django.core import mail as djmail
from django.test import TestCase, override_settings
from django.test import RequestFactory, TestCase, override_settings
from django.utils.timezone import now
from django_otp.oath import TOTP
from django_otp.plugins.otp_totp.models import TOTPDevice
@@ -50,6 +50,7 @@ from webauthn.authentication.verify_authentication_response import (
)
from pretix.base.models import Organizer, Team, U2FDevice, User
from pretix.control.views.auth import process_login
from pretix.helpers import security
@@ -892,6 +893,19 @@ class SessionTimeOutTest(TestCase):
response = self.client.get('/control/')
self.assertEqual(response.status_code, 302)
def test_plugin_auth_updates_auth_last_used(self):
session = self.client.session
session['pretix_auth_long_session'] = True
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 5
session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 - 60
session.save()
request = RequestFactory().get("/")
request.session = self.client.session
process_login(request, self.user, keep_logged_in=True)
assert request.session['pretix_auth_last_used'] >= int(time.time()) - 60
def test_update_session_activity(self):
t1 = int(time.time()) - 5
session = self.client.session

View File

@@ -21,6 +21,8 @@
#
import copy
import json
from datetime import timedelta
from decimal import Decimal
import pytest
from django.core.files.base import ContentFile
@@ -28,7 +30,9 @@ from django.utils.timezone import now
from django_scopes import scopes_disabled
from rest_framework.test import APIClient
from pretix.base.models import Event, Item, Organizer, Team
from pretix.base.models import (
Event, Item, Order, OrderPosition, Organizer, Team,
)
from pretix.plugins.ticketoutputpdf.models import TicketLayoutItem
@@ -39,14 +43,35 @@ def env():
organizer=o, name='Dummy', slug='dummy',
date_from=now(), plugins='pretix.plugins.banktransfer'
)
t = Team.objects.create(organizer=event.organizer)
t = Team.objects.create(organizer=event.organizer, can_view_orders=True)
t.limit_events.add(event)
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
tl = event.ticket_layouts.create(name="Foo", default=True, layout='[{"a": 2}]')
tl = event.ticket_layouts.create(
name="Foo",
default=True,
layout='[{"type": "poweredby", "left": "0", "bottom": "0", "size": "1.00", "content": "dark"}]',
)
TicketLayoutItem.objects.create(layout=tl, item=item1, sales_channel=o.sales_channels.get(identifier="web"))
return event, tl, item1
@pytest.fixture
def position(env):
item = env[0].items.create(name="Ticket", default_price=3, admission=True)
order = Order.objects.create(
code='FOO', event=env[0], email='dummy@dummy.test',
status=Order.STATUS_PAID, locale='en',
datetime=now() - timedelta(days=4),
expires=now() - timedelta(hours=4) + timedelta(days=10),
total=Decimal('23.00'),
sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
)
return OrderPosition.objects.create(
order=order, item=item, variation=None,
price=Decimal("23.00"), attendee_name_parts={"full_name": "Peter"}, positionid=1
)
@pytest.fixture
def client():
return APIClient()
@@ -65,7 +90,7 @@ RES_LAYOUT = {
'name': 'Foo',
'default': True,
'item_assignments': [{'item': 1, 'sales_channel': 'web'}],
'layout': [{'a': 2}],
'layout': [{"type": "poweredby", "left": "0", "bottom": "0", "size": "1.00", "content": "dark"}],
'background': 'http://example.com/static/pretixpresale/pdf/ticket_default_a4.pdf'
}
@@ -213,3 +238,92 @@ def test_api_delete(env, token_client):
)
assert resp.status_code == 204
assert not env[0].ticket_layouts.exists()
@pytest.mark.django_db
def test_renderer_batch_valid(env, token_client, position):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
{
"parts": [
{
"orderposition": position.pk,
},
{
"orderposition": position.pk,
"override_channel": "web",
},
{
"orderposition": position.pk,
"override_layout": env[1].pk,
},
]
},
format='json',
)
assert resp.status_code == 202
assert "download" in resp.data
resp = token_client.get("/" + resp.data["download"].split("/", 3)[3])
assert resp.status_code == 200
assert resp["Content-Type"] == "application/pdf"
@pytest.mark.django_db
def test_renderer_batch_invalid(env, token_client, position):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
{
"parts": [
{
"orderposition": -2,
},
]
},
format='json',
)
assert resp.status_code == 400
assert resp.data == {"parts": [{"orderposition": ["Invalid pk \"-2\" - object does not exist."]}]}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
{
"parts": [
{
"orderposition": position.pk,
"override_layout": -2,
},
]
},
format='json',
)
assert resp.status_code == 400
assert resp.data == {"parts": [{"override_layout": ["Invalid pk \"-2\" - object does not exist."]}]}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
{
"parts": [
{
"orderposition": position.pk,
"override_channel": "magic",
},
]
},
format='json',
)
assert resp.status_code == 400
assert resp.data == {"parts": [{"override_channel": ["Object with identifier=magic does not exist."]}]}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
{
"parts": [
{
"orderposition": position.pk,
}
] * 1002
},
format='json',
)
assert resp.status_code == 400
assert resp.data == {"parts": ["Please do not submit more than 1000 parts."]}