Compare commits

..

57 Commits

Author SHA1 Message Date
Lukas Bockstaller
b05de915bd Update src/pretix/control/templates/pretixcontrol/giftcards/payment.html
Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-03-13 13:58:54 +01:00
Lukas Bockstaller
384c83fffb Apply suggestion from @pajowu
Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-03-13 13:58:07 +01:00
Lukas Bockstaller
da97be5194 handle unknown gift cards 2026-03-10 18:16:19 +01:00
Lukas Bockstaller
a21e67a9ba make payment_provider and use_gift_cards exclusive 2026-03-10 17:34:50 +01:00
Lukas Bockstaller
353a08e094 reject duplicate gift card secrets 2026-03-10 16:59:51 +01:00
Lukas Bockstaller
0c05b532bf documentation lectoring 2026-03-10 10:59:36 +01:00
Lukas Bockstaller
9b4495c491 provide more context for failed transactions 2026-03-10 10:48:30 +01:00
Lukas Bockstaller
46d18dd489 docs 2026-03-10 10:21:42 +01:00
Lukas Bockstaller
b5cf122a2a let create_transactions() handle all the mailing 2026-03-09 18:06:04 +01:00
Lukas Bockstaller
4602db2bff styling 2026-03-09 17:24:44 +01:00
Lukas Bockstaller
ba8dbad733 implement giftcard payment via order create 2026-03-06 16:59:56 +01:00
Lukas Bockstaller
3b76ae48fd adds safeguard to prevent empty giftcard transactions on giftcards of value 0.00 2026-03-06 16:56:15 +01:00
Lukas Bockstaller
c07ba31307 API: add organizer-level orderpositions endpoint (#5848)
* initial implementation

* handle permissions

* split out organizer list endpoint

* remove left over empty lines

* revert import changes

* tidying up

* revert no longer needed test changes

* revert no longer needed test changes

* Apply suggestions from code review

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* add event to api response

* prefetch

* handle auth

* document event

* bump querycounts for prefetches

* Use existing Permission Denied Error Message

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-03-06 11:55:38 +01:00
Ruud Hendrickx
87b3e0c417 Translations: Update Dutch (Belgium)
Currently translated at 71.0% (4446 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
d3fd031639 Translations: Update Dutch (Belgium)
Currently translated at 69.6% (4355 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Renne Rocha
9253327334 Translations: Update Portuguese (Brazil)
Currently translated at 92.9% (5813 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
080b9cacaf Translations: Update Dutch (Belgium)
Currently translated at 63.6% (3982 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-es
9c2cc02df1 Translations: Update Spanish
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
fceae0a2fe Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-es
9fc3fdf751 Translations: Update French
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
André Almeida
04f79b7014 Translations: Update Portuguese (Brazil)
Currently translated at 92.8% (5811 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
9d0b9387e6 Translations: Update Dutch (Belgium)
Currently translated at 57.2% (3581 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial Pérez
b25e6f598d Translations: Update Galician
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/gl/

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial Pérez
e8e2648f7e Translations: Update Galician
Currently translated at 17.5% (1095 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
e0fac42225 Translations: Update Dutch (Belgium)
Currently translated at 53.1% (3326 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
3e9bc7675b Translations: Update Dutch (Belgium)
Currently translated at 50.7% (3176 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Hijiri Umemoto
1541033467 Translations: Update Japanese
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
6b8c3ef15c Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Alberto Ortega
135e07c183 Translations: Update Spanish
Currently translated at 99.9% (6256 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
fe97915b36 Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Hijiri Umemoto
233281cea4 Translations: Update Japanese
Currently translated at 99.9% (6255 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Renne Rocha
0300a44634 Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
449d930565 Translations: Update Dutch (Belgium)
Currently translated at 46.7% (2927 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial Pérez
49f49bd8a6 Translations: Update Galician
Currently translated at 16.7% (1048 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
e896704fe0 Translations: Update Dutch (Belgium)
Currently translated at 42.9% (2689 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial Pérez
cfee402a27 Translations: Update Galician
Currently translated at 16.3% (1026 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez Cerdeira
f8878e53a3 Translations: Update Galician
Currently translated at 16.3% (1026 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Renne Rocha
fd6a342bc6 Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Pedro Orlando
865433276e Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
André Almeida
f616f64f47 Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
26550887b7 Translations: Update Dutch (Belgium)
Currently translated at 30.7% (1924 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez Cerdeira
0f3de911b8 Translations: Update Galician
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/gl/

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez Cerdeira
b648390dbf Translations: Update Galician
Currently translated at 15.7% (986 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez Cerdeira
50fec0b31c Translations: Update Greek
Currently translated at 43.8% (2743 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
e44af04e43 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
André Almeida
276c3177f5 Translations: Update Portuguese (Brazil)
Currently translated at 89.7% (5616 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Pedro Orlando
27ac004a0b Translations: Update Portuguese (Brazil)
Currently translated at 89.7% (5616 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
André Almeida
6d517d4e8d Translations: Update Portuguese (Brazil)
Currently translated at 89.7% (5616 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx
d9c3deda8a Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-es
fe6add618a Translations: Update Spanish
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-es
3615a52cc4 Translations: Update French
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Kara Engelhardt
e3ae3b08bd Handle PlainHtmlAlternativeString in placeholder help text 2026-03-04 18:57:25 +02:00
Richard Schreiber
959e926a67 API: validate payment_info (#5944)
* API: validate payment_info

* improve dict-check

* Apply suggestions from code review

Co-authored-by: Raphael Michel <michel@pretix.eu>

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-03-02 12:28:47 +01:00
Raphael Michel
876ddf1321 Add a log entry on manual VAT ID validation (Z#23223874) (#5939) 2026-02-27 15:22:50 +01:00
Richard Schreiber
005b1d54d3 add missing licenseheaders 2026-02-27 09:09:27 +01:00
Ananya
2066471086 Fix #1907 – Obfuscate contact email addresses in public HTML (#5477)
* Include nix development enviornment

* Obfuscate contact email addresses in shop HTML and deanonymize via JavaScript

This change addresses #1907: "hide contact e-mail address in source code
of a shop".

- Contact email addresses rendered in public-facing templates are now
obfuscated in the HTML source (e.g., replacing "@" with "[at]" and "."
with "[dot]").
- A new JavaScript file is included in the relevant templates to
automatically rewrite and restore the email address for users after the
page loads.
- This approach helps protect email addresses from basic harvesting bots
and reduces spam, while keeping them accessible and user-friendly for
human visitors.
- The obfuscation and deanonymization logic is only applied to web
templates, not to emails sent via pretix.

This implementation follows the recommendations discussed in #1907,
using a standardized, maintainable approach that’s compatible with
pretix's asset pipeline and template structure.

* Undo nix development environment for merge into main

* convert complete mailto-link to HTML entities

* remove gitignore noise

* Update .gitignore

* fix gitignore noise

* Update .gitignore

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2026-02-27 08:50:33 +01:00
Richard Schreiber
a25bca7471 Fix static instance name in emails (Z#23224360) (#5914) 2026-02-25 13:19:53 +01:00
51 changed files with 5919 additions and 3786 deletions

View File

@@ -192,7 +192,7 @@ Cart position endpoints
* ``attendee_email`` (optional)
* ``subevent`` (optional)
* ``expires`` (optional)
* ``includes_tax`` (optional, **DEPRECATED**, do not use, will be removed)
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
* ``sales_channel`` (optional)
* ``voucher`` (optional, expect a voucher code)
* ``addons`` (optional, expect a list of nested objects of cart positions)

View File

@@ -117,6 +117,8 @@ 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.
===================================== ========================== =======================================================
@@ -156,6 +158,10 @@ plugin_data object Additional data
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
@@ -987,8 +993,6 @@ Creating orders
* does not support file upload questions
* does not support redeeming gift cards
* does not support or validate memberships
@@ -1066,7 +1070,6 @@ Creating orders
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
* ``add_to_reusable_medium`` (optional, causes the new ticket to be added to the given reusable medium, identified by its ID)
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
* ``answers``
@@ -1096,6 +1099,14 @@ 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
@@ -1720,6 +1731,56 @@ List of all order positions
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/orderpositions/
Returns a list of all order positions within all events of a given organizer (with sufficient access permissions).
The supported query parameters and output format of this endpoint are almost identical to those of the list endpoint
within an event.
The only changes are that responses also contain the ``event`` attribute in each result and that the 'pdf_data'
parameter is not supported.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/orderpositions/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
X-Page-Generated: 2017-12-01T10:00:00Z
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id:": 23442
"event": "sampleconf",
"order": "ABC12",
"positionid": 1,
"canceled": false,
"item": 1345,
...
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual positions
-----------------------------

View File

@@ -21,16 +21,12 @@ id integer Internal ID of
type string Type of medium, e.g. ``"barcode"``, ``"nfc_uid"`` or ``"nfc_mf0aes"``.
organizer string Organizer slug of the organizer who "owns" this medium.
identifier string Unique identifier of the medium. The format depends on the ``type``.
claim_token string Secret token to claim ownership of the medium (or ``null``)
label string Label to identify the medium, usually something human readable (or ``null``)
active boolean Whether this medium may be used.
created datetime Date of creation
updated datetime Date of last modification
expires datetime Expiry date (or ``null``)
customer string Identifier of a customer account this medium belongs to.
linked_orderpositions list of integer Internal IDs of tickets this medium is linked to.
linked_orderposition integer **DEPRECATED.** ID of the ticket the medium is linked to, if it is linked to
only one ticket. ``null``, if the medium is linked to none or multiple tickets.
linked_orderposition integer Internal ID of a ticket this medium is linked to.
linked_giftcard integer Internal ID of a gift card this medium is linked to.
info object Additional data, content depends on the ``type``. Consider
this internal to the system and don't use it for your own data.
@@ -43,14 +39,6 @@ Existing media types are:
- ``nfc_uid``
- ``nfc_mf0aes``
.. versionchanged:: 2025.11
The ``claim_token``, ``label``, ``linked_orderpositions`` attributes have been added, the ``linked_orderposition`` attribute has been
deprecated. Note: To maintain backwards compatibility ``linked_orderposition`` contains the internal ID of the linked order position
if the medium has exactly one order position in ``linked_orderpositions``.
Endpoints
---------
@@ -89,7 +77,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -105,13 +92,10 @@ Endpoints
:query string customer: Only show media linked to the given customer.
:query string created_since: Only show media created since a given date.
:query string updated_since: Only show media updated since a given date.
:query integer linked_orderpositions: Only show media linked to the given tickets. Note: you can pass multiple ticket IDs by passing
``linked_orderpositions`` multiple times. Any medium matching any linked orderposition will be returned.
:query integer linked_orderposition: Only show media linked to the given ticket.
:query integer linked_giftcard: Only show media linked to the given gift card.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderpositions"``,
``"linked_orderposition"`` (**DEPRECATED**), or ``"customer"``, the respective field will be shown
as a nested value instead of just an ID.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
@@ -150,7 +134,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -208,7 +191,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -216,9 +198,9 @@ Endpoints
}
:param organizer: The ``slug`` field of the organizer to look up a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderpositions`` each will have an attribute of the
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 201: no error
@@ -245,7 +227,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -270,7 +251,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -278,7 +258,7 @@ Endpoints
}
:param organizer: The ``slug`` field of the organizer to create a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
@@ -307,7 +287,7 @@ Endpoints
Content-Length: 94
{
"linked_orderpositions": [13, 29]
"linked_orderposition": 13
}
**Example response**:
@@ -328,8 +308,7 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [13, 29],
"linked_orderposition": None,
"linked_orderposition": 13,
"linked_giftcard": None,
"notes": None,
"info": {}
@@ -337,7 +316,7 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the medium to modify
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter

View File

@@ -65,11 +65,9 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
expand_nested = self.context['request'].query_params.getlist('expand')
if 'linked_giftcard' in expand_nested:
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
if 'linked_giftcard.owner_ticket' in expand_nested:
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
else:
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
@@ -78,27 +76,16 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
queryset=self.context['organizer'].issued_gift_cards.all()
)
# keep linked_orderposition (singular) for backwards compatibility, will be overwritten in self.validate
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
)
if 'linked_orderposition' in expand_nested or 'linked_orderpositions' in expand_nested:
self.fields['linked_orderpositions'] = NestedOrderPositionSerializer(
many=True,
read_only=True
)
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else:
self.fields['linked_orderpositions'] = serializers.PrimaryKeyRelatedField(
many=True,
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
)
if 'customer' in expand_nested:
if 'customer' in self.context['request'].query_params.getlist('expand'):
self.fields['customer'] = CustomerSerializer(read_only=True)
else:
self.fields['customer'] = serializers.SlugRelatedField(
@@ -110,21 +97,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
def validate(self, data):
data = super().validate(data)
if 'linked_orderposition' in data:
linked_orderposition = data['linked_orderposition']
# backwards-compatibility
if 'linked_orderpositions' in data:
raise ValidationError({
'linked_orderposition': _('You cannot use linked_orderposition and linked_orderpositions at the same time.')
})
if self.instance and self.instance.linked_orderpositions.count() > 1:
raise ValidationError({
'linked_orderposition': _('There are more than one linked_orderposition. You need to use linked_orderpositions.')
})
data['linked_orderpositions'] = [linked_orderposition] if linked_orderposition else []
del data['linked_orderposition']
if 'type' in data and 'identifier' in data:
qs = self.context['organizer'].reusable_media.filter(
identifier=data['identifier'], type=data['type']
@@ -137,14 +109,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
return data
def to_representation(self, obj):
r = super(ReusableMediaSerializer, self).to_representation(obj)
ops = r.get('linked_orderpositions')
if len(ops) < 2:
# add linked_orderposition (singular) for backwards compatibility
r['linked_orderposition'] = ops[0] if ops else None
return r
class Meta:
model = ReusableMedium
fields = (
@@ -154,12 +118,10 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
'updated',
'type',
'identifier',
'claim_token',
'label',
'active',
'expires',
'customer',
'linked_orderpositions',
'linked_orderposition',
'linked_giftcard',
'info',
'notes',

View File

@@ -19,6 +19,7 @@
# 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
import os
from collections import Counter, defaultdict
@@ -52,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, Invoice, InvoiceAddress,
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
Voucher,
@@ -61,6 +62,7 @@ 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
@@ -636,6 +638,14 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
return entry
class OrganizerOrderPositionSerializer(OrderPositionSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
class Meta(OrderPositionSerializer.Meta):
fields = OrderPositionSerializer.Meta.fields + ('event',)
read_only_fields = OrderPositionSerializer.Meta.read_only_fields + ('event',)
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
return instance.require_checkin_attention
@@ -1029,15 +1039,13 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
add_to_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
'requested_valid_from', 'use_reusable_medium', 'add_to_reusable_medium', 'discount')
'requested_valid_from', 'use_reusable_medium', 'discount')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1049,8 +1057,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
with scopes_disabled():
if 'use_reusable_medium' in self.fields:
self.fields['use_reusable_medium'].queryset = ReusableMedium.objects.all()
if 'add_to_reusable_medium' in self.fields:
self.fields['add_to_reusable_medium'].queryset = ReusableMedium.objects.all()
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -1066,9 +1072,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
)
return m
def validate_add_to_reusable_medium(self, m):
return self.validate_use_reusable_medium(m)
def validate_item(self, item):
if item.event != self.context['event']:
raise ValidationError(
@@ -1142,13 +1145,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
)
if 'use_reusable_medium' in data and 'add_to_reusable_medium' in data:
raise ValidationError({
'use_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
'add_to_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
})
return data
@@ -1205,6 +1201,7 @@ 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)
@@ -1220,7 +1217,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')
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode', 'use_gift_cards')
def validate_payment_provider(self, pp):
if pp is None:
@@ -1229,6 +1226,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The given payment provider is not known.')
return pp
def validate_payment_info(self, info):
if info:
try:
obj = json.loads(info)
except ValueError:
raise ValidationError('payment_info must be valid JSON.')
if not isinstance(obj, dict):
# only objects are allowed
raise ValidationError('payment_info must be a JSON object.')
return info
def validate_expires(self, expires):
if expires < now():
raise ValidationError('Expiration date must be in the future.')
@@ -1303,6 +1312,14 @@ 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")
@@ -1564,7 +1581,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in ('answers', '_quotas', 'use_reusable_medium', 'add_to_reusable_medium')})
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
if simulate:
pos.order = order._wrapped
else:
@@ -1638,7 +1655,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
add_to_reusable_medium = pos_data.pop('add_to_reusable_medium', None)
pos = pos_data['__instance']
pos._calculate_tax(invoice_address=ia)
@@ -1680,14 +1696,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answ.options.add(*options)
if use_reusable_medium:
for op in use_reusable_medium.linked_orderpositions.all():
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.removed',
data={
'linked_orderposition': op.pk,
}
)
use_reusable_medium.linked_orderpositions.set([pos])
use_reusable_medium.linked_orderposition = pos
use_reusable_medium.save(update_fields=['linked_orderposition'])
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.changed',
data={
@@ -1695,15 +1705,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
'linked_orderposition': pos.pk,
}
)
if add_to_reusable_medium:
add_to_reusable_medium.linked_orderpositions.add(pos)
add_to_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.added',
data={
'by_order': order.code,
'linked_orderposition': pos.pk,
}
)
if not simulate:
for cp in delete_cps:
@@ -1803,6 +1804,45 @@ 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

@@ -365,9 +365,10 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,

View File

@@ -67,6 +67,7 @@ orga_router.register(r'invoices', order.InvoiceViewSet)
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
@@ -83,7 +84,7 @@ event_router.register(r'discounts', discount.DiscountViewSet)
event_router.register(r'quotas', item.QuotaViewSet)
event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.EventOrderViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'orderpositions', order.EventOrderPositionViewSet)
event_router.register(r'transactions', order.TransactionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')

View File

@@ -520,13 +520,11 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
# with respecting the force option), or it's a reusable medium (-> proceed with that)
if not op_candidates:
try:
media = ReusableMedium.objects.active().annotate(
has_linked_orderpositions=Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk')))
).get(
media = ReusableMedium.objects.select_related('linked_orderposition').active().get(
organizer_id=checkinlists[0].event.organizer_id,
type=source_type,
identifier=raw_barcode,
has_linked_orderpositions=True,
linked_orderposition__isnull=False,
)
raw_barcode_for_checkin = raw_barcode
except ReusableMedium.DoesNotExist:
@@ -629,8 +627,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
else:
linked_event_ids = media.linked_orderpositions.values_list("order__event_id", flat=True).order_by().distinct()
if not any(event_id in list_by_event for event_id in linked_event_ids):
if media.linked_orderposition.order.event_id not in list_by_event:
# Medium exists but connected ticket is for the wrong event
if not simulate:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
@@ -656,34 +653,21 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'checkin_texts': [],
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
op_candidates = []
for op in media.linked_orderpositions.all().select_related("order"):
op_candidates.append(op)
if list_by_event[op.order.event_id].addon_match:
op_candidates += list(op.addons.all())
op_candidates = [media.linked_orderposition]
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
op_candidates += list(media.linked_orderposition.addons.all())
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
# which add-on has the right product.
if len(op_candidates) > 1:
# only check addons if at most one non-addon-op is in op_candidates
# otherwise it is likely a medium linked to multiple orderpositions, which we need to filter based on validity
if len([op for op in op_candidates if not op.addon_to]) <= 1:
op_candidates_matching_product = [
op for op in op_candidates
if (
(list_by_event[op.order.event_id].addon_match or op.secret == raw_barcode or legacy_url_support) and
(list_by_event[op.order.event_id].all_products or op.item_id in {i.pk for i in list_by_event[op.order.event_id].limit_products.all()})
)
]
else:
op_candidates_matching_product = [
op for op in op_candidates
if (
(not op.valid_from or op.valid_from < now()) and
(not op.valid_until or op.valid_until > now())
)
]
op_candidates_matching_product = [
op for op in op_candidates
if (
(list_by_event[op.order.event_id].addon_match or op.secret == raw_barcode or legacy_url_support) and
(list_by_event[op.order.event_id].all_products or op.item_id in {i.pk for i in list_by_event[op.order.event_id].limit_products.all()})
)
]
if len(op_candidates_matching_product) == 0:
# None of the found add-ons has the correct product, too bad! We could just error out here, but

View File

@@ -53,12 +53,10 @@ with scopes_disabled():
customer = django_filters.CharFilter(field_name='customer__identifier')
updated_since = django_filters.IsoDateTimeFilter(field_name='updated', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
# backwards-compatible
linked_orderposition = django_filters.NumberFilter(field_name='linked_orderpositions__id')
class Meta:
model = ReusableMedium
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderpositions', 'linked_giftcard']
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderposition', 'linked_giftcard']
class ReusableMediaViewSet(viewsets.ModelViewSet):
@@ -77,7 +75,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
).order_by().values('card').annotate(s=Sum('value')).values('s')
return self.request.organizer.reusable_media.prefetch_related(
Prefetch(
'linked_orderpositions',
'linked_orderposition',
queryset=OrderPosition.objects.select_related(
'order', 'order__event', 'order__event__organizer', 'seat',
).prefetch_related(
@@ -157,6 +155,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
)
m.linked_orderposition = None # not relevant for cross-organizer
m.customer = None # not relevant for cross-organizer
s = self.get_serializer(m)
return Response({"result": s.data})

View File

@@ -57,9 +57,10 @@ from pretix.api.serializers.order import (
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
SimulatedOrderSerializer, TransactionSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerOrderPositionSerializer,
OrganizerTransactionSerializer, PriceCalcSerializer, PrintLogSerializer,
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
TransactionSerializer,
)
from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer,
@@ -193,7 +194,7 @@ with scopes_disabled():
)
).values('id')
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderpositions__order_id', flat=True)
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderposition__order_id', flat=True)
mainq = (
code
@@ -1030,7 +1031,7 @@ with scopes_disabled():
search = django_filters.CharFilter(method='search_qs')
def search_qs(self, queryset, name, value):
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderpositions', flat=True)
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderposition', flat=True)
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)
@@ -1065,8 +1066,7 @@ with scopes_disabled():
}
class OrderPositionViewSet(viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
class OrderPositionViewSetMixin:
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('order__datetime', 'positionid')
@@ -1087,8 +1087,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
ctx['pdf_data'] = False
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
return ctx
@@ -1097,9 +1096,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
qs = OrderPosition.all
else:
qs = OrderPosition.objects
qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
qs = qs.filter(order__event__organizer=self.request.organizer)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(self.request, 'event', None):
prefetch_related_objects([self.request.organizer], 'meta_properties')
prefetch_related_objects(
[self.request.event],
@@ -1154,9 +1152,9 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
'answers', 'answers__options', 'answers__question', 'order__event', 'order__event__organizer'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
'item', 'order', 'seat'
)
return qs
@@ -1168,6 +1166,45 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
return prov
raise NotFound('Unknown output provider.')
class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrganizerOrderPositionSerializer
def get_queryset(self):
qs = super().get_queryset()
perm = self.permission if self.request.method in SAFE_METHODS else self.write_permission
if isinstance(self.request.auth, (TeamAPIToken, Device)):
auth_obj = self.request.auth
elif self.request.user.is_authenticated:
auth_obj = self.request.user
else:
raise PermissionDenied("Unknown authentication scheme")
qs = qs.filter(
order__event__in=auth_obj.get_events_with_permission(perm, request=self.request).filter(
organizer=self.request.organizer
)
)
return qs
class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
return ctx
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(order__event=self.request.event)
return qs
@action(detail=True, methods=['POST'], url_name='price_calc')
def price_calc(self, request, *args, **kwargs):
"""

View File

@@ -20,13 +20,12 @@
# <https://www.gnu.org/licenses/>.
#
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
from ..exporter import ListExporter, OrganizerLevelExportMixin
from ..models import OrderPosition, ReusableMedium
from ..models import ReusableMedium
from ..signals import register_multievent_data_exporters
@@ -41,9 +40,7 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
media = ReusableMedium.objects.filter(
organizer=self.organizer,
).select_related(
'customer', 'linked_giftcard',
).prefetch_related(
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order"))
'customer', 'linked_orderposition', 'linked_giftcard',
).order_by('created')
headers = [
@@ -61,16 +58,17 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
yield self.ProgressSetTotal(total=media.count())
for medium in media.iterator(chunk_size=1000):
yield [
row = [
medium.type,
medium.identifier,
_('Yes') if medium.active else _('No'),
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
medium.customer.identifier if medium.customer_id else '',
', '.join([f"{op.order.code}-{op.positionid}" for op in medium.linked_orderpositions.all()]),
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
medium.notes,
]
yield row
def get_filename(self):
return f'{self.organizer.slug}_media'

View File

@@ -42,6 +42,8 @@ from django.utils.html import escape
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _
from pretix.helpers.format import PlainHtmlAlternativeString
def replace_arabic_numbers(inp):
if not isinstance(inp, str):
@@ -61,11 +63,18 @@ def replace_arabic_numbers(inp):
return inp.translate(table)
def format_placeholder_help_text(placeholder_name, sample_value):
if isinstance(sample_value, PlainHtmlAlternativeString):
sample_value = sample_value.plain
title = (_("Sample: %s") % sample_value) if sample_value else ""
return ('<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(title), escape(placeholder_name)))
def format_placeholders_help_text(placeholders, event=None):
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
placeholders.sort(key=lambda x: x[0])
phs = [
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
format_placeholder_help_text(k, v)
for k, v in placeholders
]
return _('Available placeholders: {list}').format(

View File

@@ -1,35 +0,0 @@
# Generated by Django 4.2.26 on 2025-11-24 11:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0297_outgoingmail"),
]
operations = [
migrations.AddField(
model_name="reusablemedium",
name="claim_token",
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name="reusablemedium",
name="label",
field=models.CharField(max_length=200, null=True),
),
# use temporary related_name "linked_mediums" for ManyToManyField, so we can migrate existing data
migrations.AddField(
model_name="reusablemedium",
name="linked_orderpositions",
field=models.ManyToManyField(
related_name="linked_mediums", to="pretixbase.orderposition"
),
),
migrations.RunSQL(
sql="INSERT INTO pretixbase_reusablemedium_linked_orderpositions (reusablemedium_id, orderposition_id) SELECT id, linked_orderposition_id FROM pretixbase_reusablemedium WHERE linked_orderposition_id IS NOT NULL;",
reverse_sql="DELETE FROM pretixbase_reusablemedium_linked_orderpositions;",
),
]

View File

@@ -1,44 +0,0 @@
# Generated by Django 4.2.26 on 2025-11-24 11:32
from django.db import migrations, models
def reverse(apps, schema_editor):
ReusableMedium = apps.get_model('pretixbase', 'ReusableMedium')
qs = ReusableMedium.linked_orderpositions.through.objects
objs = []
# get last added orderposition from linked_orderpositions
for rm_id, op_id in qs.filter(id__in=qs.values("reusablemedium_id").annotate(max_id=models.Max('id')).values('max_id')).values_list("reusablemedium_id", "orderposition_id"):
obj = ReusableMedium(
id=rm_id,
linked_orderposition_id=op_id,
)
objs.append(obj)
ReusableMedium.objects.bulk_update(objs, ['linked_orderposition_id'])
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0298_add_reusablemedium_label"),
]
operations = [
# according to the docs, UPDATE FROM should run similarly on sqlite and postgres, but I could not get it to work
# so roll back the data migration with code before deleting data from through-table in 0297
migrations.RunPython(migrations.RunPython.noop, reverse),
migrations.RemoveField(
model_name="reusablemedium",
name="linked_orderposition",
),
# change related_name for new ManyToManyField to previously used linked_media
migrations.AlterField(
model_name="reusablemedium",
name="linked_orderpositions",
field=models.ManyToManyField(
related_name="linked_media", to="pretixbase.orderposition"
),
),
]

View File

@@ -346,7 +346,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{
'user': self,
'messages': msg,
'url': build_absolute_uri('control:user.settings')
'url': build_absolute_uri('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -391,6 +392,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'user': self,
'reason': msg,
'code': code,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -430,6 +432,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
mail(
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'url': (build_absolute_uri('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))

View File

@@ -72,16 +72,6 @@ class ReusableMedium(LoggedModel):
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Identifier'),
)
claim_token = models.CharField(
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Claim token'),
null=True, blank=True
)
label = models.CharField(
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Label'),
null=True, blank=True
)
active = models.BooleanField(
verbose_name=_('Active'),
@@ -99,14 +89,12 @@ class ReusableMedium(LoggedModel):
on_delete=models.SET_NULL,
verbose_name=_('Customer account'),
)
linked_orderpositions = models.ManyToManyField(
linked_orderposition = models.ForeignKey(
OrderPosition,
null=True, blank=True,
related_name='linked_media',
verbose_name=_('Linked tickets'),
help_text=_(
'If you link to more than one ticket, make sure there is no overlap in validity. '
'If multiple tickets are valid at once, this will lead to failed check-ins.'
)
on_delete=models.SET_NULL,
verbose_name=_('Linked ticket'),
)
linked_giftcard = models.ForeignKey(
GiftCard,

View File

@@ -1525,16 +1525,26 @@ class GiftCardPayment(BasePaymentProvider):
def payment_control_render(self, request, payment) -> str:
from .models import GiftCard
if 'gift_card' in payment.info_data:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
if any(key in payment.info_data for key in ('gift_card', 'error')):
template = get_template('pretixcontrol/giftcards/payment.html')
ctx = {
'request': request,
'event': self.event,
'gc': gc,
**({'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 {})
}
return template.render(ctx)
try:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
ctx = {
'gc': gc,
}
except GiftCard.DoesNotExist:
pass
finally:
return template.render(ctx)
def payment_control_render_short(self, payment: OrderPayment) -> str:
d = payment.info_data
@@ -1549,12 +1559,16 @@ class GiftCardPayment(BasePaymentProvider):
try:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
except GiftCard.DoesNotExist:
return {}
return {
**({'error': payment.info_data[
'error']} if 'error' in payment.info_data else {})
}
return {
'gift_card': {
'id': gc.pk,
'secret': gc.secret,
'organizer': gc.issuer.slug
'organizer': gc.issuer.slug,
** ({'error': payment.info_data['error']} if 'error' in payment.info_data else {})
}
}
@@ -1626,6 +1640,8 @@ 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:
@@ -1655,7 +1671,7 @@ class GiftCardPayment(BasePaymentProvider):
}
)
except PaymentException as e:
payment.fail(info={'error': str(e)})
payment.fail(info={**payment.info_data, 'error': str(e)}, send_mail=not is_early_special_case)
raise e
def payment_is_valid_session(self, request: HttpRequest) -> bool:

View File

@@ -3495,8 +3495,8 @@ def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
identifier=mt.generate_identifier(sender.organizer),
active=True,
customer=order.customer,
linked_orderposition=p,
)
rm.linked_orderpositions.add(p)
rm.log_action(
'pretix.reusable_medium.created',
data={

View File

@@ -176,6 +176,7 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),

View File

@@ -13,5 +13,5 @@ Start time: {{ start_time }} (new data added after this time might not have been
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -0,0 +1,34 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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 django import template
from django.utils.html import mark_safe
register = template.Library()
@register.filter("anon_email")
def anon_email(value):
"""Replaces @ with [at] and . with [dot] for anonymization."""
if not isinstance(value, str):
return value
value = value.replace("@", "[at]").replace(".", "[dot]")
return mark_safe(''.join(['&#{0};'.format(ord(char)) for char in value]))

View File

@@ -1744,7 +1744,7 @@ class ReusableMediaFilterForm(FilterForm):
Q(identifier__icontains=query)
| Q(customer__identifier__icontains=query)
| Q(customer__external_identifier__istartswith=query)
| Q(linked_orderpositions__order__code__icontains=query)
| Q(linked_orderposition__order__code__icontains=query)
| Q(linked_giftcard__secret__icontains=query)
)

View File

@@ -83,7 +83,7 @@ from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms.event import (
SafeEventMultipleChoiceField, multimail_validate,
)
from pretix.control.forms.widgets import Select2, Select2Multiple
from pretix.control.forms.widgets import Select2
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -246,15 +246,6 @@ class SafeOrderPositionChoiceField(forms.ModelChoiceField):
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
class SafeOrderPositionMultipleChoiceField(forms.ModelMultipleChoiceField):
def __init__(self, queryset, **kwargs):
queryset = queryset.model.all.none()
super().__init__(queryset, **kwargs)
def label_from_instance(self, op):
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
class EventMetaPropertyForm(I18nModelForm):
class Meta:
model = EventMetaProperty
@@ -854,12 +845,12 @@ class ReusableMediumUpdateForm(forms.ModelForm):
class Meta:
model = ReusableMedium
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderpositions', 'notes']
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderposition', 'notes']
field_classes = {
'expires': SplitDateTimeField,
'customer': SafeModelChoiceField,
'linked_giftcard': SafeModelChoiceField,
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
'linked_orderposition': SafeOrderPositionChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,
@@ -869,8 +860,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
super().__init__(*args, **kwargs)
organizer = self.instance.organizer
self.fields['linked_orderpositions'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
self.fields['linked_orderpositions'].widget = Select2Multiple(
self.fields['linked_orderposition'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
self.fields['linked_orderposition'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
@@ -878,8 +869,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
}),
}
)
self.fields['linked_orderpositions'].widget.choices = self.fields['linked_orderpositions'].choices
self.fields['linked_orderpositions'].required = False
self.fields['linked_orderposition'].widget.choices = self.fields['linked_orderposition'].choices
self.fields['linked_orderposition'].required = False
self.fields['linked_giftcard'].queryset = organizer.issued_gift_cards.all()
self.fields['linked_giftcard'].widget = Select2(
@@ -933,12 +924,12 @@ class ReusableMediumCreateForm(ReusableMediumUpdateForm):
class Meta:
model = ReusableMedium
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderpositions', 'linked_giftcard', 'customer', 'notes']
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderposition', 'linked_giftcard', 'customer', 'notes']
field_classes = {
'expires': SplitDateTimeField,
'customer': SafeModelChoiceField,
'linked_giftcard': SafeModelChoiceField,
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
'linked_orderposition': SafeOrderPositionChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,

View File

@@ -518,6 +518,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'The order requires approval before it can continue to be processed.'),
'pretix.event.order.approved': _('The order has been approved.'),
'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'),
'pretix.event.order.vatid.validated': _('The customer VAT ID has been verified.'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
'to "{new_email}".'),
'pretix.event.order.contact.confirmed': _(
@@ -741,8 +742,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.reusable_medium.created': _('The reusable medium has been created.'),
'pretix.reusable_medium.created.auto': _('The reusable medium has been created automatically.'),
'pretix.reusable_medium.changed': _('The reusable medium has been changed.'),
'pretix.reusable_medium.linked_orderposition.added': _('A new ticket has been added to the medium.'),
'pretix.reusable_medium.linked_orderposition.removed': _('A ticket has been removed from the medium.'),
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'),
'pretix.email.error': _('Sending of an email has failed.'),

View File

@@ -9,5 +9,5 @@ Please do never give this code to another person. Our support team will never as
If this code was not requested by you, please contact us immediately.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -5,5 +5,5 @@ you requested a new password. Please go to the following page to reset your pass
{{ url }}
Best regards,
Your pretix team
{% endblocktrans %}
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to a team on pretix, a platform to perform event
you have been invited to a team on {{ instance }}, a platform to perform event
ticket sales.
Organizer: {{ organizer }}
@@ -13,5 +13,5 @@ If you do not want to join, you can safely ignore or delete this email.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
this is to inform you that the account information of your pretix account has been
this is to inform you that the account information of your {{ instance }} account has been
changed. In particular, the following changes have been performed:
{{ messages }}
@@ -12,5 +12,5 @@ You can review and change your account settings here:
{{ url }}
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -3,16 +3,26 @@
<dl class="dl-horizontal">
<dt>{% trans "Gift card code" %}</dt>
<dd>
<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>
{% 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 }}
{% endif %}
</dd>
<dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd>
{% if gc %}
<dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd>
{% endif %}
{% if error %}
<dt>{% trans "Error" %}</dt>
<dd>{{ error }}</dd>
{% endif %}
</dl>

View File

@@ -54,8 +54,8 @@
<a href="?{% url_replace request 'ordering' 'identifier' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>{% trans "Media type" context "reusable_media" %}
<a href="?{% url_replace request 'ordering' '-type' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'type' %}"><i class="fa fa-caret-up"></i></a></th>
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Connections" context "reusable_media" %}</th>
<th></th>
</tr>
@@ -82,13 +82,13 @@
</a>
</span>
{% endif %}
{% for op in m.linked_orderpositions.all %}
{% if m.linked_orderposition %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=op.order.event.slug organizer=request.organizer.slug code=op.order.code %}">
{{ op.order.code }}</a>-{{ op.positionid }}
<a href="{% url "control:event.order" event=m.linked_orderposition.order.event.slug organizer=request.organizer.slug code=m.linked_orderposition.order.code %}">
{{ m.linked_orderposition.order.code }}</a>-{{ m.linked_orderposition.positionid }}
</span>
{% endfor %}
{% endif %}
{% if m.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>

View File

@@ -28,19 +28,7 @@
<dt>{% trans "Media type" context "reusable_media" %}</dt>
<dd>{{ medium.get_type_display }}</dd>
<dt>{% trans "Identifier" context "reusable_media" %}</dt>
<dd>
<code id="medium_identifier">{{ medium.identifier }}</code>
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#medium_identifier">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
</button>
{% if medium.type == "barcode" %}
<button type="button" class="btn btn-default btn-xs js-only" data-toggle="qrcode" data-qrcode="{{ medium.identifier }}">
<i class="fa fa-qrcode" aria-hidden="true"></i>
<span class="sr-only">{% trans "Create QR code" %}</span>
</button>
{% endif %}
</dd>
<dd><code>{{ medium.identifier }}</code></dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% if not medium.active %}
@@ -61,13 +49,13 @@
</a>
</span>
{% endif %}
{% for op in medium.linked_orderpositions.all %}
{% if medium.linked_orderposition %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=op.order.event.slug organizer=request.organizer.slug code=op.order.code %}">
{{ op.order.code }}</a>-{{ op.positionid }}
</span>
{% endfor %}
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=medium.linked_orderposition.order.event.slug organizer=request.organizer.slug code=medium.linked_orderposition.order.code %}">
{{ medium.linked_orderposition.order.code }}</a>-{{ medium.linked_orderposition.positionid }}
</span>
{% endif %}
{% if medium.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>

View File

@@ -1641,9 +1641,17 @@ class OrderCheckVATID(OrderView):
try:
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
with transaction.atomic():
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
self.order.log_action(
'pretix.event.order.vatid.validated',
data={
'vat_id': normalized_id,
},
user=self.request.user,
)
except VATIDFinalError as e:
messages.error(self.request, e.message)
except VATIDTemporaryError:

View File

@@ -1039,9 +1039,10 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.request.organizer.name,
'team': instance.team.name,
@@ -3338,10 +3339,8 @@ class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_queryset(self):
qs = self.request.organizer.reusable_media.select_related(
'customer',
'linked_giftcard',
).prefetch_related(
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order", "order__event"))
'customer', 'linked_orderposition', 'linked_orderposition__order', 'linked_orderposition__order__event',
'linked_giftcard'
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
@@ -3389,14 +3388,10 @@ class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
@transaction.atomic
def form_valid(self, form):
r = super().form_valid(form)
data = {
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, data={
k: getattr(form.instance, k)
for k in form.changed_data
}
if "linked_orderpositions" in data:
data["linked_orderpositions"] = data["linked_orderpositions"].values_list("pk", flat=True)
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, data=data)
})
messages.success(self.request, _('Your changes have been saved.'))
return r
@@ -3422,13 +3417,10 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
@transaction.atomic
def form_valid(self, form):
if form.has_changed():
data = {
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data={
k: getattr(self.object, k)
for k in form.changed_data
}
if "linked_orderpositions" in data:
data["linked_orderpositions"] = data["linked_orderpositions"].values_list("pk", flat=True)
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data=data)
})
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)

View File

@@ -8,16 +8,16 @@ 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: 2025-02-14 21:00+0000\n"
"Last-Translator: deborahfoell <deborah.foell@om.org>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix/el/"
">\n"
"PO-Revision-Date: 2026-02-25 23:00+0000\n"
"Last-Translator: David Ibáñez Cerdeira <dibanez@gmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix/el/>"
"\n"
"Language: el\n"
"MIME-Version: 1.0\n"
"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.9.2\n"
"X-Generator: Weblate 5.16\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -20467,7 +20467,7 @@ msgstr "Ορίστε νέο κωδικό πρόσβασης"
#: pretix/presale/templates/pretixpresale/organizers/customer_password.html:25
#: pretix/presale/templates/pretixpresale/organizers/customer_setpassword.html:25
msgid "Save"
msgstr "Αποθηκεύση"
msgstr "gardar"
#: pretix/control/templates/pretixcontrol/auth/register.html:7
msgid "Create a new account"

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-02-21 18:00+0000\n"
"PO-Revision-Date: 2026-03-03 20:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\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\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -17058,28 +17058,20 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "Debe especificar tantas butacas como vales de compra."
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "Por favor seleccione una butaca válida."
msgstr "Seleccione una opción válida."
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "Productos activos"
msgstr "Solo incluir productos activos."
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "Ya existe un vale de compra con este código."
msgstr "Ya se ha enviado un vale para esta entrada en la lista de espera."
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "El producto seleccionado ha sido desactivado."
msgstr "El producto seleccionado no esactivo."
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -17729,7 +17721,7 @@ msgstr ""
#: pretix/control/logdisplay.py:589
msgid "The voucher has been changed."
msgstr "EL vale de compra ha sido cambiado."
msgstr "El vale de compra ha sido cambiado."
#: pretix/control/logdisplay.py:590
msgid "The voucher has been deleted."
@@ -18643,7 +18635,7 @@ msgstr "Entradas"
#: pretix/control/templates/pretixcontrol/order/index.html:764
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:457
msgid "Taxes"
msgstr "gravámenes"
msgstr "Impuestos"
#: pretix/control/navigation.py:97
msgid "Invoicing"
@@ -18883,6 +18875,10 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"Parece que el navegador no acepta nuestras cookies y es necesario iniciar "
"sesión repetidamente. Por favor, compruebe si el navegador está configurado "
"para bloquear cookies o elimine todas las cookies existentes y vuelva a "
"intentarlo."
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -18994,8 +18990,9 @@ msgid ""
"This application has <strong>not</strong> been reviewed by the pretix team. "
"Granting access to your pretix account happens at your own risk."
msgstr ""
"Esta aplicación<strong>no</strong> ha sido revisada por el equipo de pretix. "
"La concesión del acceso a su cuenta pretix se realiza bajo su propio riesgo."
"Esta aplicación <strong>no</strong> ha sido revisada por el equipo de "
"pretix. La concesión del acceso a su cuenta pretix se realiza bajo su propio "
"riesgo."
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html:54
msgid "Error:"
@@ -19129,8 +19126,8 @@ msgid ""
"We've detected that you are using <strong>Microsoft Internet Explorer</"
"strong>."
msgstr ""
"Hemos detectado que estás usando <strong> Microsoft Internet Explorer </"
"strong>."
"Hemos detectado que estás usando <strong>Microsoft Internet Explorer</strong>"
"."
#: pretix/control/templates/pretixcontrol/base.html:332
#: pretix/presale/templates/pretixpresale/base.html:54
@@ -19169,7 +19166,7 @@ msgid ""
"people from actually buying tickets."
msgstr ""
"Tu evento contiene <strong>pedidos de modo de prueba</strong> a pesar de que "
"<strong> el modo de prueba se ha deshabilitado</strong>. Deberías eliminar "
"<strong>el modo de prueba se ha deshabilitado</strong>. Deberías eliminar "
"estes pedidos para asegurarte que no se muestren en tus reportes "
"estadísticos y bloquear la compra de entradas a las personas."
@@ -20764,7 +20761,7 @@ msgid ""
"duplicate payment attempts. You should review the cases and consider "
"refunding the overpaid amount to the user."
msgstr ""
"Este evento contiene <strong> pedidos pagados en exceso</strong>, por "
"Este evento contiene <strong>pedidos pagados en exceso</strong>, por "
"ejemplo, debido a que hay intentos de pago duplicados. Debe revisar los "
"casos y considerar la devolución de la cantidad pagada en exceso al usuario."
@@ -20777,7 +20774,7 @@ msgid ""
"This event contains <strong>pending refunds</strong> that you should take "
"care of."
msgstr ""
"Este evento contiene <strong>devoluciones pendientes </strong> sobre las que "
"Este evento contiene <strong>devoluciones pendientes</strong> sobre las que "
"debe prestar atención."
#: pretix/control/templates/pretixcontrol/event/index.html:50
@@ -20815,7 +20812,7 @@ msgid ""
"arrived. You should review the cases and consider either refunding the "
"customer or creating more space."
msgstr ""
"Este evento contiene <strong> pedidos completamente pagados</strong> que no "
"Este evento contiene <strong>pedidos completamente pagados</strong> que no "
"están marcadas como pagados, probablemente porque no se dejo ningún cupo al "
"momento que llegó el pago. Debería revisar estos casos y considerar, "
"devolver el dinero o crear más espacio."
@@ -21479,7 +21476,7 @@ msgid ""
"as examples, you can add more in the \"Settings\" part of your event."
msgstr ""
"pretix soporta un <a href=\"https://pretix.eu/about/en/features/payment\" "
"target=\"_blank\">amplio rango de proveedores de pago </a> permitiéndole "
"target=\"_blank\">amplio rango de proveedores de pago</a> permitiéndole "
"elegir los métodos de pago que mejor se adapten a su flujo de trabajo. Aquí "
"hay sólo dos de ellos a modo de ejemplo, puede añadir más en la parte "
"\"Configuración\" de su evento."
@@ -23891,8 +23888,8 @@ msgid ""
"Do you really want to delete this order? <strong>You really cannot revert "
"this action and we can't either.</strong>"
msgstr ""
"¿Realmente quieres eliminar este pedido? <strong> No puedes revertir esta "
"acción y tampoco nosotros. </strong>"
"¿Realmente quieres eliminar este pedido? <strong>No puedes revertir esta "
"acción y tampoco nosotros.</strong>"
#: pretix/control/templates/pretixcontrol/order/delete.html:25
msgid "Yes, delete order"
@@ -24516,9 +24513,8 @@ msgid ""
msgstr ""
"Hemos recibido la notificación de que <strong>%(amount)s</strong> ha sido "
"devuelto a través de <strong>%(method)s</strong>. Si este reembolso está "
"procesado, el pedido habrá sido pagado con un importe inferior "
"en<strong>%(pending)s</strong>. El total del pedido es <strong>%(total)s</"
"strong>."
"procesado, el pedido habrá sido pagado con un importe inferior en <strong>%"
"(pending)s</strong>. El total del pedido es <strong>%(total)s</strong>."
#: pretix/control/templates/pretixcontrol/order/refund_process.html:30
msgid "Since the order is already canceled, this will not affect its state."
@@ -27262,7 +27258,7 @@ msgid ""
"Using this option will <strong>delete all current quotas</strong> from "
"<strong>all selected dates</strong>."
msgstr ""
"Al utilizar esta opción se <strong> eliminarán todas las cuotas actuales </"
"Al utilizar esta opción se <strong>eliminarán todas las cuotas actuales</"
"strong> de <strong>todas las fechas seleccionadas</strong>."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:277
@@ -27998,7 +27994,7 @@ msgstr "Borrar vale de compra"
msgid ""
"Are you sure you want to delete the voucher <strong>%(voucher)s</strong>?"
msgstr ""
"¿Está seguro de que desea borrar el vale de compra<strong>%(voucher)s</"
"¿Está seguro de que desea borrar el vale de compra <strong>%(voucher)s</"
"strong>?"
#: pretix/control/templates/pretixcontrol/vouchers/delete_bulk.html:4
@@ -28191,10 +28187,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "Ingreso"
msgstr "Editar entrada"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -28256,7 +28250,7 @@ msgid ""
"quota is available) or you can press the big button below this text to send "
"out as many vouchers as currently possible to the persons who waited longest."
msgstr ""
"Ha configurado que los vales de compra se enviarán <strong>no </strong> "
"Ha configurado que los vales de compra se enviarán <strong>no</strong> "
"automáticamente. Puede enviarlos uno por uno en un orden de su elección "
"haciendo clic en los botones junto a una línea de esta tabla (si hay "
"suficiente cuota disponible) o puede presionar el botón grande debajo de "
@@ -30133,15 +30127,7 @@ msgstr ""
"La autenticación de dos factores ahora está desactivada para su cuenta."
#: pretix/control/views/user.py:635
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
#, python-brace-format
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -30155,8 +30141,7 @@ msgstr ""
"verlos aquí.\n"
"\n"
"Sus códigos de emergencias:\n"
"- \n"
"- "
"{tokens}"
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -30326,10 +30311,8 @@ msgid "The selected entry has been deleted."
msgstr "Se ha borrado la entrada seleccionada."
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "Las entradas de la lista de espera han sido transferidas."
msgstr "Se ha modificado la entrada de la lista de espera."
#: pretix/helpers/countries.py:134
msgid "Belarus"
@@ -37011,8 +36994,8 @@ msgid ""
"If you're looking to buy a ticket, you need to follow a direct link to an "
"event or organizer profile."
msgstr ""
"SI buscas comprar una entrada, necesitar seguir un enlace directo a un "
"evento o a un perfil de organizador."
"Si busca comprar una entrada, necesita seguir un enlace directo a un evento "
"o a un perfil de organizador."
#: pretix/presale/templates/pretixpresale/index.html:20
#, python-format

View File

@@ -4,16 +4,16 @@ 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-02-21 18:00+0000\n"
"PO-Revision-Date: 2026-03-03 20:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
"fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"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\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -13011,7 +13011,9 @@ msgstr "Texte daide du champ email"
#: pretix/base/settings.py:3398
msgid "Allow creating a new team during event creation"
msgstr "Ancienne API de lappareil denregistrement"
msgstr ""
"Autoriser la création d'une nouvelle équipe lors de la création d'un "
"événement"
#: pretix/base/settings.py:3399
msgid ""
@@ -17204,28 +17206,20 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "Vous devez spécifier autant de sièges que de codes promotionnels."
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "Veuillez sélectionner un siège valide."
msgstr "Sélectionnez une option valide."
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "Produits actifs"
msgstr "Comprend uniquement les produits actifs."
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "Un bon de réduction avec ce code existe déjà."
msgstr "Un bon pour cette inscription sur la liste d'attente a déjà été envoyé."
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "Le produit sélectionné a été désacti."
msgstr "Le produit sélectionné n'est pas actif."
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -19021,6 +19015,10 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"Il semble que votre navigateur n'accepte pas nos cookies et que vous deviez "
"vous connecter à plusieurs reprises. Veuillez vérifier si votre navigateur "
"est configuré pour bloquer les cookies, ou supprimez tous les cookies "
"existants et réessayez."
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -19108,8 +19106,8 @@ msgid ""
"Do you really want to grant the application <strong>%(application)s</strong> "
"access to your pretix account?"
msgstr ""
"Voulez-vous vraiment accorder à l'application <strong>1%(application)s2</"
"strong>3 un accès à votre compte pretix ?"
"Voulez-vous vraiment accorder à l'application <strong>%(application)s</"
"strong> un accès à votre compte pretix ?"
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html:24
#, python-format
@@ -19132,8 +19130,8 @@ msgid ""
"This application has <strong>not</strong> been reviewed by the pretix team. "
"Granting access to your pretix account happens at your own risk."
msgstr ""
"Cette application n'a <strong>1pas</strong>2 été véifiée par l'équipe "
"pretix. L'accès à votre compte Pretix se fait à vos propres risques."
"Cette application n'a <strong>pas</strong> été véifiée par l'équipe pretix. "
"L'accès à votre compte Pretix se fait à vos propres risques."
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html:54
msgid "Error:"
@@ -19536,7 +19534,7 @@ msgstr[0] ""
"Êtes-vous sûr de vouloir supprimer l'enregistrement <strong>d'un billet</"
"strong>?"
msgstr[1] ""
"Êtes-vous sûr de vouloir supprimer l'enregistrement <strong>%(count)s "
"Êtes-vous sûr de vouloir supprimer l'enregistrement <strong>%(count)s "
"billets</strong>?"
#: pretix/control/templates/pretixcontrol/checkin/bulk_revert_confirm.html:24
@@ -21634,7 +21632,7 @@ msgid ""
"as examples, you can add more in the \"Settings\" part of your event."
msgstr ""
"pretix supporte une <a href=\"https://pretix.eu/about/en/features/payment\" "
"target=\"_blank\">large gamme de fournisseurs de paiement </a> vous "
"target=\"_blank\">large gamme de fournisseurs de paiement</a> vous "
"permettant de choisir les méthodes de paiement qui conviennent le mieux à "
"votre flux de travail. En voici deux à titre d'exemple, vous pouvez en "
"ajouter dans la partie \"Paramètres\" de votre événement."
@@ -23578,8 +23576,8 @@ msgid ""
msgstr ""
"Veuillez sélectionner les produits ou les variantes de produits auxquels ce "
"quota doit s'appliquer. Si vous appliquez deux quotas à un même produit, il "
"ne sera seulement disponible si les quotas <strong>1 et </strong>2 ont "
"encore de la place."
"ne sera seulement disponible si les quotas <strong>et</strong> ont encore de "
"la place."
#: pretix/control/templates/pretixcontrol/items/quota_edit.html:41
msgid "Advanced options"
@@ -23638,8 +23636,8 @@ msgid ""
"Are you sure you want to disable the application <strong>%(application)s</"
"strong> permanently?"
msgstr ""
"Êtes-vous sûr de vouloir désactiver l'application <strong>1%(application)s2</"
"strong>3 en permanence ?"
"Êtes-vous sûr de vouloir désactiver l'application <strong>%(application)s</"
"strong> de manière permanente ?"
#: pretix/control/templates/pretixcontrol/oauth/app_list.html:4
#: pretix/control/templates/pretixcontrol/oauth/app_list.html:6
@@ -27465,8 +27463,8 @@ msgid ""
"Using this option will <strong>delete all current quotas</strong> from "
"<strong>all selected dates</strong>."
msgstr ""
"Cette option permet de <strong> supprimer tous les quotas actuels</strong> "
"de <strong>toutes les dates sélectionnées</strong>."
"Cette option permet de <strong>supprimer tous les quotas actuels</strong> de "
"<strong>toutes les dates sélectionnées</strong>."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:277
msgid ""
@@ -28399,10 +28397,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "Entrée"
msgstr "Modifier l'entrée"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -28464,7 +28460,7 @@ msgid ""
"quota is available) or you can press the big button below this text to send "
"out as many vouchers as currently possible to the persons who waited longest."
msgstr ""
"Vous avez configuré que les bons <strong>1not</strong>2 seront envoyés "
"Vous avez configuré que les bons <strong>ne</strong> seront envoyés "
"automatiquement. Vous pouvez soit les envoyer un par un dans l'ordre de "
"votre choix en cliquant sur les boutons à côté d'une ligne dans ce tableau "
"(si un quota suffisant est disponible), soit vous pouvez appuyer sur le gros "
@@ -30368,15 +30364,7 @@ msgstr ""
"compte."
#: pretix/control/views/user.py:635
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
#, python-brace-format
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -30390,8 +30378,7 @@ msgstr ""
"appareils. Vous ne pourrez plus les consulter ici.\n"
"\n"
"Vos codes d'urgence :\n"
"-\n"
"- "
"{tokens}"
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -30558,10 +30545,8 @@ msgid "The selected entry has been deleted."
msgstr "L'entrée sélectionnée a été supprimée."
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "Lentrée de la liste dattente a été transférée."
msgstr "L'entrée dans la liste d'attente a été modifiée."
#: pretix/helpers/countries.py:134
msgid "Belarus"

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-01-26 09:10+0000\n"
"PO-Revision-Date: 2025-12-03 23:00+0000\n"
"Last-Translator: sandra r <sandrarial@gestiontickets.online>\n"
"PO-Revision-Date: 2026-03-02 21:00+0000\n"
"Last-Translator: Sandra Rial Pérez <sandrarial@gestiontickets.online>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
"js/gl/>\n"
"Language: gl\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.14.3\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -162,12 +162,12 @@ msgstr "Pedidos pagados"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)"
msgstr ""
msgstr "Asistentes (ordenados)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)"
msgstr ""
msgstr "Asistentes (de pago)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue"
@@ -732,8 +732,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Os artigos da túa cesta xa non están reservados para ti. Aínda podes "
"completar o teu pedido mentres estean dispoñibles."
"Os artigos do teu carro xa non están reservados para ti. Podes completar o "
"teu pedido sempre que estean dispoñibles."
#: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired"

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-02-24 11:49+0000\n"
"PO-Revision-Date: 2026-03-02 10:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.16\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -6997,22 +6997,22 @@ msgstr "2006/112/EC号指令の第309条に基づいて免除"
#: pretix/base/models/tax.py:253
msgctxt "tax_code"
msgid "Intra-Community acquisition from second hand means of transport"
msgstr "中古輸送手段のEU域内取得"
msgstr "中古輸送手段の域内取得"
#: pretix/base/models/tax.py:255
msgctxt "tax_code"
msgid "Intra-Community acquisition of second hand goods"
msgstr "中古品のEU域内取得"
msgstr "中古品の域内取得"
#: pretix/base/models/tax.py:257
msgctxt "tax_code"
msgid "Intra-Community acquisition of works of art"
msgstr "芸術作品のEU域内取得"
msgstr "芸術作品の域内取得"
#: pretix/base/models/tax.py:259
msgctxt "tax_code"
msgid "Intra-Community acquisition of collectors items and antiques"
msgstr "コレクターアイテムおよび骨董品のEU域内取得"
msgstr "コレクターアイテムおよび骨董品の域内取得"
#: pretix/base/models/tax.py:261
msgctxt "tax_code"
@@ -16579,28 +16579,20 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "あなたはバウチャーコードの数と同じだけの席を指定する必要があります。"
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "有効な座席を選択してください。"
msgstr "有効な選択肢を選んでください。"
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "有効な製品"
msgstr "有効な製品のみを含みます。"
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "このコードを持つバウチャーはすでに存在しています。"
msgstr "この空席待ちリストの内容を持つバウチャーはすでに発送済みです。"
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "選択された製品無効化されました。"
msgstr "選択された製品無効です。"
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -18348,6 +18340,9 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"お使いのブラウザが私どものクッキーを受け入れていないようですので、繰り返し"
"ログインする必要があります。ブラウザがクッキーをブロックする設定になっている"
"か、既存のクッキーをすべて削除して再試行してください。"
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -27400,10 +27395,8 @@ msgstr "次のエントリはすでにバウチャーが添付されているた
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "入場"
msgstr "エントリーを編集する"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -29283,15 +29276,7 @@ msgid "Two-factor authentication is now disabled for your account."
msgstr "アカウントの二要素認証が無効になりました。"
#: pretix/control/views/user.py:635
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
#, python-brace-format
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -29305,8 +29290,7 @@ msgstr ""
"ことはできません。\n"
"\n"
"あなたの緊急コード:\n"
"-\n"
"- "
"{tokens}"
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -29471,10 +29455,8 @@ msgid "The selected entry has been deleted."
msgstr "選択されたエントリは削除されました。"
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "空席待ちリスト登録が転送されました。"
msgstr "空席待ちリスト登録が変更されました。"
#: pretix/helpers/countries.py:134
msgid "Belarus"

View File

@@ -7,16 +7,16 @@ 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-02-21 03: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"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"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\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -7996,7 +7996,7 @@ msgstr "Deze cadeaubon is in de tussentijd gebruikt. Probeer het opnieuw."
#: pretix/base/pdf.py:98
msgid "Ticket code (barcode content)"
msgstr "Ticketcode (waarde van QR-code)"
msgstr "Ticketcode (inhoud van QR-code)"
#: pretix/base/pdf.py:110
msgid "Order position number"
@@ -9664,7 +9664,7 @@ msgstr "Het bedrag is van uw kaart afgeschreven."
#: pretix/base/services/placeholders.py:699
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "Maak het geld over naar deze bankrekening: NL13 TEST 0123 4567 89"
msgstr "Maak het geld over naar deze bankrekening: 9999-9999-9999-9999"
#: pretix/base/services/placeholders.py:799
#: pretix/control/views/organizer.py:349
@@ -11081,8 +11081,8 @@ msgid ""
"If turned off, ticket downloads are only possible after an order has been "
"marked as paid."
msgstr ""
"Als deze optie is uitgeschakeld, kunnen tickets alleen worden gedownload "
"nadat een bestelling als betaald is gemarkeerd."
"Indien uitgeschakeld, kunnen tickets alleen gedownload worden als de "
"bestelling als betaald gemarkeerd is."
#: pretix/base/settings.py:1763
msgid "Do not issue ticket before email address is validated"
@@ -13801,12 +13801,12 @@ msgstr "Altijd"
#: pretix/base/timeline.py:60
msgctxt "timeline"
msgid "Your event starts"
msgstr "Uw evenement start"
msgstr "Start van uw evenement"
#: pretix/base/timeline.py:68
msgctxt "timeline"
msgid "Your event ends"
msgstr "Uw evenement eindigt"
msgstr "Einde van uw evenement"
#: pretix/base/timeline.py:76
msgctxt "timeline"
@@ -17002,28 +17002,20 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "U moet evenveel stoelnummers als vouchercodes opgeven."
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "Kies een geldige beschikbare stoel."
msgstr "Maak een geldige keuze."
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "Actieve producten"
msgstr "Bevat alleen actieve producten."
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "Er bestaat al een voucher met deze code."
msgstr "Er is al een voucher verzonden naar deze inschrijver op de wachtlijst."
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "Het gekozen product is uitgeschakeld."
msgstr "Het gekozen product is niet actief."
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -18811,6 +18803,9 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"Uw browser lijkt ons cookie niet te accepteren, waardoor u telkens opnieuw "
"moet inloggen. Controleer of uw browser cookies blokkeert of verwijder alle "
"opgeslagen cookies en probeer het opnieuw."
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -22359,7 +22354,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/item/base.html:24
#: pretix/control/templates/pretixcontrol/item/include_variations.html:79
msgid "Manage quotas"
msgstr "Vragen quotums"
msgstr "Quota beheren"
#: pretix/control/templates/pretixcontrol/item/base.html:27
#: pretix/control/templates/pretixcontrol/item/include_variations.html:82
@@ -28077,10 +28072,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "Binnenkomst"
msgstr "Inschrijving bewerken"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -30020,15 +30013,7 @@ msgid "Two-factor authentication is now disabled for your account."
msgstr "Twee-factor-authenticatie is nu uitgeschakeld voor uw account."
#: pretix/control/views/user.py:635
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
#, python-brace-format
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -30042,8 +30027,7 @@ msgstr ""
"U kunt ze hier niet meer opnieuw laten tonen.\n"
"\n"
"Uw noodcodes: \n"
"-\n"
"- "
"{tokens}"
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -30210,10 +30194,8 @@ msgid "The selected entry has been deleted."
msgstr "De gekozen inschrijving is verwijderd."
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "De wachtlijstinschrijving is overgedragen."
msgstr "De inschrijving op de wachtlijst is veranderd."
#: pretix/helpers/countries.py:134
msgid "Belarus"

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-02-21 03: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\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -9673,7 +9673,7 @@ msgstr "Het bedrag is van je kaart afgeschreven."
#: pretix/base/services/placeholders.py:699
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "Maak het geld over naar deze bankrekening: NL13 TEST 0123 4567 89"
msgstr "Maak het geld over naar deze bankrekening: 9999-9999-9999-9999"
#: pretix/base/services/placeholders.py:799
#: pretix/control/views/organizer.py:349
@@ -17032,28 +17032,20 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "Je moet evenveel stoelnummers als vouchercodes opgeven."
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "Kies een geldige beschikbare stoel."
msgstr "Maak een geldige keuze."
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "Actieve producten"
msgstr "Bevat alleen actieve producten."
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "Er bestaat al een voucher met deze code."
msgstr "Er is al een voucher verzonden naar deze inschrijver op de wachtlijst."
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "Het gekozen product is uitgeschakeld."
msgstr "Het gekozen product is niet actief."
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -18844,6 +18836,9 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"Je browser lijkt ons cookie niet te accepteren, waardoor je telkens opnieuw "
"moet inloggen. Controleer of je browser cookies blokkeert of verwijder alle "
"opgeslagen cookies en probeer het opnieuw."
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -28141,10 +28136,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "Binnenkomst"
msgstr "Inschrijving bewerken"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -30080,15 +30073,7 @@ msgid "Two-factor authentication is now disabled for your account."
msgstr "Twee-factor-authenticatie is nu uitgeschakeld voor je account."
#: pretix/control/views/user.py:635
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
#, python-brace-format
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -30102,8 +30087,7 @@ msgstr ""
"Je kunt ze hier niet meer opnieuw laten tonen.\n"
"\n"
"Je noodcodes:\n"
"-\n"
"- "
"{tokens}"
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -30270,10 +30254,8 @@ msgid "The selected entry has been deleted."
msgstr "De gekozen inschrijving is verwijderd."
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "De wachtlijstvermelding is overgedragen."
msgstr "De inschrijving op de wachtlijst is veranderd."
#: pretix/helpers/countries.py:134
msgid "Belarus"
@@ -34075,7 +34057,7 @@ msgid ""
"completed your payment, you can refresh this page."
msgstr ""
"Scan de QR-code hieronder om je WeChat-betaling uit te voeren. Als je de "
"betaling hebt afgerond kan je deze pagina verversen."
"betaling hebt afgerond, kan je deze pagina verversen."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html:62
msgid ""
@@ -35249,7 +35231,7 @@ msgstr "incl. belasting"
#: pretix/presale/templates/pretixpresale/event/voucher.html:359
#, python-format
msgid "<strong>plus</strong> %(rate)s%% %(name)s"
msgstr "<strong>excl.</strong> %(rate)s%% %(name)s"
msgstr "<strong>plus</strong> %(rate)s%% %(name)s"
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:180
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:320

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
{% load eventurl %}
{% load safelink %}
{% load rich_text %}
{% load anonymize_email %}
{% block thetitle %}
{% if messages %}
{{ messages|join:" " }} ::
@@ -219,7 +220,7 @@
{% endblock %}
{% block footernav %}
{% if request.event.settings.contact_mail %}
<li><a href="mailto:{{ request.event.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
<li><a href="{{ 'mailto:'|add:request.event.settings.contact_mail|anon_email }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
{% endif %}
{% if request.event.settings.privacy_url %}
<li><a href="{% safelink request.event.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>

View File

@@ -21,4 +21,5 @@
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cart.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/deanonymize_email.js" %}"></script>
{% endcompress %}

View File

@@ -5,6 +5,7 @@
{% load thumb %}
{% load eventurl %}
{% load safelink %}
{% load anonymize_email %}
{% block thetitle %}
{% block title %}{% endblock %}{% if url_name != "organizer.index" %} :: {% endif %}{{ organizer.name }}
{% endblock %}
@@ -97,7 +98,7 @@
{% endblock %}
{% block footernav %}
{% if not request.event and request.organizer.settings.contact_mail %}
<li><a href="mailto:{{ request.organizer.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
<li><a href="{{ 'mailto:'|add:request.organizer.settings.contact_mail|anon_email }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
{% endif %}
{% if not request.event and request.organizer.settings.privacy_url %}
<li><a href="{% safelink request.organizer.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>

View File

@@ -0,0 +1,7 @@
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a[href^="mailto:"]').forEach(function(link) {
// Replace [at] with @ and the [dot] with . in both the href and the displayed text (if needed)
link.href = link.href.replace('[at]', '@').replace('[dot]', '.');
link.textContent = link.textContent.replace('[at]', '@').replace('[dot]', '.');
});
});

View File

@@ -286,12 +286,12 @@ def test_by_secret_special_chars(token_client, organizer, clist, event, order):
@pytest.mark.django_db
def test_by_medium(token_client, organizer, clist, event, order):
with scopes_disabled():
rm = ReusableMedium.objects.create(
ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
linked_orderposition=order.positions.first(),
)
rm.linked_orderpositions.add(order.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
@@ -301,48 +301,6 @@ def test_by_medium(token_client, organizer, clist, event, order):
assert ci.raw_source_type == "barcode"
@pytest.mark.django_db
def test_by_medium_multiple_orderpositions(token_client, organizer, clist, event, order):
with scopes_disabled():
rm = ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
)
rm.linked_orderpositions.add(order.positions.first())
op_item_other = order.positions.all()[1]
rm.linked_orderpositions.add(op_item_other)
# multiple tickets are valid => no check-in
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'ambiguous'
with scopes_disabled():
op_item_other.valid_from = datetime.datetime(2020, 1, 1, 12, 0, 0, tzinfo=event.timezone)
op_item_other.valid_until = datetime.datetime(2020, 1, 1, 15, 0, 0, tzinfo=event.timezone)
op_item_other.save()
with freeze_time("2020-01-01 13:45:00"):
# multiple tickets are valid => no check-in
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'ambiguous'
with freeze_time("2020-01-01 10:45:00"):
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
with freeze_time("2020-01-01 15:45:00"):
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'already_redeemed'
@pytest.mark.django_db
def test_by_medium_not_connected(token_client, organizer, clist, event, order):
with scopes_disabled():
@@ -360,12 +318,12 @@ def test_by_medium_not_connected(token_client, organizer, clist, event, order):
@pytest.mark.django_db
def test_by_medium_wrong_event(token_client, organizer, clist, event, order2):
with scopes_disabled():
rm = ReusableMedium.objects.create(
ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
linked_orderposition=order2.positions.first(),
)
rm.linked_orderpositions.add(order2.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 404
assert resp.data['status'] == 'error'
@@ -379,12 +337,12 @@ def test_by_medium_wrong_event(token_client, organizer, clist, event, order2):
@pytest.mark.django_db
def test_by_medium_wrong_type(token_client, organizer, clist, event, order):
with scopes_disabled():
rm = ReusableMedium.objects.create(
ReusableMedium.objects.create(
type="nfc_uid",
identifier="abcdef",
organizer=organizer,
linked_orderposition=order.positions.first(),
)
rm.linked_orderpositions.add(order.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 404
assert resp.data['status'] == 'error'
@@ -397,13 +355,13 @@ def test_by_medium_wrong_type(token_client, organizer, clist, event, order):
@pytest.mark.django_db
def test_by_medium_inactive(token_client, organizer, clist, event, order):
with scopes_disabled():
rm = ReusableMedium.objects.create(
ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
active=False,
linked_orderposition=order.positions.first(),
)
rm.linked_orderpositions.add(order.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 404
assert resp.data['status'] == 'error'

View File

@@ -35,8 +35,8 @@ from django_scopes import scopes_disabled
from tests.const import SAMPLE_PNG
from pretix.base.models import (
InvoiceAddress, Item, Order, OrderPosition, Organizer, Question,
SeatingPlan,
GiftCard, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
Organizer, Question, SeatingPlan,
)
from pretix.base.models.orders import CartPosition, OrderFee, QuestionAnswer
@@ -895,6 +895,41 @@ def test_order_create_payment_info_optional(token_client, organizer, event, item
assert json.loads(p.info) == res['payment_info']
@pytest.mark.django_db
def test_order_create_payment_info_valid_object(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
res["payment_info"] = [{"should": "fail"}]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
res['payment_info'] = {
'foo': {
'bar': [1, 2],
'test': False
}
}
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'])
p = o.payments.first()
assert p.provider == "banktransfer"
assert p.amount == o.total
assert json.loads(p.info) == res['payment_info']
@pytest.mark.django_db
def test_order_create_position_secret_optional(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
@@ -3086,78 +3121,9 @@ def test_order_create_use_medium(token_client, organizer, event, item, quota, qu
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
medium.refresh_from_db()
assert o.positions.first() == medium.linked_orderpositions.first()
assert o.positions.first() == medium.linked_orderposition
assert resp.data['positions'][0]['pdf_data']['medium_identifier'] == medium.identifier
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.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'])
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 1
assert o.positions.first() == medium.linked_orderpositions.first()
assert resp.data['positions'][0]['pdf_data']['medium_identifier'] == medium.identifier
@pytest.mark.django_db
def test_order_create_add_to_medium(token_client, organizer, event, item, quota, question, medium):
item.media_type = medium.type
item.media_policy = Item.MEDIA_POLICY_REUSE_OR_NEW
item.save()
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['use_reusable_medium'] = medium.pk
res['positions'][0]['add_to_reusable_medium'] = medium.pk
res['positions'][0]['answers'][0]['question'] = question.pk
# do not use use_reusable_medium and add_to_reusable_medium
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
del res['positions'][0]['use_reusable_medium']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 1
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 2
res['positions'][0]['use_reusable_medium'] = medium.pk
del res['positions'][0]['add_to_reusable_medium']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.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'])
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 1
assert o.positions.first() == medium.linked_orderpositions.first()
@pytest.mark.django_db
def test_order_create_use_medium_other_organizer(token_client, organizer, event, item, quota, question, medium2):
@@ -3202,7 +3168,7 @@ def test_order_create_create_medium(token_client, organizer, event, item, quota,
i = resp.data['positions'][0]['pdf_data']['medium_identifier']
assert i
m = organizer.reusable_media.get(identifier=i)
assert m.linked_orderpositions.first() == o.positions.first()
assert m.linked_orderposition == o.positions.first()
assert m.type == "barcode"
@@ -3405,3 +3371,251 @@ 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

View File

@@ -34,7 +34,7 @@ from stripe import error
from tests.plugins.stripe.test_checkout import apple_domain_create
from tests.plugins.stripe.test_provider import MockedCharge
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models import InvoiceAddress, Order, OrderPosition, Team
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
@@ -180,6 +180,41 @@ def order2(event2, item2):
return o
@pytest.fixture
@scopes_disabled()
def team2(organizer, event2):
team2 = Team.objects.create(
organizer=organizer,
name="Test-Team 2",
can_change_teams=True,
can_manage_gift_cards=True,
can_change_items=True,
can_create_events=True,
can_change_event_settings=True,
can_change_vouchers=True,
can_view_vouchers=True,
can_change_orders=True,
can_manage_customers=True,
can_manage_reusable_media=True,
can_change_organizer_settings=True,
)
team2.limit_events.add(event2)
team2.save()
return team2
@pytest.fixture
@scopes_disabled()
def limited_token_client(client, team2):
team2.can_view_orders = True
team2.can_view_vouchers = True
team2.save()
t = team2.tokens.create(name='Foo')
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
return client
TEST_ORDERPOSITION_RES = {
"id": 1,
"order": "FOO",
@@ -987,8 +1022,64 @@ def test_refund_cancel(token_client, organizer, event, order):
assert resp.status_code == 400
@pytest.mark.parametrize(
"endpoint_template, response_code",
[('/api/v1/organizers/{}/events/{}/orderpositions/', 403), ('/api/v1/organizers/{}/orderpositions/', 200)]
)
@pytest.mark.django_db
def test_orderposition_list(token_client, organizer, device, event, order, item, subevent, subevent2, question, django_assert_num_queries):
def test_orderposition_list_limited_read(
endpoint_template, response_code, limited_token_client, organizer, device, event, order, item, subevent, subevent2, question
):
endpoint = endpoint_template.format(organizer.slug, event.slug)
i2 = copy.copy(item)
i2.pk = None
i2.save()
with scopes_disabled():
var = item.variations.create(value="Children")
res = copy.copy(TEST_ORDERPOSITION_RES)
op = order.positions.first()
op.variation = var
op.save()
res["id"] = op.pk
res["item"] = item.pk
res["variation"] = var.pk
res["answers"][0]["question"] = question.pk
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["print_logs"][0]["device_id"] = device.device_id
resp = limited_token_client.get(endpoint)
assert resp.status_code == response_code
if response_code == 200:
assert resp.json() == {'count': 0, 'next': None, 'previous': None, 'results': []}
else:
assert resp.json() == {'detail': 'You do not have permission to perform this action.'}
@pytest.mark.parametrize(
("endpoint_template", "endpoint_type"),
[
('/api/v1/organizers/{}/events/{}/orderpositions/', "event"),
('/api/v1/organizers/{}/orderpositions/', "organizer")
],
)
@pytest.mark.django_db
def test_orderposition_list(
endpoint_template,
endpoint_type,
token_client,
organizer,
device,
event,
order,
item,
subevent,
subevent2,
question,
django_assert_num_queries
):
endpoint = endpoint_template.format(organizer.slug, event.slug)
i2 = copy.copy(item)
i2.pk = None
i2.save()
@@ -1005,88 +1096,64 @@ def test_orderposition_list(token_client, organizer, device, event, order, item,
res["answers"][0]["question"] = question.pk
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["print_logs"][0]["device_id"] = device.device_id
if endpoint_type == "organizer":
res["event"] = event.slug
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint)
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=n'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order__status=n')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=p'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order__status=p')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, item.pk))
resp = token_client.get(endpoint + '?item={}'.format(item.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item__in={},{}'.format(
organizer.slug, event.slug, item.pk, i2.pk
))
resp = token_client.get(endpoint + '?item__in={},{}'.format(item.pk, i2.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, i2.pk))
resp = token_client.get(endpoint + '?item={}'.format(i2.pk))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var.pk))
resp = token_client.get(endpoint + '?variation={}'.format(var.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var2.pk))
resp = token_client.get(endpoint + '?variation={}'.format(var2.pk))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Peter'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?attendee_name=Peter')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=peter'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?attendee_name=peter')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Mark'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?attendee_name=Mark')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w'.format(
organizer.slug, event.slug))
resp = token_client.get(endpoint + '?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=abc123'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?secret=abc123')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=ABCDEFGHKL'.format(
organizer.slug, event.slug))
resp = token_client.get(endpoint + '?pseudonymization_id=ABCDEFGHKL')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=FOO'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?pseudonymization_id=FOO')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=FO'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=FO')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=z3fsn8j'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=z3fsn8j')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=Peter'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=Peter')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=5f4h6w'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=5f4h6w')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=FOO'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order=FOO')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=BAR'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order=BAR')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=false'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?has_checkin=false')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?has_checkin=true')
assert [] == resp.data['results']
with scopes_disabled():
@@ -1103,33 +1170,28 @@ def test_orderposition_list(token_client, organizer, device, event, order, item,
'gate': None,
'type': 'entry'
}]
with django_assert_num_queries(16):
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug)
)
if '/events/' in endpoint:
with django_assert_num_queries(18):
resp = token_client.get(endpoint + '?has_checkin=true')
else:
with django_assert_num_queries(17):
resp = token_client.get(endpoint + '?has_checkin=true')
assert [res] == resp.data['results']
op.subevent = subevent
op.save()
res['subevent'] = subevent.pk
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug, subevent.pk))
resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent__in={},{}'.format(organizer.slug, event.slug,
subevent.pk, subevent2.pk))
resp = token_client.get(endpoint + '?subevent__in={},{}'.format(subevent.pk, subevent2.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug,
subevent.pk + 1))
resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk + 1))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=false'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?include_canceled_positions=false')
assert len(resp.data['results']) == 1
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=true'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?include_canceled_positions=true')
assert len(resp.data['results']) == 2

View File

@@ -89,13 +89,10 @@ TEST_MEDIUM_RES = {
"organizer": "dummy",
"identifier": "ABCDEFGH",
"type": "barcode",
"claim_token": None,
"label": None,
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_orderpositions": [],
"linked_giftcard": None,
"notes": None,
"info": {},
@@ -141,7 +138,7 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
medium.linked_orderpositions.add(op)
medium.linked_orderposition = op
medium.linked_giftcard = giftcard
medium.customer = customer
medium.save()
@@ -253,90 +250,6 @@ def test_medium_create(token_client, organizer, giftcard):
assert m.updated > now() - timedelta(minutes=10)
@pytest.mark.django_db
def test_medium_create_linked_orderposition(token_client, organizer, organizer2, event, medium):
with scopes_disabled():
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
op2 = o.positions.create(item=ticket, price=Decimal("14"))
payload = dict(TEST_MEDIUM_CREATE_PAYLOAD)
# wrong organizer for orderposition
resp = token_client.post(
'/api/v1/organizers/{}/reusablemedia/'.format(organizer2.slug),
payload,
format='json'
)
assert resp.status_code == 403
# unkown orderposition
payload['linked_orderposition'] = "unknown"
resp = token_client.post(
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
payload,
format='json'
)
assert resp.status_code == 400
# create with linked_orderposition
payload['linked_orderposition'] = op.pk
resp = token_client.post(
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
payload,
format='json'
)
assert resp.status_code == 201
with scopes_disabled():
m = ReusableMedium.objects.get(pk=resp.data['id'])
assert list(m.linked_orderpositions.values_list('pk', flat=True)) == [op.pk]
# double-check API-response for fallback-values
resp = token_client.get(
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, resp.data['id'])
)
assert resp.status_code == 200
assert resp.data['linked_orderposition'] == op.pk
assert resp.data['linked_orderpositions'] == [op.pk]
# create with linked_orderposition and linked_orderpositions (not allowed)
payload['identifier'] = "FOOBAZ"
payload['linked_orderpositions'] = [op.pk, op2.pk]
resp = token_client.post(
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
payload,
format='json'
)
assert resp.status_code == 400
# multiple linked_orderpositions
del payload['linked_orderposition']
resp = token_client.post(
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
payload,
format='json'
)
assert resp.status_code == 201
with scopes_disabled():
m = ReusableMedium.objects.get(pk=resp.data['id'])
assert list(m.linked_orderpositions.values_list('pk', flat=True)) == [op.pk, op2.pk]
# double-check API-response for fallback-values
resp = token_client.get(
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, resp.data['id'])
)
assert resp.status_code == 200
assert resp.data['linked_orderposition'] is None
assert resp.data['linked_orderpositions'] == [op.pk, op2.pk]
@pytest.mark.django_db
def test_medium_foreignkeyval(token_client, organizer, giftcard2):
payload = dict(TEST_MEDIUM_CREATE_PAYLOAD)
@@ -383,53 +296,6 @@ def test_medium_patch(token_client, organizer, event, medium, giftcard, customer
assert medium.info == {'test': 2}
assert medium.identifier == "ABCDEFGH"
# test patch with linked_orderpositions
with scopes_disabled():
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
op2 = o.positions.create(item=ticket, price=Decimal("14"))
resp = token_client.patch(
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, medium.pk),
{
'linked_orderposition': op.pk,
},
format='json'
)
assert resp.status_code == 200
medium.refresh_from_db()
with scopes_disabled():
assert list(medium.linked_orderpositions.values_list('pk', flat=True)) == [op.pk]
resp = token_client.patch(
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, medium.pk),
{
'linked_orderpositions': [op.pk, op2.pk],
},
format='json'
)
assert resp.status_code == 200
medium.refresh_from_db()
with scopes_disabled():
assert list(medium.linked_orderpositions.values_list('pk', flat=True)) == [op.pk, op2.pk]
resp = token_client.patch(
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, medium.pk),
{
'linked_orderposition': op.pk,
'linked_orderpositions': [op.pk, op2.pk],
},
format='json'
)
assert resp.status_code == 400
@pytest.mark.django_db
def test_medium_no_deletion(token_client, organizer, event, medium):
@@ -553,7 +419,7 @@ def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2
ticket = org2_event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
medium2.linked_orderpositions.add(op)
medium2.linked_orderposition = op
medium2.linked_giftcard = giftcard2
medium2.save()