mirror of
https://github.com/pretix/pretix.git
synced 2025-12-09 00:42:28 +00:00
Compare commits
404 Commits
rename-con
...
validate-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3689d4a5a0 | ||
|
|
867ea39a0d | ||
|
|
b060968f62 | ||
|
|
9a47eb6385 | ||
|
|
be67e40f1c | ||
|
|
c99a9ebe9a | ||
|
|
b5cac90475 | ||
|
|
58d36706b2 | ||
|
|
c520f77bbb | ||
|
|
dead2a9bed | ||
|
|
73ceeffc7f | ||
|
|
7b6c82c341 | ||
|
|
35ac277b3d | ||
|
|
94389c3913 | ||
|
|
3e972eddbf | ||
|
|
cdeb1e86bd | ||
|
|
9a69b76880 | ||
|
|
7d5df2b69e | ||
|
|
d203eee5ab | ||
|
|
0d07615006 | ||
|
|
eb731f305b | ||
|
|
bf9af08cab | ||
|
|
95639dc6e1 | ||
|
|
1fbf75d89f | ||
|
|
f38c36c034 | ||
|
|
1488960c54 | ||
|
|
9655f3d15d | ||
|
|
4ef01df9ea | ||
|
|
7ce2d7ccb4 | ||
|
|
04fae9ea14 | ||
|
|
4b9f1712f0 | ||
|
|
9461ac27f9 | ||
|
|
1e0ede529c | ||
|
|
e386ed4352 | ||
|
|
8676033fe1 | ||
|
|
b9ca68c0be | ||
|
|
b2aca033e0 | ||
|
|
2d0b657d0f | ||
|
|
d54807073a | ||
|
|
ca1efc0a58 | ||
|
|
3746077881 | ||
|
|
04075cfc45 | ||
|
|
bdd47ee32b | ||
|
|
ff42c1fe5c | ||
|
|
0ba5c0c143 | ||
|
|
f6aa17a0ff | ||
|
|
5882a728bf | ||
|
|
11acd4ca53 | ||
|
|
40db7d939f | ||
|
|
5563183255 | ||
|
|
dfd52f05ff | ||
|
|
15ea7c65e4 | ||
|
|
0ab633ce7b | ||
|
|
cd0c6b2b0f | ||
|
|
d7be7dc1c3 | ||
|
|
aa1044cf91 | ||
|
|
3df5c890a8 | ||
|
|
0775d09df4 | ||
|
|
bccfefecf1 | ||
|
|
a78c8c910f | ||
|
|
cdc265c409 | ||
|
|
0f230be785 | ||
|
|
b077417eef | ||
|
|
0ee028a9da | ||
|
|
0c05296df3 | ||
|
|
9098eb2a26 | ||
|
|
b68f68740c | ||
|
|
c8f90c9645 | ||
|
|
98bed10e25 | ||
|
|
0f7928268b | ||
|
|
6cf916d0e6 | ||
|
|
df9147d207 | ||
|
|
0f25a1d6c8 | ||
|
|
d9572420eb | ||
|
|
bdf6b6cfa8 | ||
|
|
cdb69281ad | ||
|
|
855b8c800e | ||
|
|
3562952e6d | ||
|
|
068f82457d | ||
|
|
8a3da37b45 | ||
|
|
71f2c8093f | ||
|
|
2e674916c2 | ||
|
|
ac09b56a2c | ||
|
|
b415f8066f | ||
|
|
9af31cd29b | ||
|
|
c051623b3a | ||
|
|
8bb8c92e03 | ||
|
|
5c4451cfa3 | ||
|
|
d0116ab7dd | ||
|
|
02b5bdb321 | ||
|
|
ebfcb6f7c8 | ||
|
|
6190b93f89 | ||
|
|
40357681df | ||
|
|
a1d078b48e | ||
|
|
adca856cdb | ||
|
|
a811a31dcc | ||
|
|
177a7d07fc | ||
|
|
05e71d8e57 | ||
|
|
cd4759fb44 | ||
|
|
64c040c186 | ||
|
|
bc2cb7212d | ||
|
|
06dfba1a1e | ||
|
|
da718f3d24 | ||
|
|
2dc772cfcc | ||
|
|
d250fdf327 | ||
|
|
76fccb66bf | ||
|
|
3ecff6b8ed | ||
|
|
4bd1aba677 | ||
|
|
8853f2b189 | ||
|
|
bff0f54bf8 | ||
|
|
50c1c9c724 | ||
|
|
802268df46 | ||
|
|
a823f261f3 | ||
|
|
59a754f913 | ||
|
|
82eca01e5c | ||
|
|
943f594b6b | ||
|
|
15cbb3a416 | ||
|
|
f447e7b9c4 | ||
|
|
dcf473c543 | ||
|
|
85a9a3caa6 | ||
|
|
42b1010c36 | ||
|
|
5b851e270b | ||
|
|
2b796aa45e | ||
|
|
11460d878b | ||
|
|
1ce4c11572 | ||
|
|
11269c277b | ||
|
|
2650bf6f4f | ||
|
|
301191e4bd | ||
|
|
867cd8c59e | ||
|
|
7e8da3cef6 | ||
|
|
25f57f89b0 | ||
|
|
22f351cb89 | ||
|
|
2611ff74a5 | ||
|
|
cc1c7e1c23 | ||
|
|
e2eedac93b | ||
|
|
432064c3ae | ||
|
|
457115f4ca | ||
|
|
9d5563018e | ||
|
|
425f4da1f1 | ||
|
|
aa0ea27d6c | ||
|
|
5a2219124a | ||
|
|
f79813ea32 | ||
|
|
ba62db7a19 | ||
|
|
966d6bb8e9 | ||
|
|
2c20bf972f | ||
|
|
0deba91e4b | ||
|
|
b9b937ea0d | ||
|
|
410215f575 | ||
|
|
1fa84b772b | ||
|
|
cda950befc | ||
|
|
783d51b75f | ||
|
|
7333f82e45 | ||
|
|
b2a4ba96f8 | ||
|
|
81f5af8414 | ||
|
|
ce1406b158 | ||
|
|
b5ad68f48d | ||
|
|
5512fa8245 | ||
|
|
83f891ce24 | ||
|
|
04e4e33885 | ||
|
|
2a98907e88 | ||
|
|
22e7962a29 | ||
|
|
aa8b699b89 | ||
|
|
6adabd54dc | ||
|
|
e61f8035d3 | ||
|
|
fc4a9406e1 | ||
|
|
9d2ef94389 | ||
|
|
c060322f2f | ||
|
|
7629bbbb6a | ||
|
|
21d084c3fa | ||
|
|
b8d09a15e2 | ||
|
|
af2d35cc5a | ||
|
|
cbe18608e4 | ||
|
|
37d0a0de22 | ||
|
|
3bebdb3e28 | ||
|
|
4ad6a92f1d | ||
|
|
a34b6a04ea | ||
|
|
39e5711e95 | ||
|
|
6d422f9ae4 | ||
|
|
ccf4cbfd63 | ||
|
|
132a9aa9f2 | ||
|
|
257bd17b4a | ||
|
|
10bfd51d99 | ||
|
|
50225fd2f4 | ||
|
|
cd4fc1d6d8 | ||
|
|
4931059da3 | ||
|
|
15d15f978f | ||
|
|
34dbbdd82f | ||
|
|
9a54823515 | ||
|
|
4c76bb85a8 | ||
|
|
ed01a149b7 | ||
|
|
89adcc11c6 | ||
|
|
7037f348bf | ||
|
|
e694d3ca14 | ||
|
|
f3e1fd9135 | ||
|
|
850552c235 | ||
|
|
fb8a8142d9 | ||
|
|
5416c0cdfd | ||
|
|
a2421f9c66 | ||
|
|
8d06c79dd9 | ||
|
|
08961091f6 | ||
|
|
a7cbcb29b5 | ||
|
|
11fede5432 | ||
|
|
b8b89f3040 | ||
|
|
3b30553880 | ||
|
|
dd441c09f7 | ||
|
|
31b2841c4f | ||
|
|
baab35b81f | ||
|
|
c488901dc5 | ||
|
|
2679f79c3b | ||
|
|
ca3570df11 | ||
|
|
5bd08061a1 | ||
|
|
724c7d572f | ||
|
|
75dd98519f | ||
|
|
9bf4466732 | ||
|
|
ed9250c522 | ||
|
|
b3974067a5 | ||
|
|
cd6fbd886c | ||
|
|
0bb390f0a9 | ||
|
|
0183f3d40f | ||
|
|
82fcc4fe42 | ||
|
|
d42f8ece53 | ||
|
|
a8bffbd402 | ||
|
|
991b116026 | ||
|
|
2374d9b78c | ||
|
|
80785bee54 | ||
|
|
ea530ac6bf | ||
|
|
2dd8cc82f2 | ||
|
|
38fae12c37 | ||
|
|
e34a3ab2ce | ||
|
|
9401fbb1bc | ||
|
|
5d002d8b28 | ||
|
|
9b2c919026 | ||
|
|
e5ec1fd89a | ||
|
|
0f5c4b5cf5 | ||
|
|
c501066cff | ||
|
|
7ccb6682cf | ||
|
|
e5301dcdc5 | ||
|
|
4148cc4664 | ||
|
|
49057590f1 | ||
|
|
fc18659196 | ||
|
|
0c721c17e5 | ||
|
|
422567a6b7 | ||
|
|
0fcaeda0e9 | ||
|
|
ad8ed599dc | ||
|
|
4c2efa0a97 | ||
|
|
6efcd4b983 | ||
|
|
c29b7f28f1 | ||
|
|
871a8a2620 | ||
|
|
b7803565d6 | ||
|
|
f3b6627e63 | ||
|
|
574513550d | ||
|
|
f145d447a2 | ||
|
|
72b9b49b9d | ||
|
|
6d20d0e840 | ||
|
|
4a662a1aa1 | ||
|
|
8213b09847 | ||
|
|
c54f776b39 | ||
|
|
fdd03536f2 | ||
|
|
44303a0030 | ||
|
|
5ba10416ce | ||
|
|
efa117c836 | ||
|
|
70cd2265db | ||
|
|
b5afbfa1bf | ||
|
|
2dffe0e2c8 | ||
|
|
df0e0f9115 | ||
|
|
2fc47c5d71 | ||
|
|
c23d2e5504 | ||
|
|
58c7e3d316 | ||
|
|
2d5c3fbea6 | ||
|
|
222851620e | ||
|
|
9ac772b2f3 | ||
|
|
1408f31ec5 | ||
|
|
04f32284a8 | ||
|
|
318b80c3a5 | ||
|
|
102d172942 | ||
|
|
c084698821 | ||
|
|
edffe5c9dd | ||
|
|
09e9273a57 | ||
|
|
24ac588119 | ||
|
|
d23735b1a6 | ||
|
|
d8156186d8 | ||
|
|
abab7e5bc6 | ||
|
|
f89a33862a | ||
|
|
deb7cfa899 | ||
|
|
3f00fa58a0 | ||
|
|
49c0f6b967 | ||
|
|
fe9a7eaa24 | ||
|
|
ebac7d563c | ||
|
|
7ecc64ec73 | ||
|
|
c9a806a7d0 | ||
|
|
ab812a7d9c | ||
|
|
500bca1323 | ||
|
|
32be6a159e | ||
|
|
0152d0c639 | ||
|
|
e591c74862 | ||
|
|
29de29fe96 | ||
|
|
7bea17c70f | ||
|
|
f2b295e2a2 | ||
|
|
64c7bc67bd | ||
|
|
c41a754ce6 | ||
|
|
0bcb6b33bb | ||
|
|
1556226ff5 | ||
|
|
4c022cb964 | ||
|
|
8fb87fc489 | ||
|
|
c8775fb21a | ||
|
|
df0b322707 | ||
|
|
c200072471 | ||
|
|
076233cba8 | ||
|
|
b6efa9da7d | ||
|
|
489636c335 | ||
|
|
cbee131378 | ||
|
|
05c74b7ad6 | ||
|
|
37910f6037 | ||
|
|
0cc8e59bb0 | ||
|
|
7cdccc7d8e | ||
|
|
7e3f6df945 | ||
|
|
727ed67ff4 | ||
|
|
a51a6123f5 | ||
|
|
56964b6764 | ||
|
|
527bc83e5f | ||
|
|
626d7ecc90 | ||
|
|
69e50d35a7 | ||
|
|
32b704de70 | ||
|
|
1da00f575a | ||
|
|
b7d01e3b28 | ||
|
|
650b4b461f | ||
|
|
d14f7fb108 | ||
|
|
160f1c2e62 | ||
|
|
b9e627a86c | ||
|
|
328867c089 | ||
|
|
3e45274343 | ||
|
|
538ca9f0c2 | ||
|
|
99e10adad4 | ||
|
|
10b5f76356 | ||
|
|
39a0093c6b | ||
|
|
d8bf3d0b07 | ||
|
|
4e56ce8927 | ||
|
|
807df01f5d | ||
|
|
067e11c265 | ||
|
|
b4264c0ae7 | ||
|
|
61eff28978 | ||
|
|
4e89772c2d | ||
|
|
3212dd9b40 | ||
|
|
97c1fb9101 | ||
|
|
d5bccf8726 | ||
|
|
d768c46fa1 | ||
|
|
5a506bfbd6 | ||
|
|
3508d22591 | ||
|
|
4a6dd12884 | ||
|
|
60b906d8b7 | ||
|
|
4285612162 | ||
|
|
a3b1e4d208 | ||
|
|
3a6d7b8e92 | ||
|
|
a5d01aa2d1 | ||
|
|
89d8ca0fc2 | ||
|
|
34b656989f | ||
|
|
154f10af8f | ||
|
|
782d659c59 | ||
|
|
1b4308e101 | ||
|
|
9a119c35a8 | ||
|
|
a8ac1b1a94 | ||
|
|
6338dceb9e | ||
|
|
e4a171c11f | ||
|
|
e9edcfdfdc | ||
|
|
ef3ff52be3 | ||
|
|
a8f74d87ec | ||
|
|
6f920e6bcd | ||
|
|
a6201c841f | ||
|
|
b5ac28e36c | ||
|
|
bf5e1aeaff | ||
|
|
3f6d230c01 | ||
|
|
a4aa3cbd3b | ||
|
|
8ee90cd1c4 | ||
|
|
8d1e679a84 | ||
|
|
87f829f4d2 | ||
|
|
75dcb920a7 | ||
|
|
e68f0a7402 | ||
|
|
4255dbfb83 | ||
|
|
9def5cc7b2 | ||
|
|
17a467887c | ||
|
|
0736babf3c | ||
|
|
a5b773924c | ||
|
|
391918afe7 | ||
|
|
d8f9f9478d | ||
|
|
4d9f1a8efc | ||
|
|
23b07e29cd | ||
|
|
e1756a1ebb | ||
|
|
f5b0454e9f | ||
|
|
724a109c52 | ||
|
|
96df3d6831 | ||
|
|
dc164f7817 | ||
|
|
61ff0a767a | ||
|
|
423f0cbb90 | ||
|
|
200d520535 | ||
|
|
e2ae553c69 | ||
|
|
3ddf759a1b | ||
|
|
614a086227 | ||
|
|
35583f30bb | ||
|
|
38be6d13da | ||
|
|
6a8ec1ec7f | ||
|
|
0b799b132d | ||
|
|
0dd66f9468 | ||
|
|
149f1ee871 | ||
|
|
ec60ea9603 |
@@ -1,7 +1,7 @@
|
|||||||
This file is part of pretix (Community Edition).
|
This file is part of pretix (Community Edition).
|
||||||
|
|
||||||
Copyright (C) 2014-2020 Raphael Michel and contributors
|
Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
Copyright (C) 2020-2021 rami.io GmbH 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
|
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.
|
Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pretix
|
|||||||
:target: https://docs.pretix.eu/
|
:target: https://docs.pretix.eu/
|
||||||
|
|
||||||
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
|
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
|
||||||
|
:target: https://github.com/pretix/pretix/actions/workflows/tests.yml
|
||||||
|
|
||||||
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
|
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/pretix/pretix
|
:target: https://codecov.io/gh/pretix/pretix
|
||||||
|
|||||||
@@ -359,3 +359,156 @@ Performing a ticket search
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer or check-in list does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer or check-in list does not exist **or** you have no permission to view this resource.
|
||||||
:statuscode 404: The requested check-in list does not exist.
|
:statuscode 404: The requested check-in list does not exist.
|
||||||
|
|
||||||
|
.. _`rest-checkin-annul`:
|
||||||
|
|
||||||
|
Annulment of a check-in
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/checkinrpc/annul/
|
||||||
|
|
||||||
|
If a check-in was made in error and the person was not let in, it can be annulled. We do not recommend this to be used
|
||||||
|
in case of manual check-ins or user interfaces because it is too prone for human errors. It is mostly intended for
|
||||||
|
automated entry systems like a turnstile or automated door, where the check-in is first created, then the door is
|
||||||
|
opened, and then the check-in may be annulled if the system knows that the turnstile did not turn or was out of
|
||||||
|
order.
|
||||||
|
|
||||||
|
This endpoint supports passing multiple check-in lists for the context of a multi-event scan. However, each
|
||||||
|
check-in list passed needs to be from a distinct event.
|
||||||
|
|
||||||
|
Check-ins created by a device can only be annulled by the same device. The datetime of annulment may not be more than
|
||||||
|
15 minutes after the datetime of check-in (value subject to change).
|
||||||
|
|
||||||
|
A status code of 404 is returned if no check-in was found for the given nonce. A status code of 400 is returned when
|
||||||
|
multiple check-ins match the nonce, the input is invalid in another way, the annulment is made from the wrong device,
|
||||||
|
the check-in is already in an annulled or failed state, or the datetime constraint is not valid.
|
||||||
|
|
||||||
|
:<json string nonce: ``nonce`` value of the original check-in.
|
||||||
|
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
|
||||||
|
:<json datetime datetime: Specifies the client-side datetime of the annulment. If not supplied, the current time will be used.
|
||||||
|
:<json string error_explanation: A human-readable description of why the check-in was annulled (optional).
|
||||||
|
:>json string status: ``"ok"``
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/checkinrpc/annul/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"lists": [1],
|
||||||
|
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
||||||
|
"error_explanation": "Turnstile did not turn"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example successful response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: Invalid or incomplete request, see above
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: The requested nonce does not exist.
|
||||||
|
|
||||||
|
|
||||||
|
Check-in history
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the check-in
|
||||||
|
successful boolean Whether the check-in was successful
|
||||||
|
error_reason string Category of reason why the check-in was unsuccessful. Currently
|
||||||
|
``"canceled"``, ``"invalid"``, ``"unpaid"`` ``"product"``,
|
||||||
|
``"rules"``, ``"revoked"``, ``"incomplete"``, ``"already_redeemed"``,
|
||||||
|
``"ambiguous"``, ``"error"``, ``"blocked"``, ``"unapproved"``,
|
||||||
|
``"invalid_time"``, ``"annulled"`` or ``null``
|
||||||
|
error_explanation string Additional, human-readable reason for the check-in to be unsuccessful (or ``null``)
|
||||||
|
position integer Internal ID of the order position (or ``null`` for unknown scans)
|
||||||
|
datetime datetime Logical time when the check-in happened
|
||||||
|
created datetime Time when the check-in appeared on the server
|
||||||
|
list integer Internal ID of the check-in list
|
||||||
|
auto_checked_in boolean Whether the check-in was performed by the system automatically
|
||||||
|
gate integer Internal ID of the gate (or ``null``)
|
||||||
|
device integer Internal ID of the device (or ``null``)
|
||||||
|
device_id integer Organizer-internal ID of the device (or ``null``)
|
||||||
|
type string Type of check-in, currently ``"entry"`` or ``"exit"``
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkins/
|
||||||
|
|
||||||
|
Returns a list of all check-in events within a given event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/checkins/ 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
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"successful": true,
|
||||||
|
"error_reason": null,
|
||||||
|
"error_explanation": null,
|
||||||
|
"position": 1234,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"created": "2017-12-25T12:45:23Z",
|
||||||
|
"list": 2,
|
||||||
|
"auto_checked_in": false,
|
||||||
|
"gate": null,
|
||||||
|
"device": null,
|
||||||
|
"device_id": null,
|
||||||
|
"type": "entry",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query datetime created_since: Only return check-ins that have been created since the given date (inclusive).
|
||||||
|
:query datetime created_before: Only return check-ins that have been created before the given date (exclusive).
|
||||||
|
:query datetime datetime_since: Only return check-ins that have happened since the given date (inclusive).
|
||||||
|
:query datetime datetime_before: Only return check-ins that have happened before the given date (exclusive).
|
||||||
|
:query boolean successful: Only return check-ins that have (not) been successful.
|
||||||
|
:query boolean error_reason: Only return check-ins with a specific error reason.
|
||||||
|
:query integer list: Only return check-ins from a specific list.
|
||||||
|
:query string type: Only return check-ins of a specific type.
|
||||||
|
:query integer gate: Only return check-ins from a specific gate.
|
||||||
|
:query integer device: Only return check-ins from a specific device.
|
||||||
|
:query boolean auto_checked_in: Only return check-ins that are (not) auto-checked in.
|
||||||
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``,
|
||||||
|
and ``id``.
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event 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.
|
||||||
|
|||||||
@@ -424,9 +424,9 @@ Endpoints
|
|||||||
:param organizer: The ``slug`` field of the organizer of the event to create.
|
:param organizer: The ``slug`` field of the organizer of the event to create.
|
||||||
:param event: The ``slug`` field of the event to copy settings and items from.
|
:param event: The ``slug`` field of the event to copy settings and items from.
|
||||||
:statuscode 201: no error
|
:statuscode 201: no error
|
||||||
:statuscode 400: The event could not be created due to invalid submitted data.
|
:statuscode 400: The event could not be updated due to invalid submitted data.
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
|
||||||
|
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/
|
||||||
|
|||||||
@@ -349,6 +349,45 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/vouchers/bulk_attach/
|
||||||
|
|
||||||
|
Attaches many **existing** vouchers to an exhibitor. You need to send either the ``id`` **or** the ``code`` field of
|
||||||
|
the voucher, but you need to send the same field for all entries.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/bulk_attach/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"exhibitor_comment": "Free ticket"
|
||||||
|
},
|
||||||
|
..
|
||||||
|
]
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the event to use
|
||||||
|
:param id: The ``id`` field of the exhibitor to use
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: Invalid data sent, e.g. voucher does not exist
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
||||||
|
|
||||||
Create a new exhibitor.
|
Create a new exhibitor.
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ at :ref:`plugin-docs`.
|
|||||||
digital
|
digital
|
||||||
exhibitors
|
exhibitors
|
||||||
imported_secrets
|
imported_secrets
|
||||||
|
offlinesales
|
||||||
shipping
|
shipping
|
||||||
billing_invoices
|
billing_invoices
|
||||||
billing_var
|
billing_var
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ invoice_from_country string Sender address:
|
|||||||
invoice_from_tax_id string Sender address: Local Tax ID
|
invoice_from_tax_id string Sender address: Local Tax ID
|
||||||
invoice_from_vat_id string Sender address: EU VAT ID
|
invoice_from_vat_id string Sender address: EU VAT ID
|
||||||
invoice_to string Full recipient address
|
invoice_to string Full recipient address
|
||||||
|
invoice_to_is_business boolean Recipient address: Business vs individual (``null`` for
|
||||||
|
invoices created before pretix 2025.6).
|
||||||
invoice_to_company string Recipient address: Company name
|
invoice_to_company string Recipient address: Company name
|
||||||
invoice_to_name string Recipient address: Person name
|
invoice_to_name string Recipient address: Person name
|
||||||
invoice_to_street string Recipient address: Address lines
|
invoice_to_street string Recipient address: Address lines
|
||||||
@@ -35,6 +37,7 @@ invoice_to_state string Recipient addre
|
|||||||
invoice_to_country string Recipient address: Country code
|
invoice_to_country string Recipient address: Country code
|
||||||
invoice_to_vat_id string Recipient address: EU VAT ID
|
invoice_to_vat_id string Recipient address: EU VAT ID
|
||||||
invoice_to_beneficiary string Invoice beneficiary
|
invoice_to_beneficiary string Invoice beneficiary
|
||||||
|
invoice_to_transmission_info object Additional transmission info (see :ref:`rest-transmission-types`)
|
||||||
custom_field string Custom invoice address field
|
custom_field string Custom invoice address field
|
||||||
date date Invoice date
|
date date Invoice date
|
||||||
refers string Invoice number of an invoice this invoice refers to
|
refers string Invoice number of an invoice this invoice refers to
|
||||||
@@ -77,17 +80,12 @@ lines list of objects The actual invo
|
|||||||
for all invoice lines
|
for all invoice lines
|
||||||
created before this field was introduced as well as for
|
created before this field was introduced as well as for
|
||||||
all lines not created by a fee (e.g. a product).
|
all lines not created by a fee (e.g. a product).
|
||||||
├ event_date_from datetime Start date of the (sub)event this line was created for as it
|
├ period_start datetime Start date of the service or delivery period of the invoice line.
|
||||||
was set during invoice creation. Can be ``null`` for all invoice
|
Can be ``null`` if not known.
|
||||||
lines created before this was introduced as well as for lines in
|
├ period_end datetime End date of the service or delivery period of the invoice line.
|
||||||
an event series not created by a product (e.g. shipping or
|
Can be ``null`` if not known.
|
||||||
cancellation fees).
|
├ event_date_from datetime Deprecated alias of ``period_start``.
|
||||||
├ event_date_to datetime End date of the (sub)event this line was created for as it
|
├ event_date_to datetime Deprecated alias of ``period_end``.
|
||||||
was set during invoice creation. Can be ``null`` for all invoice
|
|
||||||
lines created before this was introduced as well as for lines in
|
|
||||||
an event series not created by a product (e.g. shipping or
|
|
||||||
cancellation fees) as well as whenever the respective (sub)event
|
|
||||||
has no end date set.
|
|
||||||
├ event_location string Location of the (sub)event this line was created for as it
|
├ event_location string Location of the (sub)event this line was created for as it
|
||||||
was set during invoice creation. Can be ``null`` for all invoice
|
was set during invoice creation. Can be ``null`` for all invoice
|
||||||
lines created before this was introduced as well as for lines in
|
lines created before this was introduced as well as for lines in
|
||||||
@@ -110,6 +108,12 @@ foreign_currency_rate decimal (string) If ``foreign_cu
|
|||||||
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
|
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
|
||||||
date at which the currency rate was obtained.
|
date at which the currency rate was obtained.
|
||||||
internal_reference string Customer's reference to be printed on the invoice.
|
internal_reference string Customer's reference to be printed on the invoice.
|
||||||
|
transmission_type string Requested transmission channel (see :ref:`rest-transmission-types`)
|
||||||
|
transmission_provider string Selected transmission provider (depends on installed
|
||||||
|
plugins). ``null`` if not yet chosen.
|
||||||
|
transmission_status string Transmission status, one of ``unknown`` (pre-2025.6),
|
||||||
|
``pending``, ``inflight``, ``failed``, and ``completed``.
|
||||||
|
transmission_date datetime Time of last change in transmission status (may be ``null``).
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -121,6 +125,76 @@ internal_reference string Customer's refe
|
|||||||
|
|
||||||
The ``tax_code`` attribute has been added.
|
The ``tax_code`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.6
|
||||||
|
|
||||||
|
The attributes ``invoice_to_is_business``, ``invoice_to_transmission_info``, ``transmission_type``,
|
||||||
|
``transmission_provider``, ``transmission_status``, and ``transmission_date`` have been added.
|
||||||
|
|
||||||
|
|
||||||
|
.. _`rest-transmission-types`:
|
||||||
|
|
||||||
|
Transmission types
|
||||||
|
------------------
|
||||||
|
|
||||||
|
pretix supports multiple ways to transmit an invoice from the organizer to the invoice recipient.
|
||||||
|
For each transmission type, different fields are supported in the ``transmission_info`` object of the
|
||||||
|
invoice address. Currently, pretix supports the following transmission types:
|
||||||
|
|
||||||
|
Email
|
||||||
|
"""""
|
||||||
|
|
||||||
|
The identifier ``"email"`` represents the transmission of PDF invoices through email.
|
||||||
|
This is the default transmission type in pretix and has some special behavior for backwards compatibility.
|
||||||
|
Transmission is always executed through the provider ``"email_pdf"``.
|
||||||
|
The ``transmission_info`` object may contain the following properties:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
transmission_email_address string Optional. An email address other than the order address
|
||||||
|
that the invoice should be sent to.
|
||||||
|
Business customers only.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Peppol
|
||||||
|
""""""
|
||||||
|
|
||||||
|
The identifier ``"peppol"`` represents the transmission of XML invoices through the `Peppol`_ network.
|
||||||
|
This is only available for business addresses.
|
||||||
|
This is not supported by pretix out of the box and requires the use of a suitable plugin.
|
||||||
|
The ``transmission_info`` object may contain the following properties:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
transmission_peppol_participant_id string Required. The Peppol participant ID of the recipient.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Italian Exchange System
|
||||||
|
"""""""""""""""""""""""
|
||||||
|
|
||||||
|
The identifier ``"it_sdi"`` represents the transmission of XML invoices through the `Sistema di Interscambio`_ network used in Italy.
|
||||||
|
This is only available for addresses with country ``"IT"``.
|
||||||
|
This is not supported by pretix out of the box and requires the use of a suitable plugin.
|
||||||
|
The ``transmission_info`` object may contain the following properties:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
transmission_it_sdi_codice_fiscale string Required for non-business address. Fiscal code of the
|
||||||
|
recipient.
|
||||||
|
transmission_it_sdi_pec string Required for business addresses. Address for certified
|
||||||
|
electronic mail.
|
||||||
|
transmission_it_sdi_recipient_code string Required for businesses. SdI recipient code.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
If this type is selected, ``vat_id`` is required for business addresses.
|
||||||
|
|
||||||
List of all invoices
|
List of all invoices
|
||||||
--------------------
|
--------------------
|
||||||
@@ -164,6 +238,7 @@ List of all invoices
|
|||||||
"invoice_from_vat_id":"",
|
"invoice_from_vat_id":"",
|
||||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||||
"invoice_to_company": "Sample company",
|
"invoice_to_company": "Sample company",
|
||||||
|
"invoice_to_is_business": true,
|
||||||
"invoice_to_name": "John Doe",
|
"invoice_to_name": "John Doe",
|
||||||
"invoice_to_street": "Test street 12",
|
"invoice_to_street": "Test street 12",
|
||||||
"invoice_to_zipcode": "12345",
|
"invoice_to_zipcode": "12345",
|
||||||
@@ -172,6 +247,7 @@ List of all invoices
|
|||||||
"invoice_to_country": "TE",
|
"invoice_to_country": "TE",
|
||||||
"invoice_to_vat_id": "EU123456789",
|
"invoice_to_vat_id": "EU123456789",
|
||||||
"invoice_to_beneficiary": "",
|
"invoice_to_beneficiary": "",
|
||||||
|
"invoice_to_transmission_info": {},
|
||||||
"custom_field": null,
|
"custom_field": null,
|
||||||
"date": "2017-12-01",
|
"date": "2017-12-01",
|
||||||
"refers": null,
|
"refers": null,
|
||||||
@@ -193,6 +269,8 @@ List of all invoices
|
|||||||
"fee_internal_type": null,
|
"fee_internal_type": null,
|
||||||
"event_date_from": "2017-12-27T10:00:00Z",
|
"event_date_from": "2017-12-27T10:00:00Z",
|
||||||
"event_date_to": null,
|
"event_date_to": null,
|
||||||
|
"period_start": "2017-12-27T10:00:00Z",
|
||||||
|
"period_end": "2017-12-27T10:00:00Z",
|
||||||
"event_location": "Heidelberg",
|
"event_location": "Heidelberg",
|
||||||
"attendee_name": null,
|
"attendee_name": null,
|
||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
@@ -204,7 +282,11 @@ List of all invoices
|
|||||||
],
|
],
|
||||||
"foreign_currency_display": "PLN",
|
"foreign_currency_display": "PLN",
|
||||||
"foreign_currency_rate": "4.2408",
|
"foreign_currency_rate": "4.2408",
|
||||||
"foreign_currency_rate_date": "2017-07-24"
|
"foreign_currency_rate_date": "2017-07-24",
|
||||||
|
"transmission_type": "email",
|
||||||
|
"transmission_provider": "email_pdf",
|
||||||
|
"transmission_status": "completed",
|
||||||
|
"transmission_date": "2017-07-24T10:00:00Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -304,6 +386,7 @@ Fetching individual invoices
|
|||||||
"invoice_from_vat_id":"",
|
"invoice_from_vat_id":"",
|
||||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||||
"invoice_to_company": "Sample company",
|
"invoice_to_company": "Sample company",
|
||||||
|
"invoice_to_is_business": true,
|
||||||
"invoice_to_name": "John Doe",
|
"invoice_to_name": "John Doe",
|
||||||
"invoice_to_street": "Test street 12",
|
"invoice_to_street": "Test street 12",
|
||||||
"invoice_to_zipcode": "12345",
|
"invoice_to_zipcode": "12345",
|
||||||
@@ -312,6 +395,7 @@ Fetching individual invoices
|
|||||||
"invoice_to_country": "TE",
|
"invoice_to_country": "TE",
|
||||||
"invoice_to_vat_id": "EU123456789",
|
"invoice_to_vat_id": "EU123456789",
|
||||||
"invoice_to_beneficiary": "",
|
"invoice_to_beneficiary": "",
|
||||||
|
"invoice_to_transmission_info": {},
|
||||||
"custom_field": null,
|
"custom_field": null,
|
||||||
"date": "2017-12-01",
|
"date": "2017-12-01",
|
||||||
"refers": null,
|
"refers": null,
|
||||||
@@ -333,6 +417,8 @@ Fetching individual invoices
|
|||||||
"fee_internal_type": null,
|
"fee_internal_type": null,
|
||||||
"event_date_from": "2017-12-27T10:00:00Z",
|
"event_date_from": "2017-12-27T10:00:00Z",
|
||||||
"event_date_to": null,
|
"event_date_to": null,
|
||||||
|
"period_start": "2017-12-27T10:00:00Z",
|
||||||
|
"period_end": "2017-12-27T10:00:00Z",
|
||||||
"event_location": "Heidelberg",
|
"event_location": "Heidelberg",
|
||||||
"attendee_name": null,
|
"attendee_name": null,
|
||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
@@ -344,7 +430,11 @@ Fetching individual invoices
|
|||||||
],
|
],
|
||||||
"foreign_currency_display": "PLN",
|
"foreign_currency_display": "PLN",
|
||||||
"foreign_currency_rate": "4.2408",
|
"foreign_currency_rate": "4.2408",
|
||||||
"foreign_currency_rate_date": "2017-07-24"
|
"foreign_currency_rate_date": "2017-07-24",
|
||||||
|
"transmission_type": "email",
|
||||||
|
"transmission_provider": "email_pdf",
|
||||||
|
"transmission_status": "completed",
|
||||||
|
"transmission_date": "2017-07-24T10:00:00Z"
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -449,3 +539,70 @@ Invoices cannot be edited directly, but the following actions can be triggered:
|
|||||||
:statuscode 400: The invoice has already been canceled
|
:statuscode 400: The invoice has already been canceled
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
|
||||||
|
Transmitting invoices
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Invoices are transmitted automatically when created during order creation or payment receipt,
|
||||||
|
but in other cases transmission may need to be triggered manually.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/transmit/
|
||||||
|
|
||||||
|
Transmits the invoice to the recipient, but only if it is in ``pending`` state.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/transmit/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/pdf
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param number: The ``number`` field of the invoice to transmit
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to transmit this invoice **or** the invoice may not be transmitted
|
||||||
|
:statuscode 409: The invoice is currently in transmission
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/retransmit/
|
||||||
|
|
||||||
|
Transmits the invoice to the recipient even if transmission was already attempted previously.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/retransmit/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/pdf
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param number: The ``number`` field of the invoice to transmit
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to transmit this invoice **or** the invoice may not be transmitted
|
||||||
|
:statuscode 409: The invoice is currently in transmission
|
||||||
|
|
||||||
|
|
||||||
|
.. _Peppol: https://en.wikipedia.org/wiki/PEPPOL
|
||||||
|
.. _Sistema di Interscambio: https://it.wikipedia.org/wiki/Fattura_elettronica_in_Italia
|
||||||
219
doc/api/resources/offlinesales.rst
Normal file
219
doc/api/resources/offlinesales.rst
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
Offline sales
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. note:: This API is only available when the plugin **pretix-offlinesales** is installed (pretix Hosted and Enterprise only).
|
||||||
|
|
||||||
|
The offline sales module allows you to create batches of tickets intended for the sale outside the system.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The offline sales batch resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal batch ID
|
||||||
|
creation datetime Time of creation
|
||||||
|
testmode boolean ``true`` if orders are created in test mode
|
||||||
|
sales_channel string Sales channel of the orders
|
||||||
|
layout integer Internal ID of the chosen ticket layout
|
||||||
|
subevent integer Internal ID of the chosen subevent (or ``null``)
|
||||||
|
item integer Internal ID of the chosen product
|
||||||
|
variation integer Internal ID of the chosen variation (or ``null``)
|
||||||
|
amount integer Number of tickets in the batch
|
||||||
|
comment string Internal comment
|
||||||
|
orders list of strings List of order codes (omitted in list view for performance reasons)
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/
|
||||||
|
|
||||||
|
Returns a list of all offline sales batches
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/offlinesalesbatches/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"creation": "2025-07-08T18:27:32.134368+02:00",
|
||||||
|
"testmode": False,
|
||||||
|
"sales_channel": "web",
|
||||||
|
"comment": "Batch for sale at the event",
|
||||||
|
"layout": 3,
|
||||||
|
"subevent": null,
|
||||||
|
"item": 23,
|
||||||
|
"variation": null,
|
||||||
|
"amount": 7
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of a valid event
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/(id)/
|
||||||
|
|
||||||
|
Returns information on a given batch.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/offlinesalesbatches/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"creation": "2025-07-08T18:27:32.134368+02:00",
|
||||||
|
"testmode": False,
|
||||||
|
"sales_channel": "web",
|
||||||
|
"comment": "Batch for sale at the event",
|
||||||
|
"layout": 3,
|
||||||
|
"subevent": null,
|
||||||
|
"item": 23,
|
||||||
|
"variation": null,
|
||||||
|
"amount": 7,
|
||||||
|
"orders": ["TSRNN", "3FBSL", "WMDNJ", "BHW9H", "MXSUG", "DSDAP", "URLLE"]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the batch 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 it.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/
|
||||||
|
|
||||||
|
With this API call, you can instruct the system to create a new batch.
|
||||||
|
|
||||||
|
Since batches can contain up to 10,000 tickets, they are created asynchronously on the server.
|
||||||
|
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
||||||
|
The body points you to the check URL of the result. Running a ``GET`` request on that result URL will
|
||||||
|
yield one of the following status codes:
|
||||||
|
|
||||||
|
* ``200 OK`` – The creation of the batch has succeeded. The body will be your resulting batch with the same information as in the detail endpoint above.
|
||||||
|
* ``409 Conflict`` – Your creation job is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||||
|
* ``410 Gone`` – Creating the batch has failed permanently (e.g. quota no longer available). The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||||
|
* ``404 Not Found`` – The job does not exist / is expired.
|
||||||
|
|
||||||
|
.. note:: To avoid performance issues, a maximum amount of 10000 is currently allowed.
|
||||||
|
|
||||||
|
.. note:: Do not wait multiple hours or more to retrieve your result. After a longer wait time, ``409`` might be returned permanently due to technical constraints, even though nothing will happen any more.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"testmode": True,
|
||||||
|
"layout": 123,
|
||||||
|
"item": 14,
|
||||||
|
"sales_channel": "web",
|
||||||
|
"amount": 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"check": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/check/29891ede-196f-4942-9e26-d055a36e98b8/"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:statuscode 202: no error
|
||||||
|
:statuscode 400: Invalid input options
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/(id)/render/
|
||||||
|
|
||||||
|
With this API call, you can render the PDF representation of a batch.
|
||||||
|
|
||||||
|
Since batches can contain up to 10,000 tickets, they are rendered asynchronously on the server.
|
||||||
|
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
||||||
|
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
|
||||||
|
yield one of the following status codes:
|
||||||
|
|
||||||
|
* ``200 OK`` – The creation of the batch has succeeded. The body will be your resulting batch with the same information as in the detail endpoint above.
|
||||||
|
* ``409 Conflict`` – Your rendering process is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||||
|
* ``410 Gone`` – Rendering the batch has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||||
|
* ``404 Not Found`` – The rendering job does not exist / is expired.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/1/render 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
|
||||||
|
|
||||||
|
{
|
||||||
|
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/1/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the batch to fetch
|
||||||
|
:statuscode 202: no error
|
||||||
|
:statuscode 400: Invalid input options
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ expires datetime The order will
|
|||||||
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
|
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
|
||||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||||
total money (string) Total value of this order
|
total money (string) Total value of this order
|
||||||
|
tax_rounding_mode string Tax rounding mode, see :ref:`algorithms-rounding`
|
||||||
comment string Internal comment on this order
|
comment string Internal comment on this order
|
||||||
api_meta object Meta data for that order. Only available through API, no guarantees
|
api_meta object Meta data for that order. Only available through API, no guarantees
|
||||||
on the content structure. You can use this to save references to your system.
|
on the content structure. You can use this to save references to your system.
|
||||||
@@ -65,11 +66,16 @@ invoice_address object Invoice address
|
|||||||
├ state string Customer state (ISO 3166-2 code). Only supported in
|
├ state string Customer state (ISO 3166-2 code). Only supported in
|
||||||
AU, BR, CA, CN, MY, MX, and US.
|
AU, BR, CA, CN, MY, MX, and US.
|
||||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||||
|
|
||||||
├ custom_field string Custom invoice address field
|
├ custom_field string Custom invoice address field
|
||||||
├ vat_id string Customer VAT ID
|
├ vat_id string Customer VAT ID
|
||||||
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
├ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
||||||
EU VAT service and validation was successful. This only
|
EU VAT service and validation was successful. This only
|
||||||
happens in rare cases.
|
happens in rare cases.
|
||||||
|
├ transmission_type string Transmission channel for invoice (see also :ref:`rest-transmission-types`).
|
||||||
|
Defaults to ``email``.
|
||||||
|
└ transmission_info object Transmission-channel specific information (or ``null``).
|
||||||
|
See also :ref:`rest-transmission-types`.
|
||||||
positions list of objects List of order positions (see below). By default, only
|
positions list of objects List of order positions (see below). By default, only
|
||||||
non-canceled positions are included.
|
non-canceled positions are included.
|
||||||
fees list of objects List of fees included in the order total. By default, only
|
fees list of objects List of fees included in the order total. By default, only
|
||||||
@@ -142,6 +148,14 @@ plugin_data object Additional data
|
|||||||
|
|
||||||
The ``plugin_data`` attribute has been added.
|
The ``plugin_data`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.6
|
||||||
|
|
||||||
|
The ``invoice_address.transmission_type`` and ``invoice_address.transmission_info`` attributes have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.10
|
||||||
|
|
||||||
|
The ``tax_rounding_mode`` attribute has been added.
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
Order position resource
|
Order position resource
|
||||||
@@ -349,6 +363,7 @@ List of all orders
|
|||||||
"payment_provider": "banktransfer",
|
"payment_provider": "banktransfer",
|
||||||
"fees": [],
|
"fees": [],
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
|
"tax_rounding_mode": "line",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"custom_followup_at": null,
|
"custom_followup_at": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
@@ -368,7 +383,9 @@ List of all orders
|
|||||||
"state": "",
|
"state": "",
|
||||||
"internal_reference": "",
|
"internal_reference": "",
|
||||||
"vat_id": "EU123456789",
|
"vat_id": "EU123456789",
|
||||||
"vat_id_validated": false
|
"vat_id_validated": false,
|
||||||
|
"transmission_type": "email",
|
||||||
|
"transmission_info": {}
|
||||||
},
|
},
|
||||||
"positions": [
|
"positions": [
|
||||||
{
|
{
|
||||||
@@ -407,6 +424,7 @@ List of all orders
|
|||||||
"seat": null,
|
"seat": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
|
"id": 1337,
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -590,6 +608,7 @@ Fetching individual orders
|
|||||||
"payment_provider": "banktransfer",
|
"payment_provider": "banktransfer",
|
||||||
"fees": [],
|
"fees": [],
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
|
"tax_rounding_mode": "line",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"api_meta": {},
|
"api_meta": {},
|
||||||
"custom_followup_at": null,
|
"custom_followup_at": null,
|
||||||
@@ -610,7 +629,9 @@ Fetching individual orders
|
|||||||
"state": "",
|
"state": "",
|
||||||
"internal_reference": "",
|
"internal_reference": "",
|
||||||
"vat_id": "EU123456789",
|
"vat_id": "EU123456789",
|
||||||
"vat_id_validated": false
|
"vat_id_validated": false,
|
||||||
|
"transmission_type": "email",
|
||||||
|
"transmission_info": {}
|
||||||
},
|
},
|
||||||
"positions": [
|
"positions": [
|
||||||
{
|
{
|
||||||
@@ -649,6 +670,7 @@ Fetching individual orders
|
|||||||
"seat": null,
|
"seat": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
|
"id": 1337,
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -996,6 +1018,7 @@ Creating orders
|
|||||||
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
|
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
|
||||||
charge will be created), this is just informative in case you *handled the payment already*.
|
charge will be created), this is just informative in case you *handled the payment already*.
|
||||||
* ``payment_date`` (optional) – Date and time of the completion of the payment.
|
* ``payment_date`` (optional) – Date and time of the completion of the payment.
|
||||||
|
* ``tax_rounding_mode`` (optional)
|
||||||
* ``comment`` (optional)
|
* ``comment`` (optional)
|
||||||
* ``custom_followup_at`` (optional)
|
* ``custom_followup_at`` (optional)
|
||||||
* ``checkin_attention`` (optional)
|
* ``checkin_attention`` (optional)
|
||||||
@@ -1015,8 +1038,10 @@ Creating orders
|
|||||||
* ``internal_reference``
|
* ``internal_reference``
|
||||||
* ``vat_id``
|
* ``vat_id``
|
||||||
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
||||||
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
||||||
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
||||||
|
* ``transmission_type`` (optional, defaults to ``email``)
|
||||||
|
* ``transmission_info`` (optional, see also :ref:`rest-transmission-types`)
|
||||||
|
|
||||||
* ``positions``
|
* ``positions``
|
||||||
|
|
||||||
@@ -1041,6 +1066,7 @@ 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)
|
* ``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)
|
* ``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)
|
* ``use_reusable_medium`` (optional, causes the new ticket to take over 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``
|
* ``answers``
|
||||||
|
|
||||||
* ``question``
|
* ``question``
|
||||||
@@ -1617,6 +1643,7 @@ List of all order positions
|
|||||||
"blocked": null,
|
"blocked": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
|
"id": 1337,
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -1745,6 +1772,7 @@ Fetching individual positions
|
|||||||
"seat": null,
|
"seat": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
|
"id": 1337,
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -1926,6 +1954,7 @@ Manipulating individual positions
|
|||||||
|
|
||||||
(Full order position resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
|
:query boolean check_quotas: Whether to check quotas before committing item changes, default is ``true``
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
:param id: The ``id`` field of the order position to update
|
:param id: The ``id`` field of the order position to update
|
||||||
@@ -2005,6 +2034,7 @@ Manipulating individual positions
|
|||||||
|
|
||||||
(Full order position resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
|
:query boolean check_quotas: Whether to check quotas before creating the new position, default is ``true``
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
|
|
||||||
@@ -2291,6 +2321,7 @@ otherwise, such as splitting an order or changing fees.
|
|||||||
|
|
||||||
(Full order position resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
|
:query boolean check_quotas: Whether to check quotas before patching or creating positions, default is ``true``
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
:param code: The ``code`` field of the order to update
|
:param code: The ``code`` field of the order to update
|
||||||
@@ -2486,6 +2517,7 @@ Order payment endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"amount": "23.00",
|
"amount": "23.00",
|
||||||
|
"comment": "Overpayment",
|
||||||
"mark_canceled": false
|
"mark_canceled": false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ name string The organizer's
|
|||||||
slug string A short form of the name, used e.g. in URLs.
|
slug string A short form of the name, used e.g. in URLs.
|
||||||
public_url string The public, customer-facing URL of the organizer, where
|
public_url string The public, customer-facing URL of the organizer, where
|
||||||
the list of all events can be found (read-only).
|
the list of all events can be found (read-only).
|
||||||
|
plugins list A list of package names of the enabled plugins for this
|
||||||
|
organizer. Note that most plugins are enabled on the
|
||||||
|
event level (or both levels). If you remove a plugin
|
||||||
|
that is also enabled on some events, it will
|
||||||
|
automatically be removed from all events as well.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +58,10 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"name": "Big Events LLC",
|
"name": "Big Events LLC",
|
||||||
"slug": "Big Events",
|
"slug": "Big Events",
|
||||||
"public_url": "https://pretix.eu/bigevents/"
|
"public_url": "https://pretix.eu/bigevents/",
|
||||||
|
"plugins": [
|
||||||
|
"pretix_datev"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -87,7 +95,10 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"name": "Big Events LLC",
|
"name": "Big Events LLC",
|
||||||
"slug": "Big Events",
|
"slug": "Big Events",
|
||||||
"public_url": "https://pretix.eu/bigevents/"
|
"public_url": "https://pretix.eu/bigevents/",
|
||||||
|
"plugins": [
|
||||||
|
"pretix_datev"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -95,6 +106,50 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/
|
||||||
|
|
||||||
|
Updates an organizer. Currently only the ``plugins`` field may be updated.
|
||||||
|
|
||||||
|
Permission required: "Can change organizer settings"
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"pretix_seating"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Big Events LLC",
|
||||||
|
"slug": "Big Events",
|
||||||
|
"public_url": "https://pretix.eu/bigevents/",
|
||||||
|
"plugins": [
|
||||||
|
"pretix_seating"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to update
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The organizer could not be updated due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
|
||||||
|
|
||||||
Organizer settings
|
Organizer settings
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ closed boolean Whether the quo
|
|||||||
field).
|
field).
|
||||||
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
|
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
|
||||||
have been scanned at an exit.
|
have been scanned at an exit.
|
||||||
|
ignore_for_event_availability boolean Whether the quota is ignored when calculating the event's
|
||||||
|
availability of tickets.
|
||||||
available boolean Whether this quota is available. Only returned if ``with_availability=true``
|
available boolean Whether this quota is available. Only returned if ``with_availability=true``
|
||||||
is set on the request. Do not rely on this value for critical operations, it may be
|
is set on the request. Do not rely on this value for critical operations, it may be
|
||||||
slightly out of date.
|
slightly out of date.
|
||||||
@@ -36,6 +38,10 @@ available_number integer Number of avail
|
|||||||
slightly out of date. ``null`` means unlimited.
|
slightly out of date. ``null`` means unlimited.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.7
|
||||||
|
|
||||||
|
The attribute ``ignore_for_event_availability`` has been added.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -72,7 +78,8 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false
|
"closed": false,
|
||||||
|
"ignore_for_event_availability": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -118,7 +125,8 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false
|
"closed": false,
|
||||||
|
"ignore_for_event_availability": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -149,7 +157,8 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false
|
"closed": false,
|
||||||
|
"ignore_for_event_availability": false
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -168,7 +177,8 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false
|
"closed": false,
|
||||||
|
"ignore_for_event_availability": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
|
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
|
||||||
@@ -223,7 +233,8 @@ Endpoints
|
|||||||
],
|
],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false
|
"closed": false,
|
||||||
|
"ignore_for_event_availability": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ The voucher resource contains the following public fields:
|
|||||||
Field Type Description
|
Field Type Description
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the voucher
|
id integer Internal ID of the voucher
|
||||||
|
created datetime The creation date of the voucher. For vouchers created before pretix 2025.7.0, this is guessed retroactively and might not be accurate.
|
||||||
code string The voucher code that is required to redeem the voucher
|
code string The voucher code that is required to redeem the voucher
|
||||||
max_usages integer The maximum number of times this voucher can be
|
max_usages integer The maximum number of times this voucher can be
|
||||||
redeemed (default: 1).
|
redeemed (default: 1).
|
||||||
@@ -49,8 +50,14 @@ subevent integer ID of the date
|
|||||||
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
||||||
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
|
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
|
||||||
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
|
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
|
||||||
|
budget money (string) The budget a voucher is allowed to consume before being used up (or ``null``)
|
||||||
|
budget_used money (string) The amount of budget the voucher has already used up.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.7
|
||||||
|
|
||||||
|
The attributes ``created``, ``budget``, and ``budget_used`` have been added.
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -82,6 +89,7 @@ Endpoints
|
|||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"created": "2020-09-18T14:17:40.971519Z",
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -99,7 +107,9 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false
|
"all_bundles_included": false,
|
||||||
|
"budget": None,
|
||||||
|
"budget_used": "0.00"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -152,6 +162,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"created": "2020-09-18T14:17:40.971519Z",
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -169,7 +180,9 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false
|
"all_bundles_included": false,
|
||||||
|
"budget": None,
|
||||||
|
"budget_used": "0.00"
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -222,6 +235,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"created": "2020-09-18T14:17:40.971519Z",
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -239,7 +253,9 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false
|
"all_bundles_included": false,
|
||||||
|
"budget": None,
|
||||||
|
"budget_used": "0.00"
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
||||||
@@ -313,6 +329,7 @@ Endpoints
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"created": "2020-09-18T14:17:40.971519Z",
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
…
|
…
|
||||||
}, …
|
}, …
|
||||||
@@ -359,6 +376,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"created": "2020-09-18T14:17:40.971519Z",
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -376,7 +394,9 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false
|
"all_bundles_included": false,
|
||||||
|
"budget": None,
|
||||||
|
"budget_used": "0.00"
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ The following values for ``action_types`` are valid with pretix core:
|
|||||||
* ``pretix.event.added``
|
* ``pretix.event.added``
|
||||||
* ``pretix.event.changed``
|
* ``pretix.event.changed``
|
||||||
* ``pretix.event.deleted``
|
* ``pretix.event.deleted``
|
||||||
|
* ``pretix.voucher.added``
|
||||||
|
* ``pretix.voucher.changed``
|
||||||
|
* ``pretix.voucher.deleted``
|
||||||
* ``pretix.subevent.added``
|
* ``pretix.subevent.added``
|
||||||
* ``pretix.subevent.changed``
|
* ``pretix.subevent.changed``
|
||||||
* ``pretix.subevent.deleted``
|
* ``pretix.subevent.deleted``
|
||||||
|
|||||||
@@ -178,3 +178,124 @@ Flowchart
|
|||||||
---------
|
---------
|
||||||
|
|
||||||
.. image:: /images/cart_pricing.png
|
.. image:: /images/cart_pricing.png
|
||||||
|
|
||||||
|
|
||||||
|
.. _`algorithms-rounding`:
|
||||||
|
|
||||||
|
Rounding of taxes
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
pretix internally always stores taxes on a per-line level, like this:
|
||||||
|
|
||||||
|
========== ========== =========== ======= =============
|
||||||
|
Product Tax rate Net price Tax Gross price
|
||||||
|
========== ========== =========== ======= =============
|
||||||
|
Ticket A 19 % 84.03 15.97 100.00
|
||||||
|
Ticket B 19 % 84.03 15.97 100.00
|
||||||
|
Ticket C 19 % 84.03 15.97 100.00
|
||||||
|
Ticket D 19 % 84.03 15.97 100.00
|
||||||
|
Ticket E 19 % 84.03 15.97 100.00
|
||||||
|
Sum 420.15 79.85 500.00
|
||||||
|
========== ========== =========== ======= =============
|
||||||
|
|
||||||
|
Whether the net price is computed from the gross price or vice versa is configured on the tax rule and may differ for every line.
|
||||||
|
|
||||||
|
The line-based computation has a few significant advantages:
|
||||||
|
|
||||||
|
- We can report both net and gross prices for every individual ticket.
|
||||||
|
|
||||||
|
- We can report both net and gross prices for every filter imaginable, such as the gross sum of all sales of Ticket A
|
||||||
|
or the net sum of all sales for a specific date in an event series. All numbers will be exact.
|
||||||
|
|
||||||
|
- When splitting the order into two, both net price and gross price are split without any changes in rounding.
|
||||||
|
|
||||||
|
The main disadvantage is that the tax looks "wrong" when computed from the sum. Taking the sum of net prices (420.15)
|
||||||
|
and multiplying it with the tax rate (19%) yields a tax amount of 79.83 (instead of 79.85) and a gross sum of 499.98
|
||||||
|
(instead of 499.98). This becomes a problem when juristictions, data formats, or external systems expect this calculation
|
||||||
|
to work on the level of the entire order. A prominent example is the EN 16931 standard for e-invoicing that
|
||||||
|
does not allow the computation as created by pretix.
|
||||||
|
|
||||||
|
However, calculating the tax rate from the net total has significant disadvantages:
|
||||||
|
|
||||||
|
- It is impossible to guarantee a stable gross price this way, i.e. if you advertise a price of €100 per ticket to
|
||||||
|
consumers, they will be confused when they only need to pay €499.98 for 5 tickets.
|
||||||
|
|
||||||
|
- Some prices are impossible, e.g. you cannot sell a ticket for a gross price of €99.99 at a 19% tax rate, since there
|
||||||
|
is no two-decimal net price that would be computed to a gross price of €99.99.
|
||||||
|
|
||||||
|
- When splitting an order into two, the combined of the new orders is not guaranteed to be the same as the total of the
|
||||||
|
original order. Therefore, additional payments or refunds of very small amounts might be necessary.
|
||||||
|
|
||||||
|
To allow organizers to make their own choices on this matter, pretix provides the following options:
|
||||||
|
|
||||||
|
Compute taxes for every line individually
|
||||||
|
"""""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
Algorithm identifier: ``line``
|
||||||
|
|
||||||
|
This is our original algorithm where the tax value is rounded for every line individually.
|
||||||
|
|
||||||
|
**This is our current default algorithm and we recommend it whenever you do not have different requirements** (see below).
|
||||||
|
For the example above:
|
||||||
|
|
||||||
|
========== ========== =========== ======= =============
|
||||||
|
Product Tax rate Net price Tax Gross price
|
||||||
|
========== ========== =========== ======= =============
|
||||||
|
Ticket A 19 % 84.03 15.97 100.00
|
||||||
|
Ticket B 19 % 84.03 15.97 100.00
|
||||||
|
Ticket C 19 % 84.03 15.97 100.00
|
||||||
|
Ticket D 19 % 84.03 15.97 100.00
|
||||||
|
Ticket E 19 % 84.03 15.97 100.00
|
||||||
|
Sum 420.15 79.85 500.00
|
||||||
|
========== ========== =========== ======= =============
|
||||||
|
|
||||||
|
|
||||||
|
Compute taxes based on net total
|
||||||
|
""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
Algorithm identifier: ``sum_by_net``
|
||||||
|
|
||||||
|
In this algorithm, the tax value and gross total are computed from the sum of the net prices. To accomplish this within
|
||||||
|
our data model, the gross price and tax of some of the tickets will be changed by the minimum currency unit (e.g. €0.01).
|
||||||
|
The net price of the tickets always stay the same.
|
||||||
|
|
||||||
|
**This is the algorithm intended by EN 16931 invoices and our recommendation to use for e-invoicing when (primarily) business customers are involved.**
|
||||||
|
|
||||||
|
The main downside is that it might be confusing when selling to consumers, since the amounts to be paid change in unexpected ways.
|
||||||
|
For the example above, the customer expects to pay 5 times 100.00, but they are are in fact charged 499.98:
|
||||||
|
|
||||||
|
========== ========== =========== ============================== ==============================
|
||||||
|
Product Tax rate Net price Tax Gross price
|
||||||
|
========== ========== =========== ============================== ==============================
|
||||||
|
Ticket A 19 % 84.03 15.96 (incl. -0.01 rounding) 99.99 (incl. -0.01 rounding)
|
||||||
|
Ticket B 19 % 84.03 15.96 (incl. -0.01 rounding) 99.99 (incl. -0.01 rounding)
|
||||||
|
Ticket C 19 % 84.03 15.97 100.00
|
||||||
|
Ticket D 19 % 84.03 15.97 100.00
|
||||||
|
Ticket E 19 % 84.03 15.97 100.00
|
||||||
|
Sum 420.15 78.83 499.98
|
||||||
|
========== ========== =========== ============================== ==============================
|
||||||
|
|
||||||
|
Compute taxes based on net total with stable gross prices
|
||||||
|
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
Algorithm identifier: ``sum_by_net_keep_gross``
|
||||||
|
|
||||||
|
In this algorithm, the tax value and gross total are computed from the sum of the net prices. However, the net prices
|
||||||
|
of some of the tickets will be changed automatically by the minimum currency unit (e.g. €0.01) such that the resulting
|
||||||
|
gross prices stay the same.
|
||||||
|
|
||||||
|
**This is less confusing to consumers and the end result is still compliant to EN 16931, so we recommend this for e-invoicing when (primarily) consumers are involved.**
|
||||||
|
|
||||||
|
The main downside is that it might be confusing when selling to business customers, since the prices of the identical tickets appear to be different.
|
||||||
|
Full computation for the example above:
|
||||||
|
|
||||||
|
========== ========== ============================= ============================== =============
|
||||||
|
Product Tax rate Net price Tax Gross price
|
||||||
|
========== ========== ============================= ============================== =============
|
||||||
|
Ticket A 19 % 84.04 (incl. 0.01 rounding) 15.96 (incl. -0.01 rounding) 100.00
|
||||||
|
Ticket B 19 % 84.04 (incl. 0.01 rounding) 15.96 (incl. -0.01 rounding) 100.00
|
||||||
|
Ticket C 19 % 84.03 15.97 100.00
|
||||||
|
Ticket D 19 % 84.03 15.97 100.00
|
||||||
|
Ticket E 19 % 84.03 15.97 100.00
|
||||||
|
Sum 420.17 79.83 500.00
|
||||||
|
========== ========== ============================= ============================== =============
|
||||||
|
|||||||
207
doc/development/api/datasync.rst
Normal file
207
doc/development/api/datasync.rst
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
.. highlight:: python
|
||||||
|
:linenothreshold: 5
|
||||||
|
|
||||||
|
Data sync providers
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. warning:: This feature is considered **experimental**. It might change at any time without prior notice.
|
||||||
|
|
||||||
|
pretix provides connectivity to many external services through plugins. A common requirement
|
||||||
|
is unidirectionally sending (order, customer, ticket, ...) data into external systems.
|
||||||
|
The transfer is usually triggered by signals provided by pretix core (e.g. :data:`order_placed`),
|
||||||
|
but performed asynchronously.
|
||||||
|
|
||||||
|
Such plugins should use the :class:`OutboundSyncProvider` API to utilize the queueing, retry and mapping
|
||||||
|
mechanisms as well as the user interface for configuration and monitoring. Sync providers are registered
|
||||||
|
in the :py:attr:`pretix.base.datasync.datasync.datasync_providers` :ref:`registry <registries>`.
|
||||||
|
|
||||||
|
An :class:`OutboundSyncProvider` for subscribing event participants to a mailing list could start
|
||||||
|
like this, for example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from pretix.base.datasync.datasync import (OutboundSyncProvider, datasync_providers)
|
||||||
|
|
||||||
|
@datasync_providers.register
|
||||||
|
class MyListSyncProvider(OutboundSyncProvider):
|
||||||
|
identifier = "my_list"
|
||||||
|
display_name = "My Mailing List Service"
|
||||||
|
# ...
|
||||||
|
|
||||||
|
|
||||||
|
The plugin must register listeners in `signals.py` for all signals that should to trigger a sync and
|
||||||
|
within it has to call :meth:`MyListSyncProvider.enqueue_order` to enqueue the order for synchronization:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@receiver(order_placed, dispatch_uid="mylist_order_placed")
|
||||||
|
def on_order_placed(sender, order, **kwargs):
|
||||||
|
MyListSyncProvider.enqueue_order(order, "order_placed")
|
||||||
|
|
||||||
|
|
||||||
|
Property mappings
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Most of these plugins need to translate data from some pretix objects (e.g. orders)
|
||||||
|
into an external system's data structures. Sometimes, there is only one reasonable way or the
|
||||||
|
plugin author makes an opinionated decision what information from which objects should be
|
||||||
|
transferred into which data structures in the external system.
|
||||||
|
|
||||||
|
Otherwise, you can use a :class:`PropertyMappingFormSet` to let the user set up a mapping from pretix model fields
|
||||||
|
to external data fields. You could store the mapping information either in the event settings, or in a separate
|
||||||
|
data model. Your implementation of :attr:`OutboundSyncProvider.mappings`
|
||||||
|
needs to provide a list of mappings, which can be e.g. static objects or model instances, as long as they
|
||||||
|
have at least the properties defined in
|
||||||
|
:class:`pretix.base.datasync.datasync.StaticMapping`.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# class MyListSyncProvider, contd.
|
||||||
|
def mappings(self):
|
||||||
|
return [
|
||||||
|
StaticMapping(
|
||||||
|
id=1, pretix_model='Order', external_object_type='Contact',
|
||||||
|
pretix_id_field='email', external_id_field='email',
|
||||||
|
property_mappings=self.event.settings.mylist_order_mapping,
|
||||||
|
))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Currently, we support `orders` and `order positions` as data sources, with the data fields defined in
|
||||||
|
:func:`pretix.base.datasync.sourcefields.get_data_fields`.
|
||||||
|
|
||||||
|
To perform the actual sync, implement :func:`sync_object_with_properties` and optionally
|
||||||
|
:func:`finalize_sync_order`. The former is called for each object to be created according to the ``mappings``.
|
||||||
|
For each order that was enqueued using :func:`enqueue_order`:
|
||||||
|
|
||||||
|
- each Mapping with ``pretix_model == "Order"`` results in one call to :func:`sync_object_with_properties`,
|
||||||
|
- each Mapping with ``pretix_model == "OrderPosition"`` results in one call to
|
||||||
|
:func:`sync_object_with_properties` per order position,
|
||||||
|
- :func:`finalize_sync_order` is called one time after all calls to :func:`sync_object_with_properties`.
|
||||||
|
|
||||||
|
|
||||||
|
Implementation examples
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
For example implementations, see the test cases in :mod:`tests.base.test_datasync`.
|
||||||
|
|
||||||
|
In :class:`SimpleOrderSync`, a basic data transfer of order data only is
|
||||||
|
shown. Therein, a ``sync_object_with_properties`` method is defined as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from pretix.base.datasync.utils import assign_properties
|
||||||
|
|
||||||
|
# class MyListSyncProvider, contd.
|
||||||
|
def sync_object_with_properties(
|
||||||
|
self, external_id_field, id_value, properties: list, inputs: dict,
|
||||||
|
mapping, mapped_objects: dict, **kwargs,
|
||||||
|
):
|
||||||
|
# First, we query the external service if our object-to-sync already exists there.
|
||||||
|
# This is necessary to make sure our method is idempotent, i.e. handles already synced
|
||||||
|
# data gracefully.
|
||||||
|
pre_existing_object = self.fake_api_client.retrieve_object(
|
||||||
|
mapping.external_object_type,
|
||||||
|
external_id_field,
|
||||||
|
id_value
|
||||||
|
)
|
||||||
|
|
||||||
|
# We use the helper function ``assign_properties`` to update a pre-existing object.
|
||||||
|
update_values = assign_properties(
|
||||||
|
new_values=properties,
|
||||||
|
old_values=pre_existing_object or {},
|
||||||
|
is_new=pre_existing_object is None,
|
||||||
|
list_sep=";",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then we can send our new data to the external service. The specifics of course depends
|
||||||
|
# on your API, e.g. you may need to use different endpoints for creating or updating an
|
||||||
|
# object, or pass the identifier separately instead of in the same dictionary as the
|
||||||
|
# other properties.
|
||||||
|
result = self.fake_api_client.create_or_update_object(mapping.external_object_type, {
|
||||||
|
**update_values,
|
||||||
|
external_id_field: id_value,
|
||||||
|
"_id": pre_existing_object and pre_existing_object.get("_id"),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Finally, return a dictionary containing at least `object_type`, `external_id_field`,
|
||||||
|
# `id_value`, `external_link_href`, and `external_link_display_name` keys.
|
||||||
|
# Further keys may be provided for your internal use. This dictionary is provided
|
||||||
|
# in following calls in the ``mapped_objects`` dict, to allow creating associations
|
||||||
|
# to this object.
|
||||||
|
return {
|
||||||
|
"object_type": mapping.external_object_type,
|
||||||
|
"external_id_field": external_id_field,
|
||||||
|
"id_value": id_value,
|
||||||
|
"external_link_href": f"https://example.org/external-system/{mapping.external_object_type}/{id_value}/",
|
||||||
|
"external_link_display_name": f"Contact #{id_value} - Jane Doe",
|
||||||
|
"my_result": result,
|
||||||
|
}
|
||||||
|
|
||||||
|
.. note:: The result dictionaries of earlier invocations of :func:`sync_object_with_properties` are
|
||||||
|
only provided in subsequent calls of the same sync run, such that a mapping can
|
||||||
|
refer to e.g. the external id of an object created by a preceding mapping.
|
||||||
|
However, the result dictionaries are currently not provided across runs. This will
|
||||||
|
likely change in a future revision of this API, to allow easier integration of external
|
||||||
|
systems that do not allow retrieving/updating data by a pretix-provided key.
|
||||||
|
|
||||||
|
``mapped_objects`` is a dictionary of lists of dictionaries. The keys to the dictionary are
|
||||||
|
the mapping identifiers (``mapping.id``), the lists contain the result dictionaries returned
|
||||||
|
by :func:`sync_object_with_properties`.
|
||||||
|
|
||||||
|
|
||||||
|
In :class:`OrderAndTicketAssociationSync`, an example is given where orders, order positions,
|
||||||
|
and the association between them are transferred.
|
||||||
|
|
||||||
|
|
||||||
|
The OutboundSyncProvider base class
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
.. autoclass:: pretix.base.datasync.datasync.OutboundSyncProvider
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Property mapping format
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
To allow the user to configure property mappings, you can use the PropertyMappingFormSet,
|
||||||
|
which will generate the required ``property_mappings`` value automatically. If you need
|
||||||
|
to specify the property mappings programmatically, you can refer to the description below
|
||||||
|
on their format.
|
||||||
|
|
||||||
|
.. autoclass:: pretix.control.forms.mapping.PropertyMappingFormSet
|
||||||
|
:members: to_property_mappings_json
|
||||||
|
|
||||||
|
A simple JSON-serialized ``property_mappings`` list for mapping some order information can look like this:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"pretix_field": "email",
|
||||||
|
"external_field": "orderemail",
|
||||||
|
"value_map": "",
|
||||||
|
"overwrite": "overwrite",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pretix_field": "order_status",
|
||||||
|
"external_field": "status",
|
||||||
|
"value_map": "{\"n\": \"pending\", \"p\": \"paid\", \"e\": \"expired\", \"c\": \"canceled\", \"r\": \"refunded\"}",
|
||||||
|
"overwrite": "overwrite",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pretix_field": "order_total",
|
||||||
|
"external_field": "total",
|
||||||
|
"value_map": "",
|
||||||
|
"overwrite": "overwrite",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Translating mappings on Event copy
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Property mappings can contain references to event-specific primary keys. Therefore, plugins must register to the
|
||||||
|
event_copy_data signal and call translate_property_mappings on all property mappings they store.
|
||||||
|
|
||||||
|
.. autofunction:: pretix.base.datasync.utils.translate_property_mappings
|
||||||
@@ -23,21 +23,21 @@ There are multiple signals that will be sent out in the ordering cycle:
|
|||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:no-index:
|
:no-index:
|
||||||
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, build_invoice_data, invoice_line_text
|
||||||
|
|
||||||
Check-ins
|
Check-ins
|
||||||
"""""""""
|
"""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:no-index:
|
:no-index:
|
||||||
:members: checkin_created
|
:members: checkin_created, checkin_annulled
|
||||||
|
|
||||||
|
|
||||||
Frontend
|
Frontend
|
||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
|
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head, filter_subevents
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ Contents:
|
|||||||
email
|
email
|
||||||
placeholder
|
placeholder
|
||||||
invoice
|
invoice
|
||||||
|
invoicetransmission
|
||||||
shredder
|
shredder
|
||||||
import
|
import
|
||||||
customview
|
customview
|
||||||
cookieconsent
|
cookieconsent
|
||||||
auth
|
auth
|
||||||
|
datasync
|
||||||
general
|
general
|
||||||
quality
|
quality
|
||||||
|
|||||||
65
doc/development/api/invoicetransmission.rst
Normal file
65
doc/development/api/invoicetransmission.rst
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.. highlight:: python
|
||||||
|
:linenothreshold: 5
|
||||||
|
|
||||||
|
Writing an invoice transmission plugin
|
||||||
|
======================================
|
||||||
|
|
||||||
|
An invoice transmission provider transports an invoice from the sender to the recipient.
|
||||||
|
There are pre-defined types of invoice transmission in pretix, currently ``"email"``, ``"peppol"``, and ``"it_sdi"``.
|
||||||
|
You can find more information about them at :ref:`rest-transmission-types`.
|
||||||
|
|
||||||
|
New transmission types can not be added by plugins but need to be added to pretix itself.
|
||||||
|
However, plugins can provide implementations for the actual transmission.
|
||||||
|
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
||||||
|
|
||||||
|
Output registration
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
New invoice transmission providers can be registered through the :ref:`registry <registries>` mechanism
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from pretix.base.invoicing.transmission import transmission_providers, TransmissionProvider
|
||||||
|
|
||||||
|
@transmission_providers.new()
|
||||||
|
class SdiTransmissionProvider(TransmissionProvider):
|
||||||
|
identifier = "fatturapa_providerabc"
|
||||||
|
type = "it_sdi"
|
||||||
|
verbose_name = _("FatturaPA through provider ABC")
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
The provider class
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. class:: pretix.base.invoicing.transmission.TransmissionProvider
|
||||||
|
|
||||||
|
.. autoattribute:: identifier
|
||||||
|
|
||||||
|
This is an abstract attribute, you **must** override this!
|
||||||
|
|
||||||
|
.. autoattribute:: type
|
||||||
|
|
||||||
|
This is an abstract attribute, you **must** override this!
|
||||||
|
|
||||||
|
.. autoattribute:: verbose_name
|
||||||
|
|
||||||
|
This is an abstract attribute, you **must** override this!
|
||||||
|
|
||||||
|
.. autoattribute:: priority
|
||||||
|
|
||||||
|
.. autoattribute:: testmode_supported
|
||||||
|
|
||||||
|
.. automethod:: is_ready
|
||||||
|
|
||||||
|
This is an abstract method, you **must** override this!
|
||||||
|
|
||||||
|
.. automethod:: is_available
|
||||||
|
|
||||||
|
This is an abstract method, you **must** override this!
|
||||||
|
|
||||||
|
.. automethod:: transmit
|
||||||
|
|
||||||
|
This is an abstract method, you **must** override this!
|
||||||
|
|
||||||
|
.. automethod:: settings_url
|
||||||
@@ -56,6 +56,20 @@ restricted boolean (optional) ``False`` by default, restricts a plugin
|
|||||||
for an event by system administrators / superusers.
|
for an event by system administrators / superusers.
|
||||||
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
|
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
|
||||||
compatibility string Specifier for compatible pretix versions.
|
compatibility string Specifier for compatible pretix versions.
|
||||||
|
level string System level the plugin can be activated at.
|
||||||
|
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT`` for plugins that can be activated
|
||||||
|
at event level and then be active for that event only.
|
||||||
|
Set to ``pretix.base.plugins.PLUGIN_LEVEL_ORGANIZER`` for plugins that can be
|
||||||
|
activated only for the organizer as a whole and are active for any event within
|
||||||
|
that organizer.
|
||||||
|
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID`` for plugins that
|
||||||
|
can be activated at organizer level but are considered active only within events
|
||||||
|
for which they have also been specifically activated.
|
||||||
|
More levels, e.g. user-level plugins, might be invented in the future.
|
||||||
|
settings_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
|
||||||
|
to the plugin's settings.
|
||||||
|
navigation_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
|
||||||
|
to the plugin's system pages.
|
||||||
================== ==================== ===========================================================
|
================== ==================== ===========================================================
|
||||||
|
|
||||||
A working example would be:
|
A working example would be:
|
||||||
@@ -63,9 +77,9 @@ A working example would be:
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pretix.base.plugins import PluginConfig
|
from pretix.base.plugins import PluginConfig, PLUGIN_LEVEL_EVENT
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
raise RuntimeError("Please use pretix 2025.7 or above to run this plugin!")
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +93,7 @@ A working example would be:
|
|||||||
version = '1.0.0'
|
version = '1.0.0'
|
||||||
category = 'PAYMENT'
|
category = 'PAYMENT'
|
||||||
picture = 'pretix_paypal/paypal_logo.svg'
|
picture = 'pretix_paypal/paypal_logo.svg'
|
||||||
|
level = PLUGIN_LEVEL_EVENT
|
||||||
visible = True
|
visible = True
|
||||||
featured = False
|
featured = False
|
||||||
restricted = False
|
restricted = False
|
||||||
@@ -142,14 +157,14 @@ method to make your receivers available:
|
|||||||
from . import signals # NOQA
|
from . import signals # NOQA
|
||||||
|
|
||||||
You can optionally specify code that is executed when your plugin is activated for an event
|
You can optionally specify code that is executed when your plugin is activated for an event
|
||||||
in the ``installed`` method:
|
or organizer in the ``installed`` method:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class PaypalApp(AppConfig):
|
class PaypalApp(AppConfig):
|
||||||
…
|
…
|
||||||
|
|
||||||
def installed(self, event):
|
def installed(self, event_or_organizer):
|
||||||
pass # Your code here
|
pass # Your code here
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Development setup
|
|||||||
|
|
||||||
This tutorial helps you to get started hacking with pretix on your own computer. You need this to
|
This tutorial helps you to get started hacking with pretix on your own computer. You need this to
|
||||||
be able to contribute to pretix, but it might also be helpful if you want to write your own plugins.
|
be able to contribute to pretix, but it might also be helpful if you want to write your own plugins.
|
||||||
If you want to install pretix on a server for actual usage, go to the [administrator documentation](https://docs.pretix.eu/self-hosting/) instead.
|
If you want to install pretix on a server for actual usage, go to the `administrator documentation`_ instead.
|
||||||
|
|
||||||
Obtain a copy of the source code
|
Obtain a copy of the source code
|
||||||
--------------------------------
|
--------------------------------
|
||||||
@@ -221,3 +221,4 @@ your virtual environment.::
|
|||||||
|
|
||||||
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
||||||
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
||||||
|
.. _administrator documentation: https://docs.pretix.eu/self-hosting/
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 49 KiB |
@@ -23,6 +23,7 @@ partition "For every cart position" {
|
|||||||
--> "Store as line_price (gross), tax_rate"
|
--> "Store as line_price (gross), tax_rate"
|
||||||
}
|
}
|
||||||
--> "Apply discount engine"
|
--> "Apply discount engine"
|
||||||
|
--> "Apply tax rounding"
|
||||||
--> "Store as price (gross)"
|
--> "Store as price (gross)"
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pyenchant==3.2.*
|
pyenchant==3.3.*
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pyenchant==3.2.*
|
pyenchant==3.3.*
|
||||||
|
|||||||
@@ -28,23 +28,23 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.13.*",
|
"BeautifulSoup4==4.14.*",
|
||||||
"bleach==6.2.*",
|
"bleach==6.2.*",
|
||||||
"celery==5.5.*",
|
"celery==5.5.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=44.0.0",
|
"cryptography>=44.0.0",
|
||||||
"css-inline==0.15.*",
|
"css-inline==0.17.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"Django[argon2]==4.2.*,>=4.2.15",
|
"Django[argon2]==4.2.*,>=4.2.24",
|
||||||
"django-bootstrap3==25.1",
|
"django-bootstrap3==25.2",
|
||||||
"django-compressor==4.5.1",
|
"django-compressor==4.5.1",
|
||||||
"django-countries==7.6.*",
|
"django-countries==7.6.*",
|
||||||
"django-filter==25.1",
|
"django-filter==25.1",
|
||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.4",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==1.2.*",
|
"django-hierarkey==2.0.*,>=2.0.1",
|
||||||
"django-hijack==3.7.*",
|
"django-hijack==3.7.*",
|
||||||
"django-i18nfield==1.10.*",
|
"django-i18nfield==1.11.*",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==5.0",
|
"django-localflavor==5.0",
|
||||||
"django-markup",
|
"django-markup",
|
||||||
@@ -64,7 +64,7 @@ dependencies = [
|
|||||||
"kombu==5.5.*",
|
"kombu==5.5.*",
|
||||||
"libsass==0.23.*",
|
"libsass==0.23.*",
|
||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.8.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.9", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||||
"mt-940==4.30.*",
|
"mt-940==4.30.*",
|
||||||
"oauthlib==3.3.*",
|
"oauthlib==3.3.*",
|
||||||
@@ -76,23 +76,23 @@ dependencies = [
|
|||||||
"phonenumberslite==9.0.*",
|
"phonenumberslite==9.0.*",
|
||||||
"Pillow==11.3.*",
|
"Pillow==11.3.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==6.31.*",
|
"protobuf==6.33.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.22",
|
"pycparser==2.23",
|
||||||
"pycryptodome==3.23.*",
|
"pycryptodome==3.23.*",
|
||||||
"pypdf==5.7.*",
|
"pypdf==6.1.*",
|
||||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==8.2",
|
"qrcode==8.2",
|
||||||
"redis==6.2.*",
|
"redis==6.4.*",
|
||||||
"reportlab==4.4.*",
|
"reportlab==4.4.*",
|
||||||
"requests==2.31.*",
|
"requests==2.32.*",
|
||||||
"sentry-sdk==2.31.*",
|
"sentry-sdk==2.43.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.7.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
@@ -100,27 +100,27 @@ dependencies = [
|
|||||||
"ua-parser==1.0.*",
|
"ua-parser==1.0.*",
|
||||||
"vat_moss_forked==2020.3.20.0.11.0",
|
"vat_moss_forked==2020.3.20.0.11.0",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==2.6.*",
|
"webauthn==2.7.*",
|
||||||
"zeep==4.3.*"
|
"zeep==4.3.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.12.*",
|
"aiohttp==3.13.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.30.*",
|
"fakeredis==2.32.*",
|
||||||
"flake8==7.3.*",
|
"flake8==7.3.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==6.0.*",
|
"isort==6.1.*",
|
||||||
"pep8-naming==0.15.*",
|
"pep8-naming==0.15.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio>=0.24",
|
"pytest-asyncio>=0.24",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
"pytest-mock==3.14.*",
|
"pytest-mock==3.15.*",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.8.*",
|
"pytest-xdist==3.8.*",
|
||||||
"pytest==8.4.*",
|
"pytest==8.4.*",
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ coverage:
|
|||||||
coverage run -m py.test
|
coverage run -m py.test
|
||||||
|
|
||||||
npminstall:
|
npminstall:
|
||||||
# keep this in sync with setup.py!
|
# keep this in sync with pretix/_build.py!
|
||||||
mkdir -p pretix/static.dist/node_prefix/
|
mkdir -p pretix/static.dist/node_prefix/
|
||||||
cp -r pretix/static/npm_dir/* pretix/static.dist/node_prefix/
|
cp -r pretix/static/npm_dir/* pretix/static.dist/node_prefix/
|
||||||
npm install --prefix=pretix/static.dist/node_prefix
|
npm ci --prefix=pretix/static.dist/node_prefix
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2025.7.0.dev0"
|
__version__ = "2025.10.0.dev0"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -115,6 +115,7 @@ ALL_LANGUAGES = [
|
|||||||
('sk', _('Slovak')),
|
('sk', _('Slovak')),
|
||||||
('sv', _('Swedish')),
|
('sv', _('Swedish')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
|
('es-419', _('Spanish (Latin America)')),
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('uk', _('Ukrainian')),
|
('uk', _('Ukrainian')),
|
||||||
]
|
]
|
||||||
@@ -172,6 +173,12 @@ EXTRA_LANG_INFO = {
|
|||||||
'name': 'Norwegian Bokmal',
|
'name': 'Norwegian Bokmal',
|
||||||
'name_local': 'norsk (bokmål)',
|
'name_local': 'norsk (bokmål)',
|
||||||
},
|
},
|
||||||
|
'es-419': {
|
||||||
|
'bidi': False,
|
||||||
|
'code': 'es-419',
|
||||||
|
'name': 'Spanish (Latin America)',
|
||||||
|
'name_local': 'Español',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -39,7 +39,7 @@ def npm_install():
|
|||||||
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
|
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
|
||||||
os.makedirs(node_prefix, exist_ok=True)
|
os.makedirs(node_prefix, exist_ok=True)
|
||||||
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
|
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
|
||||||
subprocess.check_call('npm install', shell=True, cwd=node_prefix)
|
subprocess.check_call('npm ci', shell=True, cwd=node_prefix)
|
||||||
npm_installed = True
|
npm_installed = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -104,3 +104,14 @@ class MiniCheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckinRPCAnnulInputSerializer(serializers.Serializer):
|
||||||
|
lists = serializers.PrimaryKeyRelatedField(required=True, many=True, queryset=CheckinList.objects.none())
|
||||||
|
nonce = serializers.CharField(required=True, allow_null=False)
|
||||||
|
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
||||||
|
error_explanation = serializers.CharField(required=False, allow_null=True)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -50,6 +50,7 @@ from rest_framework.relations import SlugRelatedField
|
|||||||
from pretix.api.serializers import (
|
from pretix.api.serializers import (
|
||||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||||
)
|
)
|
||||||
|
from pretix.api.serializers.fields import PluginsField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -61,6 +62,9 @@ from pretix.base.models.items import (
|
|||||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||||
)
|
)
|
||||||
from pretix.base.models.tax import CustomRulesValidator
|
from pretix.base.models.tax import CustomRulesValidator
|
||||||
|
from pretix.base.plugins import (
|
||||||
|
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||||
|
)
|
||||||
from pretix.base.services.seating import (
|
from pretix.base.services.seating import (
|
||||||
SeatProtected, generate_seats, validate_plan_change,
|
SeatProtected, generate_seats, validate_plan_change,
|
||||||
)
|
)
|
||||||
@@ -126,22 +130,6 @@ class SeatCategoryMappingField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PluginsField(Field):
|
|
||||||
|
|
||||||
def to_representation(self, obj):
|
|
||||||
from pretix.base.plugins import get_all_plugins
|
|
||||||
|
|
||||||
return sorted([
|
|
||||||
p.module for p in get_all_plugins()
|
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
|
||||||
])
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
return {
|
|
||||||
'plugins': data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TimeZoneField(ChoiceField):
|
class TimeZoneField(ChoiceField):
|
||||||
def get_attribute(self, instance):
|
def get_attribute(self, instance):
|
||||||
return instance.cache.get_or_set(
|
return instance.cache.get_or_set(
|
||||||
@@ -283,17 +271,28 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
plugins_available = {
|
plugins_available = {
|
||||||
p.module: p for p in get_all_plugins(self.instance)
|
p.module: p for p in get_all_plugins(event=self.instance)
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||||
}
|
}
|
||||||
|
current_plugins = self.instance.get_plugins() if self.instance and self.instance.pk else []
|
||||||
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
||||||
|
|
||||||
|
allowed_levels = (PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
||||||
for plugin in value.get('plugins'):
|
for plugin in value.get('plugins'):
|
||||||
if plugin not in plugins_available:
|
if plugin not in plugins_available:
|
||||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||||
if getattr(plugins_available[plugin], 'restricted', False):
|
if getattr(plugins_available[plugin], 'restricted', False):
|
||||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||||
|
level = getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT)
|
||||||
|
if level not in allowed_levels:
|
||||||
|
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
|
||||||
|
|
||||||
|
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and plugin not in self.context['organizer'].get_plugins():
|
||||||
|
if plugin not in current_plugins:
|
||||||
|
# Technically, this is allowed, but consumers might be confused if the API call doesn't do anything
|
||||||
|
# so we prevent this change.
|
||||||
|
raise ValidationError('Plugin should be enabled on organizer level first: \'{name}\'.'.format(name=plugin))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -806,6 +805,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_reissue_after_modify',
|
'invoice_reissue_after_modify',
|
||||||
'invoice_include_free',
|
'invoice_include_free',
|
||||||
'invoice_generate',
|
'invoice_generate',
|
||||||
|
'invoice_period',
|
||||||
'invoice_numbers_consecutive',
|
'invoice_numbers_consecutive',
|
||||||
'invoice_numbers_prefix',
|
'invoice_numbers_prefix',
|
||||||
'invoice_numbers_prefix_cancellations',
|
'invoice_numbers_prefix_cancellations',
|
||||||
@@ -829,6 +829,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_eu_currencies',
|
'invoice_eu_currencies',
|
||||||
'invoice_logo_image',
|
'invoice_logo_image',
|
||||||
'invoice_renderer_highlight_order_code',
|
'invoice_renderer_highlight_order_code',
|
||||||
|
'tax_rounding',
|
||||||
'cancel_allow_user',
|
'cancel_allow_user',
|
||||||
'cancel_allow_user_until',
|
'cancel_allow_user_until',
|
||||||
'cancel_allow_user_unpaid_keep',
|
'cancel_allow_user_unpaid_keep',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,45 +19,16 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from django import forms
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.api.serializers.forms import form_field_to_serializer_field
|
||||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||||
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
|
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
|
||||||
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
|
from pretix.base.timeframes import SerializerDateFrameField
|
||||||
|
|
||||||
|
|
||||||
class FormFieldWrapperField(serializers.Field):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.form_field = kwargs.pop('form_field')
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
return self.form_field.widget.format_value(value)
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
|
|
||||||
d = self.form_field.clean(d)
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
simple_mappings = (
|
|
||||||
(forms.DateField, serializers.DateField, ()),
|
|
||||||
(forms.TimeField, serializers.TimeField, ()),
|
|
||||||
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
|
|
||||||
(forms.DateTimeField, serializers.DateTimeField, ()),
|
|
||||||
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
|
||||||
(forms.FloatField, serializers.FloatField, ()),
|
|
||||||
(forms.IntegerField, serializers.IntegerField, ()),
|
|
||||||
(forms.EmailField, serializers.EmailField, ()),
|
|
||||||
(forms.UUIDField, serializers.UUIDField, ()),
|
|
||||||
(forms.URLField, serializers.URLField, ()),
|
|
||||||
(forms.BooleanField, serializers.BooleanField, ()),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SerializerDescriptionField(serializers.Field):
|
class SerializerDescriptionField(serializers.Field):
|
||||||
@@ -81,13 +52,6 @@ class ExporterSerializer(serializers.Serializer):
|
|||||||
input_parameters = SerializerDescriptionField(source='_serializer')
|
input_parameters = SerializerDescriptionField(source='_serializer')
|
||||||
|
|
||||||
|
|
||||||
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
|
||||||
def to_representation(self, value):
|
|
||||||
if isinstance(value, int):
|
|
||||||
return value
|
|
||||||
return super().to_representation(value)
|
|
||||||
|
|
||||||
|
|
||||||
class JobRunSerializer(serializers.Serializer):
|
class JobRunSerializer(serializers.Serializer):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
ex = kwargs.pop('exporter')
|
ex = kwargs.pop('exporter')
|
||||||
@@ -102,59 +66,7 @@ class JobRunSerializer(serializers.Serializer):
|
|||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
for k, v in ex.export_form_fields.items():
|
for k, v in ex.export_form_fields.items():
|
||||||
for m_from, m_to, m_kwargs in simple_mappings:
|
self.fields[k] = form_field_to_serializer_field(v)
|
||||||
if isinstance(v, m_from):
|
|
||||||
self.fields[k] = m_to(
|
|
||||||
required=v.required,
|
|
||||||
allow_null=not v.required,
|
|
||||||
validators=v.validators,
|
|
||||||
**{kwarg: getattr(v, kwargs, None) for kwarg in m_kwargs}
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
if isinstance(v, forms.NullBooleanField):
|
|
||||||
self.fields[k] = serializers.BooleanField(
|
|
||||||
required=v.required,
|
|
||||||
allow_null=True,
|
|
||||||
validators=v.validators,
|
|
||||||
)
|
|
||||||
if isinstance(v, forms.ModelMultipleChoiceField):
|
|
||||||
self.fields[k] = PrimaryKeyRelatedField(
|
|
||||||
queryset=v.queryset,
|
|
||||||
required=v.required,
|
|
||||||
allow_empty=not v.required,
|
|
||||||
validators=v.validators,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
elif isinstance(v, forms.ModelChoiceField):
|
|
||||||
self.fields[k] = PrimaryKeyRelatedField(
|
|
||||||
queryset=v.queryset,
|
|
||||||
required=v.required,
|
|
||||||
allow_null=not v.required,
|
|
||||||
validators=v.validators,
|
|
||||||
)
|
|
||||||
elif isinstance(v, forms.MultipleChoiceField):
|
|
||||||
self.fields[k] = serializers.MultipleChoiceField(
|
|
||||||
choices=v.choices,
|
|
||||||
required=v.required,
|
|
||||||
allow_empty=not v.required,
|
|
||||||
validators=v.validators,
|
|
||||||
)
|
|
||||||
elif isinstance(v, forms.ChoiceField):
|
|
||||||
self.fields[k] = serializers.ChoiceField(
|
|
||||||
choices=v.choices,
|
|
||||||
required=v.required,
|
|
||||||
allow_null=not v.required,
|
|
||||||
validators=v.validators,
|
|
||||||
)
|
|
||||||
elif isinstance(v, DateFrameField):
|
|
||||||
self.fields[k] = SerializerDateFrameField(
|
|
||||||
required=v.required,
|
|
||||||
allow_null=not v.required,
|
|
||||||
validators=v.validators,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required)
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if isinstance(data, QueryDict):
|
if isinstance(data, QueryDict):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -109,3 +109,19 @@ class UploadedFileField(serializers.Field):
|
|||||||
return None
|
return None
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
return request.build_absolute_uri(url)
|
return request.build_absolute_uri(url)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginsField(serializers.Field):
|
||||||
|
|
||||||
|
def to_representation(self, obj):
|
||||||
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
|
return sorted([
|
||||||
|
p.module for p in get_all_plugins()
|
||||||
|
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
||||||
|
])
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return {
|
||||||
|
'plugins': data
|
||||||
|
}
|
||||||
|
|||||||
115
src/pretix/api/serializers/forms.py
Normal file
115
src/pretix/api/serializers/forms.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#
|
||||||
|
# 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 forms
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
|
||||||
|
|
||||||
|
simple_mappings = (
|
||||||
|
(forms.DateField, serializers.DateField, ()),
|
||||||
|
(forms.TimeField, serializers.TimeField, ()),
|
||||||
|
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
|
||||||
|
(forms.DateTimeField, serializers.DateTimeField, ()),
|
||||||
|
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
||||||
|
(forms.FloatField, serializers.FloatField, ()),
|
||||||
|
(forms.IntegerField, serializers.IntegerField, ()),
|
||||||
|
(forms.EmailField, serializers.EmailField, ()),
|
||||||
|
(forms.UUIDField, serializers.UUIDField, ()),
|
||||||
|
(forms.URLField, serializers.URLField, ()),
|
||||||
|
(forms.BooleanField, serializers.BooleanField, ()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||||
|
def to_representation(self, value):
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
return super().to_representation(value)
|
||||||
|
|
||||||
|
|
||||||
|
class FormFieldWrapperField(serializers.Field):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.form_field = kwargs.pop('form_field')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return self.form_field.widget.format_value(value)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
|
||||||
|
d = self.form_field.clean(d)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def form_field_to_serializer_field(field):
|
||||||
|
for m_from, m_to, m_kwargs in simple_mappings:
|
||||||
|
if isinstance(field, m_from):
|
||||||
|
return m_to(
|
||||||
|
required=field.required,
|
||||||
|
allow_null=not field.required,
|
||||||
|
validators=field.validators,
|
||||||
|
**{kwarg: getattr(field, kwarg, None) for kwarg in m_kwargs}
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(field, forms.NullBooleanField):
|
||||||
|
return serializers.BooleanField(
|
||||||
|
required=field.required,
|
||||||
|
allow_null=True,
|
||||||
|
validators=field.validators,
|
||||||
|
)
|
||||||
|
if isinstance(field, forms.ModelMultipleChoiceField):
|
||||||
|
return PrimaryKeyRelatedField(
|
||||||
|
queryset=field.queryset,
|
||||||
|
required=field.required,
|
||||||
|
allow_empty=not field.required,
|
||||||
|
validators=field.validators,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
elif isinstance(field, forms.ModelChoiceField):
|
||||||
|
return PrimaryKeyRelatedField(
|
||||||
|
queryset=field.queryset,
|
||||||
|
required=field.required,
|
||||||
|
allow_null=not field.required,
|
||||||
|
validators=field.validators,
|
||||||
|
)
|
||||||
|
elif isinstance(field, forms.MultipleChoiceField):
|
||||||
|
return serializers.MultipleChoiceField(
|
||||||
|
choices=field.choices,
|
||||||
|
required=field.required,
|
||||||
|
allow_empty=not field.required,
|
||||||
|
validators=field.validators,
|
||||||
|
)
|
||||||
|
elif isinstance(field, forms.ChoiceField):
|
||||||
|
return serializers.ChoiceField(
|
||||||
|
choices=field.choices,
|
||||||
|
required=field.required,
|
||||||
|
allow_null=not field.required,
|
||||||
|
validators=field.validators,
|
||||||
|
)
|
||||||
|
elif isinstance(field, DateFrameField):
|
||||||
|
return SerializerDateFrameField(
|
||||||
|
required=field.required,
|
||||||
|
allow_null=not field.required,
|
||||||
|
validators=field.validators,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return FormFieldWrapperField(form_field=field, required=field.required, allow_null=not field.required)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -550,7 +550,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||||
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||||
|
|
||||||
Question.clean_items(event, full_data.get('items'))
|
Question.clean_items(event, full_data.get('items') or [])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate_options(self, value):
|
def validate_options(self, value):
|
||||||
@@ -566,7 +566,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
||||||
items = validated_data.pop('items')
|
items = validated_data.pop('items', [])
|
||||||
|
|
||||||
question = Question.objects.create(**validated_data)
|
question = Question.objects.create(**validated_data)
|
||||||
question.items.set(items)
|
question.items.set(items)
|
||||||
@@ -582,7 +582,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Quota
|
model = Quota
|
||||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out',
|
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out',
|
||||||
'release_after_exit', 'available', 'available_number')
|
'release_after_exit', 'available', 'available_number', 'ignore_for_event_availability')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -42,6 +42,7 @@ from rest_framework.reverse import reverse
|
|||||||
|
|
||||||
from pretix.api.serializers import CompatibleJSONField
|
from pretix.api.serializers import CompatibleJSONField
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
|
from pretix.api.serializers.forms import form_field_to_serializer_field
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||||
@@ -49,10 +50,12 @@ from pretix.api.serializers.item import (
|
|||||||
from pretix.api.signals import order_api_details, orderposition_api_details
|
from pretix.api.signals import order_api_details, orderposition_api_details
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
|
from pretix.base.invoicing.transmission import get_transmission_types
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
CachedFile, Checkin, Customer, Device, Invoice, InvoiceAddress,
|
||||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
||||||
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
||||||
|
Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
@@ -62,10 +65,13 @@ from pretix.base.pdf import get_images, get_variables
|
|||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
||||||
from pretix.base.services.pricing import (
|
from pretix.base.services.pricing import (
|
||||||
apply_discounts, get_line_price, get_listed_price, is_included_for_free,
|
apply_discounts, apply_rounding, get_line_price, get_listed_price,
|
||||||
|
is_included_for_free,
|
||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import (
|
||||||
|
COUNTRIES_WITH_STATE_IN_ADDRESS, ROUNDING_MODES,
|
||||||
|
)
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
@@ -102,6 +108,13 @@ class CountryField(serializers.Field):
|
|||||||
return str(src) if src else None
|
return str(src) if src else None
|
||||||
|
|
||||||
|
|
||||||
|
class TransmissionInfoSerializer(serializers.Serializer):
|
||||||
|
def __init__(self, *args, transmission_type, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for k, v in transmission_type.invoice_address_form_fields.items():
|
||||||
|
self.fields[k] = form_field_to_serializer_field(v)
|
||||||
|
|
||||||
|
|
||||||
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||||
country = CompatibleCountryField(source='*')
|
country = CompatibleCountryField(source='*')
|
||||||
name = serializers.CharField(required=False)
|
name = serializers.CharField(required=False)
|
||||||
@@ -109,7 +122,8 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceAddress
|
model = InvoiceAddress
|
||||||
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
||||||
'state', 'vat_id', 'vat_id_validated', 'custom_field', 'internal_reference')
|
'state', 'vat_id', 'vat_id_validated', 'custom_field', 'internal_reference', 'transmission_type',
|
||||||
|
'transmission_info')
|
||||||
read_only_fields = ('last_modified',)
|
read_only_fields = ('last_modified',)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -147,6 +161,48 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|||||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if data.get("transmission_type"):
|
||||||
|
for t in get_transmission_types():
|
||||||
|
if data.get("transmission_type") == t.identifier:
|
||||||
|
if not t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
|
||||||
|
raise ValidationError({
|
||||||
|
"transmission_type": "The selected transmission type is not available for this country or address type."
|
||||||
|
})
|
||||||
|
|
||||||
|
ts = TransmissionInfoSerializer(transmission_type=t, data=data.get("transmission_info", {}))
|
||||||
|
try:
|
||||||
|
ts.is_valid(raise_exception=True)
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ValidationError(
|
||||||
|
{"transmission_info": e.detail}
|
||||||
|
)
|
||||||
|
data["transmission_info"] = ts.validated_data
|
||||||
|
|
||||||
|
required_fields = t.invoice_address_form_fields_required(data.get("country"), data.get("is_business"))
|
||||||
|
for r in required_fields:
|
||||||
|
if r in self.fields:
|
||||||
|
if not data.get(r):
|
||||||
|
raise ValidationError(
|
||||||
|
{r: "This field is required for the selected type of invoice transmission."}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not ts.validated_data.get(r):
|
||||||
|
raise ValidationError(
|
||||||
|
{"transmission_info": {r: "This field is required for the selected type of invoice transmission."}}
|
||||||
|
)
|
||||||
|
break # do not call else branch of for loop
|
||||||
|
elif t.exclusive:
|
||||||
|
if t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
|
||||||
|
raise ValidationError({
|
||||||
|
"transmission_type": "The transmission type '%s' must be used for this country or address type." % (
|
||||||
|
t.identifier,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
raise ValidationError(
|
||||||
|
{"transmission_type": "Unknown transmission type."}
|
||||||
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -273,7 +329,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class CheckinSerializer(I18nAwareModelSerializer):
|
class InlineCheckinSerializer(I18nAwareModelSerializer):
|
||||||
device_id = serializers.SlugRelatedField(
|
device_id = serializers.SlugRelatedField(
|
||||||
source='device',
|
source='device',
|
||||||
slug_field='device_id',
|
slug_field='device_id',
|
||||||
@@ -285,6 +341,21 @@ class CheckinSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
||||||
|
|
||||||
|
|
||||||
|
class CheckinSerializer(I18nAwareModelSerializer):
|
||||||
|
device_id = serializers.SlugRelatedField(
|
||||||
|
source='device',
|
||||||
|
slug_field='device_id',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Checkin
|
||||||
|
fields = (
|
||||||
|
'id', 'successful', 'error_reason', 'error_explanation', 'position', 'datetime', 'list', 'created',
|
||||||
|
'auto_checked_in', 'gate', 'device', 'device_id', 'type'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PrintLogSerializer(serializers.ModelSerializer):
|
class PrintLogSerializer(serializers.ModelSerializer):
|
||||||
device_id = serializers.SlugRelatedField(
|
device_id = serializers.SlugRelatedField(
|
||||||
source='device',
|
source='device',
|
||||||
@@ -508,7 +579,7 @@ class OrderPositionPluginDataField(serializers.Field):
|
|||||||
|
|
||||||
|
|
||||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||||
checkins = CheckinSerializer(many=True, read_only=True)
|
checkins = InlineCheckinSerializer(many=True, read_only=True)
|
||||||
print_logs = PrintLogSerializer(many=True, read_only=True)
|
print_logs = PrintLogSerializer(many=True, read_only=True)
|
||||||
answers = AnswerSerializer(many=True)
|
answers = AnswerSerializer(many=True)
|
||||||
downloads = PositionDownloadsField(source='*', read_only=True)
|
downloads = PositionDownloadsField(source='*', read_only=True)
|
||||||
@@ -781,14 +852,15 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
list_serializer_class = OrderListSerializer
|
list_serializer_class = OrderListSerializer
|
||||||
fields = (
|
fields = (
|
||||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'tax_rounding_mode', 'comment', 'custom_followup_at', 'invoice_address',
|
||||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
'positions', 'downloads', 'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds',
|
||||||
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date', 'plugin_data',
|
'require_approval', 'sales_channel', 'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date',
|
||||||
|
'plugin_data',
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
'payment_provider', 'fees', 'total', 'tax_rounding_mode', 'positions', 'downloads', 'customer',
|
||||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
|
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -953,7 +1025,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
||||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
|
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
|
||||||
'requested_valid_from', 'use_reusable_medium')
|
'requested_valid_from', 'use_reusable_medium', 'discount')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -1049,6 +1121,10 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if data.get('price') is None and data.get('discount'):
|
||||||
|
raise ValidationError(
|
||||||
|
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -1103,11 +1179,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
queryset=SalesChannel.objects.none(),
|
queryset=SalesChannel.objects.none(),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
|
||||||
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||||
|
self.fields['positions'].child.fields['discount'].queryset = self.context['event'].discounts.all()
|
||||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||||
self.fields['expires'].required = False
|
self.fields['expires'].required = False
|
||||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
@@ -1118,7 +1196,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||||
'require_approval', 'valid_if_pending', 'expires', 'api_meta')
|
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -1515,19 +1593,22 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||||
|
|
||||||
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
||||||
discount_results = apply_discounts(
|
if not any([p.get("discount") for p in positions_data]):
|
||||||
self.context['event'],
|
# If any discount is set by the client (i.e. pretixPOS), we do not recalculate but believe the client
|
||||||
order.sales_channel,
|
# to avoid differences in end results.
|
||||||
[
|
discount_results = apply_discounts(
|
||||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
self.context['event'],
|
||||||
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
order.sales_channel,
|
||||||
for cp in order_positions
|
[
|
||||||
]
|
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
||||||
)
|
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||||
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
for cp in order_positions
|
||||||
if new_price != pos.price and pos._auto_generated_price:
|
]
|
||||||
pos.price = new_price
|
)
|
||||||
pos.discount = discount
|
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
||||||
|
if new_price != pos.price and pos._auto_generated_price:
|
||||||
|
pos.price = new_price
|
||||||
|
pos.discount = discount
|
||||||
|
|
||||||
# Save instances
|
# Save instances
|
||||||
for pos_data in positions_data:
|
for pos_data in positions_data:
|
||||||
@@ -1641,7 +1722,31 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
order.total += sum([f.value for f in fees])
|
rounding_mode = validated_data.get("tax_rounding_mode")
|
||||||
|
if not rounding_mode:
|
||||||
|
if isinstance(self.context.get("auth"), Device):
|
||||||
|
# Safety fallback to avoid differences in tax reporting
|
||||||
|
brand = self.context.get("auth").software_brand or ""
|
||||||
|
if "pretixPOS" in brand or "pretixKIOSK" in brand:
|
||||||
|
rounding_mode = "line"
|
||||||
|
if not rounding_mode:
|
||||||
|
rounding_mode = self.context["event"].settings.tax_rounding
|
||||||
|
changed = apply_rounding(
|
||||||
|
rounding_mode,
|
||||||
|
self.context["event"].currency,
|
||||||
|
[*pos_map.values(), *fees]
|
||||||
|
)
|
||||||
|
for line in changed:
|
||||||
|
if isinstance(line, OrderPosition):
|
||||||
|
line.save(update_fields=[
|
||||||
|
"price", "price_includes_rounding_correction", "tax_value", "tax_value_includes_rounding_correction"
|
||||||
|
])
|
||||||
|
elif isinstance(line, OrderFee):
|
||||||
|
line.save(update_fields=[
|
||||||
|
"value", "value_includes_rounding_correction", "tax_value", "tax_value_includes_rounding_correction"
|
||||||
|
])
|
||||||
|
|
||||||
|
order.total = sum([c.price for c in pos_map.values()]) + sum([f.value for f in fees])
|
||||||
if simulate:
|
if simulate:
|
||||||
order.fees = fees
|
order.fees = fees
|
||||||
order.positions = pos_map.values()
|
order.positions = pos_map.values()
|
||||||
@@ -1705,12 +1810,14 @@ class LinePositionField(serializers.IntegerField):
|
|||||||
|
|
||||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||||
position = LinePositionField(read_only=True)
|
position = LinePositionField(read_only=True)
|
||||||
|
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
|
||||||
|
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceLine
|
model = InvoiceLine
|
||||||
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
||||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_code', 'tax_name', 'fee_type',
|
'event_date_to', 'period_start', 'period_end', 'gross_value', 'tax_value', 'tax_rate', 'tax_code',
|
||||||
'fee_internal_type', 'event_location')
|
'tax_name', 'fee_type', 'fee_internal_type', 'event_location')
|
||||||
|
|
||||||
|
|
||||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||||
@@ -1725,12 +1832,13 @@ class InvoiceSerializer(I18nAwareModelSerializer):
|
|||||||
model = Invoice
|
model = Invoice
|
||||||
fields = ('event', 'order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
|
fields = ('event', 'order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
|
||||||
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
|
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
|
||||||
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
|
'invoice_to', 'invoice_to_is_business', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street',
|
||||||
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
|
'invoice_to_zipcode', 'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id',
|
||||||
'custom_field', 'date', 'refers', 'locale',
|
'invoice_to_beneficiary', 'invoice_to_transmission_info', 'custom_field', 'date', 'refers', 'locale',
|
||||||
'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp',
|
'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp',
|
||||||
'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate',
|
'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate',
|
||||||
'foreign_currency_rate_date', 'internal_reference')
|
'foreign_currency_rate_date', 'internal_reference', 'transmission_type', 'transmission_provider',
|
||||||
|
'transmission_status', 'transmission_date')
|
||||||
|
|
||||||
|
|
||||||
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
|
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -83,6 +83,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
|||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ocm = self.context['ocm']
|
ocm = self.context['ocm']
|
||||||
|
check_quotas = self.context.get('check_quotas', True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ocm.add_position(
|
ocm.add_position(
|
||||||
@@ -96,7 +97,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
|||||||
valid_until=validated_data.get('valid_until'),
|
valid_until=validated_data.get('valid_until'),
|
||||||
)
|
)
|
||||||
if self.context.get('commit', True):
|
if self.context.get('commit', True):
|
||||||
ocm.commit()
|
ocm.commit(check_quotas=check_quotas)
|
||||||
return validated_data['order'].positions.order_by('-positionid').first()
|
return validated_data['order'].positions.order_by('-positionid').first()
|
||||||
else:
|
else:
|
||||||
return OrderPosition() # fake to appease DRF
|
return OrderPosition() # fake to appease DRF
|
||||||
@@ -310,6 +311,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ocm = self.context['ocm']
|
ocm = self.context['ocm']
|
||||||
|
check_quotas = self.context.get('check_quotas', True)
|
||||||
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
||||||
item = validated_data.get('item', instance.item)
|
item = validated_data.get('item', instance.item)
|
||||||
variation = validated_data.get('variation', instance.variation)
|
variation = validated_data.get('variation', instance.variation)
|
||||||
@@ -356,7 +358,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
|||||||
ocm.change_ticket_secret(instance, secret)
|
ocm.change_ticket_secret(instance, secret)
|
||||||
|
|
||||||
if self.context.get('commit', True):
|
if self.context.get('commit', True):
|
||||||
ocm.commit()
|
ocm.commit(check_quotas=check_quotas)
|
||||||
instance.refresh_from_db()
|
instance.refresh_from_db()
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
raise ValidationError(str(e))
|
raise ValidationError(str(e))
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -24,6 +24,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -32,6 +33,7 @@ from rest_framework.exceptions import ValidationError
|
|||||||
|
|
||||||
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||||
from pretix.api.serializers import AsymmetricField
|
from pretix.api.serializers import AsymmetricField
|
||||||
|
from pretix.api.serializers.fields import PluginsField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
@@ -43,6 +45,10 @@ from pretix.base.models import (
|
|||||||
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||||
|
from pretix.base.plugins import (
|
||||||
|
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||||
|
PLUGIN_LEVEL_ORGANIZER,
|
||||||
|
)
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
from pretix.base.settings import validate_organizer_settings
|
from pretix.base.settings import validate_organizer_settings
|
||||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
@@ -53,13 +59,47 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||||
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
||||||
|
plugins = PluginsField(required=False, source='*')
|
||||||
|
name = serializers.CharField(read_only=True)
|
||||||
|
slug = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
def get_organizer_url(self, organizer):
|
def get_organizer_url(self, organizer):
|
||||||
return build_absolute_uri(organizer, 'presale:organizer.index')
|
return build_absolute_uri(organizer, 'presale:organizer.index')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Organizer
|
model = Organizer
|
||||||
fields = ('name', 'slug', 'public_url')
|
fields = ('name', 'slug', 'public_url', 'plugins')
|
||||||
|
|
||||||
|
def validate_plugins(self, value):
|
||||||
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
|
plugins_available = {
|
||||||
|
p.module: p for p in get_all_plugins(organizer=self.instance)
|
||||||
|
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||||
|
}
|
||||||
|
settings_holder = self.instance
|
||||||
|
|
||||||
|
allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
||||||
|
for plugin in value.get('plugins'):
|
||||||
|
if plugin not in plugins_available:
|
||||||
|
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||||
|
if getattr(plugins_available[plugin], 'restricted', False):
|
||||||
|
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||||
|
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||||
|
if getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT) not in allowed_levels:
|
||||||
|
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
plugins = validated_data.pop('plugins', None)
|
||||||
|
organizer = super().update(instance, validated_data)
|
||||||
|
# Plugins
|
||||||
|
if plugins is not None:
|
||||||
|
organizer.set_active_plugins(plugins)
|
||||||
|
organizer.save()
|
||||||
|
return organizer
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
||||||
@@ -444,6 +484,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
|||||||
'reusable_media_type_nfc_mf0aes',
|
'reusable_media_type_nfc_mf0aes',
|
||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
||||||
|
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@@ -64,14 +66,15 @@ class SeatGuidField(serializers.CharField):
|
|||||||
|
|
||||||
class VoucherSerializer(I18nAwareModelSerializer):
|
class VoucherSerializer(I18nAwareModelSerializer):
|
||||||
seat = SeatGuidField(allow_null=True, required=False)
|
seat = SeatGuidField(allow_null=True, required=False)
|
||||||
|
budget_used = serializers.DecimalField(read_only=True, max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Voucher
|
model = Voucher
|
||||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
fields = ('id', 'created', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
||||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
||||||
'all_bundles_included')
|
'all_bundles_included', 'budget', 'budget_used')
|
||||||
read_only_fields = ('id', 'redeemed')
|
read_only_fields = ('id', 'redeemed', 'budget_used')
|
||||||
list_serializer_class = VoucherListSerializer
|
list_serializer_class = VoucherListSerializer
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -21,22 +21,22 @@
|
|||||||
#
|
#
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.dispatch import Signal, receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.api.models import ApiCall, WebHookCall
|
from pretix.api.models import ApiCall, WebHookCall
|
||||||
from pretix.base.signals import EventPluginSignal, periodic_task
|
from pretix.base.signals import EventPluginSignal, GlobalSignal, periodic_task
|
||||||
from pretix.helpers.periodic import minimum_interval
|
from pretix.helpers.periodic import minimum_interval
|
||||||
|
|
||||||
register_webhook_events = Signal()
|
register_webhook_events = GlobalSignal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known webhook events. Receivers should return an
|
This signal is sent out to get all known webhook events. Receivers should return an
|
||||||
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
||||||
instances.
|
instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
register_device_security_profile = Signal()
|
register_device_security_profile = GlobalSignal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known device security_profiles. Receivers should
|
This signal is sent out to get all known device security_profiles. Receivers should
|
||||||
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -92,6 +92,7 @@ event_router.register(r'taxrules', event.TaxRuleViewSet)
|
|||||||
event_router.register(r'seats', event.SeatViewSet)
|
event_router.register(r'seats', event.SeatViewSet)
|
||||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||||
|
event_router.register(r'checkins', checkin.CheckinViewSet)
|
||||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||||
event_router.register(r'scheduled_exports', exporters.ScheduledEventExportViewSet)
|
event_router.register(r'scheduled_exports', exporters.ScheduledEventExportViewSet)
|
||||||
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
||||||
@@ -132,6 +133,8 @@ urlpatterns = [
|
|||||||
name="checkinrpc.redeem"),
|
name="checkinrpc.redeem"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/search/$', checkin.CheckinRPCSearchView.as_view(),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/search/$', checkin.CheckinRPCSearchView.as_view(),
|
||||||
name="checkinrpc.search"),
|
name="checkinrpc.search"),
|
||||||
|
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/annul/$', checkin.CheckinRPCAnnulView.as_view(),
|
||||||
|
name="checkinrpc.annul"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
|
||||||
name="organizer.settings"),
|
name="organizer.settings"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -20,12 +20,13 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import operator
|
import operator
|
||||||
|
from datetime import timedelta
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError as BaseValidationError
|
from django.core.exceptions import ValidationError as BaseValidationError
|
||||||
from django.db import transaction
|
from django.db import connection, transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
||||||
prefetch_related_objects,
|
prefetch_related_objects,
|
||||||
@@ -39,21 +40,24 @@ from django.utils.translation import gettext
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
from rest_framework import views, viewsets
|
from rest_framework import status, views, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import (
|
||||||
|
NotFound, PermissionDenied, ValidationError,
|
||||||
|
)
|
||||||
from rest_framework.fields import DateTimeField
|
from rest_framework.fields import DateTimeField
|
||||||
from rest_framework.generics import ListAPIView
|
from rest_framework.generics import ListAPIView
|
||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.serializers.checkin import (
|
from pretix.api.serializers.checkin import (
|
||||||
CheckinListSerializer, CheckinRPCRedeemInputSerializer,
|
CheckinListSerializer, CheckinRPCAnnulInputSerializer,
|
||||||
MiniCheckinListSerializer,
|
CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.item import QuestionSerializer
|
from pretix.api.serializers.item import QuestionSerializer
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
CheckinListOrderPositionSerializer, FailedCheckinSerializer,
|
CheckinListOrderPositionSerializer, CheckinSerializer,
|
||||||
|
FailedCheckinSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import RichOrderingFilter
|
from pretix.api.views import RichOrderingFilter
|
||||||
from pretix.api.views.order import OrderPositionFilter
|
from pretix.api.views.order import OrderPositionFilter
|
||||||
@@ -66,6 +70,8 @@ from pretix.base.models.orders import PrintLog
|
|||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||||
)
|
)
|
||||||
|
from pretix.base.signals import checkin_annulled
|
||||||
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class CheckinListFilter(FilterSet):
|
class CheckinListFilter(FilterSet):
|
||||||
@@ -91,6 +97,16 @@ with scopes_disabled():
|
|||||||
)
|
)
|
||||||
return queryset.filter(expr)
|
return queryset.filter(expr)
|
||||||
|
|
||||||
|
class CheckinFilter(FilterSet):
|
||||||
|
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
|
||||||
|
created_before = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='lt')
|
||||||
|
datetime_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||||
|
datetime_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Checkin
|
||||||
|
fields = ['successful', 'error_reason', 'list', 'type', 'gate', 'device', 'auto_checked_in']
|
||||||
|
|
||||||
|
|
||||||
class CheckinListViewSet(viewsets.ModelViewSet):
|
class CheckinListViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = CheckinListSerializer
|
serializer_class = CheckinListSerializer
|
||||||
@@ -813,7 +829,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
ctx['expand'] = self.request.query_params.getlist('expand')
|
ctx['expand'] = self.request.query_params.getlist('expand')
|
||||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_filterset_kwargs(self):
|
def get_filterset_kwargs(self):
|
||||||
@@ -832,9 +848,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
def get_queryset(self, ignore_status=False, ignore_products=False):
|
def get_queryset(self, ignore_status=False, ignore_products=False):
|
||||||
qs = _checkin_list_position_queryset(
|
qs = _checkin_list_position_queryset(
|
||||||
[self.checkinlist],
|
[self.checkinlist],
|
||||||
ignore_status=self.request.query_params.get('ignore_status', 'false') == 'true' or ignore_status,
|
ignore_status=self.request.query_params.get('ignore_status', 'false').lower() == 'true' or ignore_status,
|
||||||
ignore_products=ignore_products,
|
ignore_products=ignore_products,
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -876,7 +892,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
||||||
questions_supported=self.request.data.get('questions_supported', True),
|
questions_supported=self.request.data.get('questions_supported', True),
|
||||||
canceled_supported=self.request.data.get('canceled_supported', False),
|
canceled_supported=self.request.data.get('canceled_supported', False),
|
||||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||||
@@ -911,7 +927,7 @@ class CheckinRPCRedeemView(views.APIView):
|
|||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
||||||
questions_supported=s.validated_data['questions_supported'],
|
questions_supported=s.validated_data['questions_supported'],
|
||||||
use_order_locale=s.validated_data['use_order_locale'],
|
use_order_locale=s.validated_data['use_order_locale'],
|
||||||
canceled_supported=True,
|
canceled_supported=True,
|
||||||
@@ -989,9 +1005,9 @@ class CheckinRPCSearchView(ListAPIView):
|
|||||||
def get_queryset(self, ignore_status=False, ignore_products=False):
|
def get_queryset(self, ignore_status=False, ignore_products=False):
|
||||||
qs = _checkin_list_position_queryset(
|
qs = _checkin_list_position_queryset(
|
||||||
self.lists,
|
self.lists,
|
||||||
ignore_status=self.request.query_params.get('ignore_status', 'false') == 'true' or ignore_status,
|
ignore_status=self.request.query_params.get('ignore_status', 'false').lower() == 'true' or ignore_status,
|
||||||
ignore_products=ignore_products,
|
ignore_products=ignore_products,
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -999,3 +1015,101 @@ class CheckinRPCSearchView(ListAPIView):
|
|||||||
qs = qs.none()
|
qs = qs.none()
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class CheckinRPCAnnulView(views.APIView):
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
|
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
|
||||||
|
elif self.request.user.is_authenticated:
|
||||||
|
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
|
||||||
|
organizer=self.request.organizer
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("unknown authentication method")
|
||||||
|
|
||||||
|
s = CheckinRPCAnnulInputSerializer(data=request.data, context={'events': events})
|
||||||
|
s.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
qs = Checkin.all.all()
|
||||||
|
if isinstance(request.auth, Device):
|
||||||
|
qs = qs.filter(device=request.auth)
|
||||||
|
ci = qs.select_for_update(
|
||||||
|
of=OF_SELF,
|
||||||
|
).select_related("position", "position__order", "position__order__event").get(
|
||||||
|
list__in=s.validated_data['lists'],
|
||||||
|
nonce=s.validated_data['nonce'],
|
||||||
|
)
|
||||||
|
if connection.features.has_select_for_update_of and ci.position_id:
|
||||||
|
# Lock position as well, can't do it with of= above because relation is nullable
|
||||||
|
OrderPosition.objects.select_for_update(of=OF_SELF).get(pk=ci.position_id)
|
||||||
|
|
||||||
|
if not ci.successful or not ci.position:
|
||||||
|
raise ValidationError("Cannot annul an unsuccessful checkin")
|
||||||
|
except Checkin.DoesNotExist:
|
||||||
|
raise NotFound("No check-in found based on nonce")
|
||||||
|
except Checkin.MultipleObjectsReturned:
|
||||||
|
raise ValidationError("Multiple check-ins found based on nonce")
|
||||||
|
|
||||||
|
annulment_time = s.validated_data.get("datetime") or now()
|
||||||
|
|
||||||
|
if annulment_time - ci.datetime > timedelta(minutes=15):
|
||||||
|
# Compare to sent datetime, which makes this cheatable, but allows offline annulment of checkins
|
||||||
|
ci.position.order.log_action('pretix.event.checkin.annulment.ignored', data={
|
||||||
|
'checkin': ci.pk,
|
||||||
|
'position': ci.position.id,
|
||||||
|
'positionid': ci.position.positionid,
|
||||||
|
'datetime': annulment_time,
|
||||||
|
'error_explanation': s.validated_data.get("error_explanation"),
|
||||||
|
'type': ci.type,
|
||||||
|
'list': ci.list_id,
|
||||||
|
}, user=request.user, auth=request.auth)
|
||||||
|
return Response({
|
||||||
|
"non_field_errors": ["Annulment is not allowed more than 15 minutes after check-in"]
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if ci.device and ci.device != request.auth:
|
||||||
|
return Response({
|
||||||
|
"non_field_errors": ["Annulment is only allowed from the same device"]
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
ci.successful = False
|
||||||
|
ci.error_reason = Checkin.REASON_ANNULLED
|
||||||
|
ci.error_explanation = s.validated_data.get("error_explanation")
|
||||||
|
ci.save(update_fields=["successful", "error_reason", "error_explanation"])
|
||||||
|
ci.position.order.log_action('pretix.event.checkin.annulled', data={
|
||||||
|
'checkin': ci.pk,
|
||||||
|
'position': ci.position.id,
|
||||||
|
'positionid': ci.position.positionid,
|
||||||
|
'datetime': annulment_time,
|
||||||
|
'error_explanation': s.validated_data.get("error_explanation"),
|
||||||
|
'type': ci.type,
|
||||||
|
'list': ci.list_id,
|
||||||
|
}, user=request.user, auth=request.auth)
|
||||||
|
checkin_annulled.send(ci.position.order.event, checkin=ci)
|
||||||
|
|
||||||
|
return Response({"status": "ok"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = CheckinSerializer
|
||||||
|
queryset = Checkin.all.none()
|
||||||
|
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
||||||
|
filterset_class = CheckinFilter
|
||||||
|
ordering = ('created', 'id')
|
||||||
|
ordering_fields = ('created', 'datetime', 'id',)
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = Checkin.all.filter().select_related(
|
||||||
|
"position",
|
||||||
|
"device",
|
||||||
|
)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['event'] = self.request.event
|
||||||
|
return ctx
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -88,7 +88,7 @@ from pretix.base.secrets import assign_ticket_secret
|
|||||||
from pretix.base.services import tickets
|
from pretix.base.services import tickets
|
||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||||
regenerate_invoice,
|
regenerate_invoice, transmit_invoice,
|
||||||
)
|
)
|
||||||
from pretix.base.services.mail import SendMailException
|
from pretix.base.services.mail import SendMailException
|
||||||
from pretix.base.services.orders import (
|
from pretix.base.services.orders import (
|
||||||
@@ -228,7 +228,7 @@ class OrderViewSetMixin:
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.get_base_queryset()
|
qs = self.get_base_queryset()
|
||||||
if 'fees' not in self.request.GET.getlist('exclude'):
|
if 'fees' not in self.request.GET.getlist('exclude'):
|
||||||
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
|
if self.request.query_params.get('include_canceled_fees', 'false').lower() == 'true':
|
||||||
fqs = OrderFee.all
|
fqs = OrderFee.all
|
||||||
else:
|
else:
|
||||||
fqs = OrderFee.objects
|
fqs = OrderFee.objects
|
||||||
@@ -246,11 +246,11 @@ class OrderViewSetMixin:
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _positions_prefetch(self, request):
|
def _positions_prefetch(self, request):
|
||||||
if request.query_params.get('include_canceled_positions', 'false') == 'true':
|
if request.query_params.get('include_canceled_positions', 'false').lower() == 'true':
|
||||||
opq = OrderPosition.all
|
opq = OrderPosition.all
|
||||||
else:
|
else:
|
||||||
opq = OrderPosition.objects
|
opq = OrderPosition.objects
|
||||||
if request.query_params.get('pdf_data', 'false') == 'true' and getattr(request, 'event', None):
|
if request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(request, 'event', None):
|
||||||
prefetch_related_objects([request.organizer], 'meta_properties')
|
prefetch_related_objects([request.organizer], 'meta_properties')
|
||||||
prefetch_related_objects(
|
prefetch_related_objects(
|
||||||
[request.event],
|
[request.event],
|
||||||
@@ -344,7 +344,8 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
ctx['auth'] = self.request.auth
|
||||||
|
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_base_queryset(self):
|
def get_base_queryset(self):
|
||||||
@@ -743,7 +744,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
auth=request.auth,
|
auth=request.auth,
|
||||||
)
|
)
|
||||||
order_placed.send(self.request.event, order=order)
|
order_placed.send(self.request.event, order=order, bulk=False)
|
||||||
if order.status == Order.STATUS_PAID:
|
if order.status == Order.STATUS_PAID:
|
||||||
order_paid.send(self.request.event, order=order)
|
order_paid.send(self.request.event, order=order)
|
||||||
order.log_action(
|
order.log_action(
|
||||||
@@ -764,7 +765,13 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
) and not order.invoices.last()
|
) and not order.invoices.last()
|
||||||
invoice = None
|
invoice = None
|
||||||
if gen_invoice:
|
if gen_invoice:
|
||||||
invoice = generate_invoice(order, trigger_pdf=True)
|
try:
|
||||||
|
invoice = generate_invoice(order, trigger_pdf=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Could not generate invoice.")
|
||||||
|
order.log_action("pretix.event.order.invoice.failed", data={
|
||||||
|
"exception": str(e)
|
||||||
|
})
|
||||||
|
|
||||||
# Refresh serializer only after running signals
|
# Refresh serializer only after running signals
|
||||||
prefetch_related_objects([order], self._positions_prefetch(request))
|
prefetch_related_objects([order], self._positions_prefetch(request))
|
||||||
@@ -943,6 +950,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def change(self, request, **kwargs):
|
def change(self, request, **kwargs):
|
||||||
order = self.get_object()
|
order = self.get_object()
|
||||||
|
check_quotas = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
|
||||||
|
|
||||||
serializer = OrderChangeOperationSerializer(
|
serializer = OrderChangeOperationSerializer(
|
||||||
context={'order': order, **self.get_serializer_context()},
|
context={'order': order, **self.get_serializer_context()},
|
||||||
@@ -1008,7 +1016,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
elif serializer.validated_data.get('recalculate_taxes') == 'keep_gross':
|
elif serializer.validated_data.get('recalculate_taxes') == 'keep_gross':
|
||||||
ocm.recalculate_taxes(keep='gross')
|
ocm.recalculate_taxes(keep='gross')
|
||||||
|
|
||||||
ocm.commit()
|
ocm.commit(check_quotas=check_quotas)
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
raise ValidationError(str(e))
|
raise ValidationError(str(e))
|
||||||
|
|
||||||
@@ -1086,17 +1094,18 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
|
||||||
|
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
if self.request.query_params.get('include_canceled_positions', 'false').lower() == 'true':
|
||||||
qs = OrderPosition.all
|
qs = OrderPosition.all
|
||||||
else:
|
else:
|
||||||
qs = OrderPosition.objects
|
qs = OrderPosition.objects
|
||||||
|
|
||||||
qs = qs.filter(order__event=self.request.event)
|
qs = qs.filter(order__event=self.request.event)
|
||||||
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
|
||||||
prefetch_related_objects([self.request.organizer], 'meta_properties')
|
prefetch_related_objects([self.request.organizer], 'meta_properties')
|
||||||
prefetch_related_objects(
|
prefetch_related_objects(
|
||||||
[self.request.event],
|
[self.request.event],
|
||||||
@@ -1661,6 +1670,9 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
else:
|
else:
|
||||||
mark_refunded = request.data.get('mark_canceled', False)
|
mark_refunded = request.data.get('mark_canceled', False)
|
||||||
|
|
||||||
|
if not isinstance(request.data.get("comment", ""), str):
|
||||||
|
return Response({'comment': 'Invalid type.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
|
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||||
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@@ -1687,6 +1699,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
amount=amount,
|
amount=amount,
|
||||||
provider=payment.provider,
|
provider=payment.provider,
|
||||||
info='{}',
|
info='{}',
|
||||||
|
comment=request.data.get("comment"),
|
||||||
)
|
)
|
||||||
payment.order.log_action('pretix.event.order.refund.created', {
|
payment.order.log_action('pretix.event.order.refund.created', {
|
||||||
'local_id': r.local_id,
|
'local_id': r.local_id,
|
||||||
@@ -1889,6 +1902,12 @@ class RetryException(APIException):
|
|||||||
default_code = 'retry_later'
|
default_code = 'retry_later'
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentlyInflightException(APIException):
|
||||||
|
status_code = 409
|
||||||
|
default_detail = 'The requested action is already in progress.'
|
||||||
|
default_code = 'currently_inflight'
|
||||||
|
|
||||||
|
|
||||||
class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = InvoiceSerializer
|
serializer_class = InvoiceSerializer
|
||||||
queryset = Invoice.objects.none()
|
queryset = Invoice.objects.none()
|
||||||
@@ -1937,13 +1956,52 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def transmit(self, request, **kwargs):
|
||||||
|
invoice = self.get_object()
|
||||||
|
if invoice.shredded:
|
||||||
|
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||||
|
|
||||||
|
if invoice.transmission_status != Invoice.TRANSMISSION_STATUS_PENDING:
|
||||||
|
raise PermissionDenied('The invoice is not in pending state.')
|
||||||
|
|
||||||
|
transmit_invoice.apply_async(args=(self.request.event.pk, invoice.pk, False))
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def retransmit(self, request, **kwargs):
|
||||||
|
invoice = self.get_object()
|
||||||
|
if invoice.shredded:
|
||||||
|
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||||
|
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
invoice = Invoice.objects.select_for_update(of=OF_SELF).get(pk=invoice.pk)
|
||||||
|
|
||||||
|
if invoice.transmission_status == Invoice.TRANSMISSION_STATUS_INFLIGHT:
|
||||||
|
raise CurrentlyInflightException()
|
||||||
|
|
||||||
|
invoice.transmission_status = Invoice.TRANSMISSION_STATUS_PENDING
|
||||||
|
invoice.transmission_date = now()
|
||||||
|
invoice.save(update_fields=["transmission_status", "transmission_date"])
|
||||||
|
invoice.order.log_action(
|
||||||
|
'pretix.event.order.invoice.retransmitted',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={
|
||||||
|
'invoice': invoice.pk,
|
||||||
|
'full_invoice_no': invoice.full_invoice_no,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
transmit_invoice.apply_async(args=(self.request.event.pk, invoice.pk, True))
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def regenerate(self, request, **kwargs):
|
def regenerate(self, request, **kwargs):
|
||||||
inv = self.get_object()
|
inv = self.get_object()
|
||||||
if inv.canceled:
|
if inv.canceled:
|
||||||
raise ValidationError('The invoice has already been canceled.')
|
raise ValidationError('The invoice has already been canceled.')
|
||||||
if not inv.event.settings.invoice_regenerate_allowed:
|
if not inv.regenerate_allowed:
|
||||||
raise PermissionDenied('Invoices may not be changed after they are created.')
|
raise PermissionDenied('Invoice may not be regenerated.')
|
||||||
elif inv.shredded:
|
elif inv.shredded:
|
||||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||||
elif inv.sent_to_organizer:
|
elif inv.sent_to_organizer:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,7 +19,9 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import operator
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
@@ -48,15 +50,18 @@ from pretix.api.serializers.organizer import (
|
|||||||
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
Customer, Device, Event, GiftCard, GiftCardTransaction, LogEntry,
|
||||||
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Team,
|
||||||
TeamInvite, User,
|
TeamAPIToken, TeamInvite, User,
|
||||||
|
)
|
||||||
|
from pretix.base.plugins import (
|
||||||
|
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||||
)
|
)
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = OrganizerSerializer
|
serializer_class = OrganizerSerializer
|
||||||
queryset = Organizer.objects.none()
|
queryset = Organizer.objects.none()
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
@@ -65,6 +70,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
filter_backends = (TotalOrderingFilter,)
|
filter_backends = (TotalOrderingFilter,)
|
||||||
ordering = ('slug',)
|
ordering = ('slug',)
|
||||||
ordering_fields = ('name', 'slug')
|
ordering_fields = ('name', 'slug')
|
||||||
|
write_permission = "can_change_organizer_settings"
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
@@ -83,6 +89,67 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
else:
|
else:
|
||||||
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
|
original_data = self.get_serializer(instance=serializer.instance).data
|
||||||
|
|
||||||
|
current_plugins_value = serializer.instance.get_plugins()
|
||||||
|
updated_plugins_value = serializer.validated_data.get('plugins', None)
|
||||||
|
|
||||||
|
super().perform_update(serializer)
|
||||||
|
|
||||||
|
if serializer.data == original_data:
|
||||||
|
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||||
|
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||||
|
return
|
||||||
|
|
||||||
|
if updated_plugins_value is not None and set(updated_plugins_value) != set(current_plugins_value):
|
||||||
|
enabled = {m: 'enabled' for m in updated_plugins_value if m not in current_plugins_value}
|
||||||
|
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
|
||||||
|
changed = merge_dicts(enabled, disabled)
|
||||||
|
|
||||||
|
plugins_available = {
|
||||||
|
p.module: p
|
||||||
|
for p in get_all_plugins(organizer=serializer.instance)
|
||||||
|
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||||
|
}
|
||||||
|
qs = []
|
||||||
|
for module in disabled:
|
||||||
|
pluginmeta = plugins_available[module]
|
||||||
|
level = getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT)
|
||||||
|
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
||||||
|
qs.append(Q(plugins__regex='(^|,)' + module + '(,|$)'))
|
||||||
|
|
||||||
|
if qs:
|
||||||
|
events_to_disable = set(self.request.organizer.events.filter(
|
||||||
|
reduce(operator.or_, qs)
|
||||||
|
).values_list("pk", flat=True))
|
||||||
|
logentries_to_save = []
|
||||||
|
events_to_save = []
|
||||||
|
|
||||||
|
for e in self.request.organizer.events.filter(pk__in=events_to_disable):
|
||||||
|
for module in disabled:
|
||||||
|
if module in e.get_plugins():
|
||||||
|
logentries_to_save.append(
|
||||||
|
e.log_action('pretix.event.plugins.disabled', user=self.request.user, auth=self.request.auth,
|
||||||
|
data={'plugin': module}, save=False)
|
||||||
|
)
|
||||||
|
e.disable_plugin(module)
|
||||||
|
events_to_save.append(e)
|
||||||
|
|
||||||
|
Event.objects.bulk_update(events_to_save, fields=["plugins"])
|
||||||
|
LogEntry.objects.bulk_create(logentries_to_save)
|
||||||
|
|
||||||
|
for module, operation in changed.items():
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.organizer.plugins.' + operation,
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={'plugin': module}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlanViewSet(viewsets.ModelViewSet):
|
class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = SeatingPlanSerializer
|
serializer_class = SeatingPlanSerializer
|
||||||
@@ -479,7 +546,8 @@ class DeviceViewSet(mixins.CreateModelMixin,
|
|||||||
|
|
||||||
|
|
||||||
class OrganizerSettingsView(views.APIView):
|
class OrganizerSettingsView(views.APIView):
|
||||||
permission = 'can_change_organizer_settings'
|
permission = None
|
||||||
|
write_permission = 'can_change_organizer_settings'
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -78,6 +78,13 @@ class WebhookEvent:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
|
@property
|
||||||
|
def help_text(self) -> str:
|
||||||
|
"""
|
||||||
|
A human-readable description
|
||||||
|
"""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def get_all_webhook_events():
|
def get_all_webhook_events():
|
||||||
global _ALL_EVENTS
|
global _ALL_EVENTS
|
||||||
@@ -97,9 +104,10 @@ def get_all_webhook_events():
|
|||||||
|
|
||||||
|
|
||||||
class ParametrizedWebhookEvent(WebhookEvent):
|
class ParametrizedWebhookEvent(WebhookEvent):
|
||||||
def __init__(self, action_type, verbose_name):
|
def __init__(self, action_type, verbose_name, help_text=""):
|
||||||
self._action_type = action_type
|
self._action_type = action_type
|
||||||
self._verbose_name = verbose_name
|
self._verbose_name = verbose_name
|
||||||
|
self._help_text = help_text
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -110,6 +118,10 @@ class ParametrizedWebhookEvent(WebhookEvent):
|
|||||||
def verbose_name(self):
|
def verbose_name(self):
|
||||||
return self._verbose_name
|
return self._verbose_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def help_text(self):
|
||||||
|
return self._help_text
|
||||||
|
|
||||||
|
|
||||||
class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
|
class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
|
||||||
def build_payload(self, logentry: LogEntry):
|
def build_payload(self, logentry: LogEntry):
|
||||||
@@ -161,6 +173,19 @@ class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ParametrizedVoucherWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
|
||||||
|
def build_payload(self, logentry: LogEntry):
|
||||||
|
# do not use content_object, this is also called in deletion
|
||||||
|
return {
|
||||||
|
'notification_id': logentry.pk,
|
||||||
|
'organizer': logentry.event.organizer.slug,
|
||||||
|
'event': logentry.event.slug,
|
||||||
|
'voucher': logentry.object_id,
|
||||||
|
'action': logentry.action_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent):
|
class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
|
||||||
def build_payload(self, logentry: LogEntry):
|
def build_payload(self, logentry: LogEntry):
|
||||||
@@ -346,8 +371,9 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
ParametrizedItemWebhookEvent(
|
ParametrizedItemWebhookEvent(
|
||||||
'pretix.event.item.*',
|
'pretix.event.item.*',
|
||||||
_('Product changed (including product added or deleted and including changes to nested objects like '
|
_('Product changed'),
|
||||||
'variations or bundles)'),
|
_('This includes product added or deleted and changes to nested objects like '
|
||||||
|
'variations or bundles.'),
|
||||||
),
|
),
|
||||||
ParametrizedEventWebhookEvent(
|
ParametrizedEventWebhookEvent(
|
||||||
'pretix.event.live.activated',
|
'pretix.event.live.activated',
|
||||||
@@ -381,6 +407,19 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.orders.waitinglist.voucher_assigned',
|
'pretix.event.orders.waitinglist.voucher_assigned',
|
||||||
_('Waiting list entry received voucher'),
|
_('Waiting list entry received voucher'),
|
||||||
),
|
),
|
||||||
|
ParametrizedVoucherWebhookEvent(
|
||||||
|
'pretix.voucher.added',
|
||||||
|
_('Voucher added'),
|
||||||
|
),
|
||||||
|
ParametrizedVoucherWebhookEvent(
|
||||||
|
'pretix.voucher.changed',
|
||||||
|
_('Voucher changed'),
|
||||||
|
_('Only includes explicit changes to the voucher, not e.g. an increase of the number of redemptions.')
|
||||||
|
),
|
||||||
|
ParametrizedVoucherWebhookEvent(
|
||||||
|
'pretix.voucher.deleted',
|
||||||
|
_('Voucher deleted'),
|
||||||
|
),
|
||||||
ParametrizedCustomerWebhookEvent(
|
ParametrizedCustomerWebhookEvent(
|
||||||
'pretix.customer.created',
|
'pretix.customer.created',
|
||||||
_('Customer account created'),
|
_('Customer account created'),
|
||||||
@@ -400,8 +439,12 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
def notify_webhooks(logentry_ids: list):
|
def notify_webhooks(logentry_ids: list):
|
||||||
if not isinstance(logentry_ids, list):
|
if not isinstance(logentry_ids, list):
|
||||||
logentry_ids = [logentry_ids]
|
logentry_ids = [logentry_ids]
|
||||||
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
|
qs = LogEntry.all.select_related(
|
||||||
_org, _at, webhooks = None, None, None
|
'event', 'event__organizer', 'organizer'
|
||||||
|
).order_by(
|
||||||
|
'action_type', 'organizer_id', 'event_id',
|
||||||
|
).filter(id__in=logentry_ids)
|
||||||
|
_org, _at, _ev, webhooks = None, None, None, None
|
||||||
for logentry in qs:
|
for logentry in qs:
|
||||||
if not logentry.organizer:
|
if not logentry.organizer:
|
||||||
break # We need to know the organizer
|
break # We need to know the organizer
|
||||||
@@ -411,7 +454,7 @@ def notify_webhooks(logentry_ids: list):
|
|||||||
if not notification_type:
|
if not notification_type:
|
||||||
break # Ignore, no webhooks for this event type
|
break # Ignore, no webhooks for this event type
|
||||||
|
|
||||||
if _org != logentry.organizer or _at != logentry.action_type or webhooks is None:
|
if _org != logentry.organizer or _at != logentry.action_type or _ev != logentry.event_id or webhooks is None:
|
||||||
_org = logentry.organizer
|
_org = logentry.organizer
|
||||||
_at = logentry.action_type
|
_at = logentry.action_type
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -43,10 +43,10 @@ class PretixBaseConfig(AppConfig):
|
|||||||
from . import exporter # NOQA
|
from . import exporter # NOQA
|
||||||
from . import payment # NOQA
|
from . import payment # NOQA
|
||||||
from . import exporters # NOQA
|
from . import exporters # NOQA
|
||||||
from . import invoice # NOQA
|
from .invoicing import pdf, transmission, email, peppol, national # NOQA
|
||||||
from . import notifications # NOQA
|
from . import notifications # NOQA
|
||||||
from . import email # NOQA
|
from . import email # NOQA
|
||||||
from .services import auth, checkin, currencies, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
from .services import auth, checkin, currencies, datasync, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||||
from .models import _transactions # NOQA
|
from .models import _transactions # NOQA
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH 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
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -199,6 +199,7 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
params['client_id'] = provider.configuration['client_id']
|
params['client_id'] = provider.configuration['client_id']
|
||||||
params['client_secret'] = provider.configuration['client_secret']
|
params['client_secret'] = provider.configuration['client_secret']
|
||||||
|
|
||||||
|
resp = None
|
||||||
try:
|
try:
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -214,7 +215,10 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except RequestException:
|
except RequestException:
|
||||||
logger.exception('Could not retrieve authorization token')
|
if resp:
|
||||||
|
logger.exception(f'Could not retrieve authorization token. Response: {resp.text}')
|
||||||
|
else:
|
||||||
|
logger.exception('Could not retrieve authorization token')
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
_('Login was not successful. Error message: "{error}".').format(
|
||||||
error='could not reach login provider',
|
error='could not reach login provider',
|
||||||
@@ -222,6 +226,7 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'access_token' not in data:
|
if 'access_token' not in data:
|
||||||
|
logger.error(f'Could not find access token. Response: {data}')
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
_('Login was not successful. Error message: "{error}".').format(
|
||||||
error='access token missing',
|
error='access token missing',
|
||||||
@@ -229,6 +234,7 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
)
|
)
|
||||||
|
|
||||||
endpoint = provider.configuration['provider_config']['userinfo_endpoint']
|
endpoint = provider.configuration['provider_config']['userinfo_endpoint']
|
||||||
|
resp = None
|
||||||
try:
|
try:
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
@@ -240,7 +246,10 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
userinfo = resp.json()
|
userinfo = resp.json()
|
||||||
except RequestException:
|
except RequestException:
|
||||||
logger.exception('Could not retrieve user info')
|
if resp:
|
||||||
|
logger.exception(f'Could not retrieve user info. Response: {resp.text}')
|
||||||
|
else:
|
||||||
|
logger.exception('Could not retrieve user info')
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
_('Login was not successful. Error message: "{error}".').format(
|
||||||
error='could not fetch user info',
|
error='could not fetch user info',
|
||||||
|
|||||||
21
src/pretix/base/datasync/__init__.py
Normal file
21
src/pretix/base/datasync/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
450
src/pretix/base/datasync/datasync.py
Normal file
450
src/pretix/base/datasync/datasync.py
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from collections import namedtuple
|
||||||
|
from datetime import timedelta
|
||||||
|
from functools import cached_property
|
||||||
|
from typing import List, Optional, Protocol
|
||||||
|
|
||||||
|
import sentry_sdk
|
||||||
|
from django.db import DatabaseError, transaction
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.base.datasync.sourcefields import (
|
||||||
|
EVENT, EVENT_OR_SUBEVENT, ORDER, ORDER_POSITION, get_data_fields,
|
||||||
|
)
|
||||||
|
from pretix.base.i18n import language
|
||||||
|
from pretix.base.logentrytype_registry import make_link
|
||||||
|
from pretix.base.models.datasync import OrderSyncQueue, OrderSyncResult
|
||||||
|
from pretix.base.signals import PluginAwareRegistry
|
||||||
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
datasync_providers = PluginAwareRegistry({"identifier": lambda o: o.identifier})
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSyncError(Exception):
|
||||||
|
def __init__(self, messages, full_message=None):
|
||||||
|
self.messages = messages
|
||||||
|
self.full_message = full_message
|
||||||
|
|
||||||
|
|
||||||
|
class UnrecoverableSyncError(BaseSyncError):
|
||||||
|
"""
|
||||||
|
A SyncProvider encountered a permanent problem, where a retry will not be successful.
|
||||||
|
"""
|
||||||
|
failure_mode = "permanent"
|
||||||
|
|
||||||
|
|
||||||
|
class SyncConfigError(UnrecoverableSyncError):
|
||||||
|
"""
|
||||||
|
A SyncProvider is misconfigured in a way where a retry without configuration change will
|
||||||
|
not be successful.
|
||||||
|
"""
|
||||||
|
failure_mode = "config"
|
||||||
|
|
||||||
|
|
||||||
|
class RecoverableSyncError(BaseSyncError):
|
||||||
|
"""
|
||||||
|
A SyncProvider has encountered a temporary problem, and the sync should be retried
|
||||||
|
at a later time.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectMapping(Protocol):
|
||||||
|
id: int
|
||||||
|
pretix_model: str
|
||||||
|
external_object_type: str
|
||||||
|
pretix_id_field: str
|
||||||
|
external_id_field: str
|
||||||
|
property_mappings: str
|
||||||
|
|
||||||
|
|
||||||
|
StaticMapping = namedtuple('StaticMapping', ('id', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mappings'))
|
||||||
|
|
||||||
|
|
||||||
|
class OutboundSyncProvider:
|
||||||
|
max_attempts = 5
|
||||||
|
|
||||||
|
def __init__(self, event):
|
||||||
|
self.event = event
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@property
|
||||||
|
def display_name(cls):
|
||||||
|
return str(cls.identifier)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enqueue_order(cls, order, triggered_by, not_before=None, immediate=False):
|
||||||
|
"""
|
||||||
|
Adds an order to the sync queue. May only be called on derived classes which define an ``identifier`` attribute.
|
||||||
|
|
||||||
|
Should be called in the appropriate signal receivers, e.g.::
|
||||||
|
|
||||||
|
@receiver(order_placed, dispatch_uid="mysync_order_placed")
|
||||||
|
def on_order_placed(sender, order, **kwargs):
|
||||||
|
MySyncProvider.enqueue_order(order, "order_placed")
|
||||||
|
|
||||||
|
:param order: the Order that should be synced
|
||||||
|
:param triggered_by: the reason why the order should be synced, e.g. name of the signal
|
||||||
|
(currently only used internally for logging)
|
||||||
|
:param immediate: whether a new sync task should run immediately for this order, instead
|
||||||
|
of waiting for the next periodic_task interval
|
||||||
|
:return: Return a tuple (queue_item, created), where created is a boolean
|
||||||
|
specifying whether a new queue item was created.
|
||||||
|
"""
|
||||||
|
if not hasattr(cls, 'identifier'):
|
||||||
|
raise TypeError('Call this method on a derived class that defines an "identifier" attribute.')
|
||||||
|
queue_item, created = OrderSyncQueue.objects.update_or_create(
|
||||||
|
order=order,
|
||||||
|
sync_provider=cls.identifier,
|
||||||
|
in_flight=False,
|
||||||
|
defaults={
|
||||||
|
"event": order.event,
|
||||||
|
"triggered_by": triggered_by,
|
||||||
|
"not_before": not_before or now(),
|
||||||
|
"need_manual_retry": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if immediate:
|
||||||
|
from pretix.base.services.datasync import sync_single
|
||||||
|
sync_single.apply_async(args=(queue_item.pk,))
|
||||||
|
return queue_item, created
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_external_link_info(cls, event, external_link_href, external_link_display_name):
|
||||||
|
return {
|
||||||
|
"href": external_link_href,
|
||||||
|
"val": external_link_display_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_external_link_html(cls, event, external_link_href, external_link_display_name):
|
||||||
|
info = cls.get_external_link_info(event, external_link_href, external_link_display_name)
|
||||||
|
return make_link(info, '{val}')
|
||||||
|
|
||||||
|
def next_retry_date(self, sq):
|
||||||
|
"""
|
||||||
|
Optionally override to configure a different retry backoff behavior
|
||||||
|
"""
|
||||||
|
return now() + timedelta(hours=1)
|
||||||
|
|
||||||
|
def should_sync_order(self, order):
|
||||||
|
"""
|
||||||
|
Optionally override this method to exclude certain orders from sync by returning ``False``
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mappings(self):
|
||||||
|
"""
|
||||||
|
Implementations must override this property to provide the data mappings as a list of objects.
|
||||||
|
|
||||||
|
They can return instances of the ``StaticMapping`` `namedtuple` defined above, or create their own
|
||||||
|
class (e.g. a Django model).
|
||||||
|
|
||||||
|
:return: The returned objects must have at least the following properties:
|
||||||
|
|
||||||
|
- `id`: Unique identifier for this mapping. If the mappings are Django models, the database primary key
|
||||||
|
should be used. This may be referenced in other mappings, to establish relations between objects.
|
||||||
|
- `pretix_model`: Which pretix model to use as data source in this mapping. Possible values are
|
||||||
|
the keys of ``sourcefields.AVAILABLE_MODELS``
|
||||||
|
- `external_object_type`: Destination object type in the target system. opaque string of maximum 128 characters.
|
||||||
|
- `pretix_id_field`: Which pretix data field should be used to identify the mapped object. Any ``DataFieldInfo.key``
|
||||||
|
returned by ``sourcefields.get_data_fields()`` for the combination of ``Event`` and ``pretix_model``.
|
||||||
|
- `external_id_field`: Destination identifier field in the target system.
|
||||||
|
- `property_mappings`: Mapping configuration as generated by ``PropertyMappingFormSet.to_property_mappings_list()``.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def sync_queued_orders(self, queued_orders):
|
||||||
|
"""
|
||||||
|
This method should catch all Exceptions and handle them appropriately. It should never throw
|
||||||
|
an Exception, as that may block the entire queue.
|
||||||
|
"""
|
||||||
|
for queue_item in queued_orders:
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
sq = (
|
||||||
|
OrderSyncQueue.objects
|
||||||
|
.select_for_update(of=OF_SELF, nowait=True)
|
||||||
|
.select_related("order")
|
||||||
|
.get(pk=queue_item.pk)
|
||||||
|
)
|
||||||
|
if sq.in_flight:
|
||||||
|
continue
|
||||||
|
sq.in_flight = True
|
||||||
|
sq.in_flight_since = now()
|
||||||
|
sq.save()
|
||||||
|
except DatabaseError:
|
||||||
|
# Either select_for_update failed to lock the row, or we couldn't set in_flight
|
||||||
|
# as this order is already in flight (UNIQUE violation). In either case, we ignore
|
||||||
|
# this order for now.
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
mapped_objects = self.sync_order(sq.order)
|
||||||
|
if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()):
|
||||||
|
sq.order.log_action("pretix.event.order.data_sync.success", {
|
||||||
|
"provider": self.identifier,
|
||||||
|
"objects": {
|
||||||
|
mapping_id: [osr and osr.to_result_dict() for osr in results]
|
||||||
|
for mapping_id, results in mapped_objects.items()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
sq.delete()
|
||||||
|
except UnrecoverableSyncError as e:
|
||||||
|
sq.set_sync_error(e.failure_mode, e.messages, e.full_message)
|
||||||
|
except RecoverableSyncError as e:
|
||||||
|
sq.failed_attempts += 1
|
||||||
|
sq.not_before = self.next_retry_date(sq)
|
||||||
|
# model changes saved by set_sync_error / clear_in_flight calls below
|
||||||
|
if sq.failed_attempts >= self.max_attempts:
|
||||||
|
logger.exception('Failed to sync order (max attempts exceeded)')
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
sq.set_sync_error("exceeded", e.messages, e.full_message)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"Could not sync order {sq.order.code} to {type(self).__name__} "
|
||||||
|
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
sq.clear_in_flight()
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception('Failed to sync order (unhandled exception)')
|
||||||
|
sentry_sdk.capture_exception(e)
|
||||||
|
sq.set_sync_error("internal", [], str(e))
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def data_fields(self):
|
||||||
|
return {
|
||||||
|
f.key: f
|
||||||
|
for f in get_data_fields(self.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_field_value(self, inputs, mapping_entry):
|
||||||
|
key = mapping_entry["pretix_field"]
|
||||||
|
try:
|
||||||
|
field = self.data_fields[key]
|
||||||
|
except KeyError:
|
||||||
|
with language(self.event.settings.locale):
|
||||||
|
raise SyncConfigError([_(
|
||||||
|
'Field "{field_name}" does not exist. Please check your {provider_name} settings.'
|
||||||
|
).format(field_name=key, provider_name=self.display_name)])
|
||||||
|
try:
|
||||||
|
input = inputs[field.required_input]
|
||||||
|
except KeyError:
|
||||||
|
with language(self.event.settings.locale):
|
||||||
|
raise SyncConfigError([_(
|
||||||
|
'Field "{field_name}" requires {required_input}, but only got {available_inputs}. Please check your {provider_name} settings.'
|
||||||
|
).format(field_name=key, required_input=field.required_input, available_inputs=", ".join(inputs.keys()), provider_name=self.display_name)])
|
||||||
|
val = field.getter(input)
|
||||||
|
if isinstance(val, list):
|
||||||
|
if field.enum_opts and mapping_entry.get("value_map"):
|
||||||
|
map = json.loads(mapping_entry["value_map"])
|
||||||
|
try:
|
||||||
|
val = [map[el] for el in val]
|
||||||
|
except KeyError:
|
||||||
|
with language(self.event.settings.locale):
|
||||||
|
raise SyncConfigError([_(
|
||||||
|
'Please update value mapping for field "{field_name}" - option "{val}" not assigned'
|
||||||
|
).format(field_name=key, val=val)])
|
||||||
|
|
||||||
|
val = ",".join(val)
|
||||||
|
return val
|
||||||
|
|
||||||
|
def get_properties(self, inputs: dict, property_mappings: List[dict]):
|
||||||
|
return [
|
||||||
|
(m["external_field"], self.get_field_value(inputs, m), m["overwrite"])
|
||||||
|
for m in property_mappings
|
||||||
|
]
|
||||||
|
|
||||||
|
def sync_object_with_properties(
|
||||||
|
self,
|
||||||
|
external_id_field: str,
|
||||||
|
id_value,
|
||||||
|
properties: list,
|
||||||
|
inputs: dict,
|
||||||
|
mapping: ObjectMapping,
|
||||||
|
mapped_objects: dict,
|
||||||
|
**kwargs,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
This method is called for each object that needs to be created/updated in the external system -- which these are is
|
||||||
|
determined by the implementation of the `mapping` property.
|
||||||
|
|
||||||
|
:param external_id_field: Identifier field in the external system as provided in ``mapping.external_identifier``
|
||||||
|
:param id_value: Identifier contents as retrieved from the property specified by ``mapping.pretix_identifier`` of the model
|
||||||
|
specified by ``mapping.pretix_model``
|
||||||
|
:param properties: All properties defined in ``mapping.property_mappings``, as list of three-tuples
|
||||||
|
``(external_field, value, overwrite)``
|
||||||
|
:param inputs: All pretix model instances from which data can be retrieved for this mapping.
|
||||||
|
Dictionary mapping from sourcefields.ORDER_POSITION, .ORDER, .EVENT, .EVENT_OR_SUBEVENT to the
|
||||||
|
relevant Django model.
|
||||||
|
Most providers don't need to use this parameter directly, as `properties` and `id_value`
|
||||||
|
already contain the values as evaluated from the available inputs.
|
||||||
|
:param mapping: The mapping object as returned by ``self.mappings``
|
||||||
|
:param mapped_objects: Information about objects that were synced in the same sync run, by mapping definitions
|
||||||
|
*before* the current one in order of ``self.mappings``.
|
||||||
|
Type is a dictionary ``{mapping.id: [list of OrderSyncResult objects]}``
|
||||||
|
Useful to create associations between objects in the target system.
|
||||||
|
|
||||||
|
Example code to create return value::
|
||||||
|
|
||||||
|
return {
|
||||||
|
# optional:
|
||||||
|
"action": "nothing_to_do", # to inform that no action was taken, because the data was already up-to-date.
|
||||||
|
# other values for action (e.g. create, update) currently have no special
|
||||||
|
# meaning, but are visible for debugging purposes to admins.
|
||||||
|
|
||||||
|
# optional:
|
||||||
|
"external_link_href": "https://external-system.example.com/backend/link/to/contact/123/",
|
||||||
|
"external_link_display_name": "Contact #123 - Jane Doe",
|
||||||
|
"...optionally further values you need in mapped_objects for association": 123456789,
|
||||||
|
}
|
||||||
|
|
||||||
|
The return value needs to be a JSON serializable dict, or None.
|
||||||
|
|
||||||
|
Return None only in case you decide this object should not be synced at all in this mapping. Do not return None in
|
||||||
|
case the object is already up-to-date in the target system (return "action": "nothing_to_do" instead).
|
||||||
|
|
||||||
|
This method needs to be idempotent, i.e. calling it multiple times with the same input values should create
|
||||||
|
only a single object in the target system.
|
||||||
|
|
||||||
|
Subsequent calls with the same mapping and id_value should update the existing object, instead of creating a new one.
|
||||||
|
In a SQL database, you might use an `INSERT OR UPDATE` or `UPSERT` statement; many REST APIs provide an equivalent API call.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def sync_object(
|
||||||
|
self,
|
||||||
|
inputs: dict,
|
||||||
|
mapping,
|
||||||
|
mapped_objects: dict,
|
||||||
|
):
|
||||||
|
logger.debug("Syncing object %r, %r, %r", inputs, mapping, mapped_objects)
|
||||||
|
properties = self.get_properties(inputs, mapping.property_mappings)
|
||||||
|
logger.debug("Properties: %r", properties)
|
||||||
|
|
||||||
|
id_value = self.get_field_value(inputs, {"pretix_field": mapping.pretix_id_field})
|
||||||
|
if not id_value:
|
||||||
|
return None
|
||||||
|
|
||||||
|
info = self.sync_object_with_properties(
|
||||||
|
external_id_field=mapping.external_id_field,
|
||||||
|
id_value=id_value,
|
||||||
|
properties=properties,
|
||||||
|
inputs=inputs,
|
||||||
|
mapping=mapping,
|
||||||
|
mapped_objects=mapped_objects,
|
||||||
|
)
|
||||||
|
if not info:
|
||||||
|
return None
|
||||||
|
external_link_href = info.pop('external_link_href', None)
|
||||||
|
external_link_display_name = info.pop('external_link_display_name', None)
|
||||||
|
obj, created = OrderSyncResult.objects.update_or_create(
|
||||||
|
order=inputs.get(ORDER), order_position=inputs.get(ORDER_POSITION), sync_provider=self.identifier,
|
||||||
|
mapping_id=mapping.id,
|
||||||
|
defaults=dict(
|
||||||
|
external_object_type=mapping.external_object_type,
|
||||||
|
external_id_field=mapping.external_id_field,
|
||||||
|
id_value=id_value,
|
||||||
|
external_link_href=external_link_href,
|
||||||
|
external_link_display_name=external_link_display_name,
|
||||||
|
sync_info=info,
|
||||||
|
transmitted=now(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def sync_order(self, order):
|
||||||
|
if not self.should_sync_order(order):
|
||||||
|
logger.debug("Skipping order %r", order)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
logger.debug("Syncing order %r", order)
|
||||||
|
positions = list(
|
||||||
|
order.all_positions
|
||||||
|
.prefetch_related("answers", "answers__question")
|
||||||
|
.select_related(
|
||||||
|
"voucher",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
order_inputs = {ORDER: order, EVENT: self.event}
|
||||||
|
mapped_objects = {}
|
||||||
|
for mapping in self.mappings:
|
||||||
|
if mapping.pretix_model == 'Order':
|
||||||
|
mapped_objects[mapping.id] = [
|
||||||
|
self.sync_object(order_inputs, mapping, mapped_objects)
|
||||||
|
]
|
||||||
|
elif mapping.pretix_model == 'OrderPosition':
|
||||||
|
mapped_objects[mapping.id] = [
|
||||||
|
self.sync_object({
|
||||||
|
**order_inputs, EVENT_OR_SUBEVENT: op.subevent or self.event, ORDER_POSITION: op
|
||||||
|
}, mapping, mapped_objects)
|
||||||
|
for op in positions
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
raise SyncConfigError("Invalid pretix model '{}'".format(mapping.pretix_model))
|
||||||
|
self.finalize_sync_order(order)
|
||||||
|
return mapped_objects
|
||||||
|
|
||||||
|
def filter_mapped_objects(self, mapped_objects, inputs):
|
||||||
|
"""
|
||||||
|
For order positions, only
|
||||||
|
"""
|
||||||
|
if ORDER_POSITION in inputs:
|
||||||
|
return {
|
||||||
|
mapping_id: [
|
||||||
|
osr for osr in results
|
||||||
|
if osr and (osr.order_position_id is None or osr.order_position_id == inputs[ORDER_POSITION].id)
|
||||||
|
]
|
||||||
|
for mapping_id, results in mapped_objects.items()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return mapped_objects
|
||||||
|
|
||||||
|
def finalize_sync_order(self, order):
|
||||||
|
"""
|
||||||
|
Called after ``sync_object`` has been called successfully for all objects of a specific order. Can
|
||||||
|
be used for saving bulk information per order.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Called after all orders of an event have been synced. Can be used for clean-up tasks (e.g. closing
|
||||||
|
a session).
|
||||||
|
"""
|
||||||
|
pass
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user