forked from CGM_Public/pretix_original
Compare commits
224 Commits
v3.6.0
...
release/3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d1fc31cd | ||
|
|
597211d83a | ||
|
|
17679d4304 | ||
|
|
0fb70c78a9 | ||
|
|
e254e90e49 | ||
|
|
9c6e5f025d | ||
|
|
3c86532218 | ||
|
|
3834ae566f | ||
|
|
6766b2b19e | ||
|
|
b6d2f67c7c | ||
|
|
e70f593a94 | ||
|
|
ed5726fc0c | ||
|
|
5400d26c60 | ||
|
|
0bb6104532 | ||
|
|
16aa403735 | ||
|
|
1c279a92a7 | ||
|
|
35985dcb11 | ||
|
|
b0dcbe31fa | ||
|
|
b3c3ee3b22 | ||
|
|
aab340fd87 | ||
|
|
1871324ef4 | ||
|
|
d799d560b7 | ||
|
|
01e2851a76 | ||
|
|
ef2a4244ed | ||
|
|
55539dc8e5 | ||
|
|
ef303bfcc4 | ||
|
|
fff9ac04a9 | ||
|
|
76d27fbfaa | ||
|
|
2b1123b487 | ||
|
|
3607d8706d | ||
|
|
31fdf8721b | ||
|
|
128a1f349a | ||
|
|
7d432f0639 | ||
|
|
1ffc799c4d | ||
|
|
25dd8f2e2f | ||
|
|
b121596e4b | ||
|
|
cf835df62e | ||
|
|
7a3b7d4f02 | ||
|
|
b151d8f455 | ||
|
|
06de74d877 | ||
|
|
2ae9e3e0d9 | ||
|
|
0c0fe58bbf | ||
|
|
7b1e1a48ef | ||
|
|
c7dd50de0d | ||
|
|
a1caa65776 | ||
|
|
260973345d | ||
|
|
2c9b2620ea | ||
|
|
909c80e710 | ||
|
|
5a218ae6a9 | ||
|
|
b498d45621 | ||
|
|
b02196434b | ||
|
|
c0edce7760 | ||
|
|
cc46d55f5e | ||
|
|
ea8abb8dab | ||
|
|
f765d094b4 | ||
|
|
86f222870d | ||
|
|
19b5270d76 | ||
|
|
db76b9b0ef | ||
|
|
d23e53873f | ||
|
|
c116a4b998 | ||
|
|
2471d4bca5 | ||
|
|
8e04dbdcca | ||
|
|
0928358396 | ||
|
|
23f783c15c | ||
|
|
edae96c84f | ||
|
|
242ebdfae9 | ||
|
|
0ee502abec | ||
|
|
29cb1e93d8 | ||
|
|
c89242855c | ||
|
|
61a1368ed2 | ||
|
|
ac3e00fa03 | ||
|
|
d9d0f7b6f3 | ||
|
|
ad5e2df3be | ||
|
|
ec34561815 | ||
|
|
e1540b1648 | ||
|
|
a6b265455d | ||
|
|
8a6334bd86 | ||
|
|
173a23722a | ||
|
|
ab8eb2a34d | ||
|
|
30dcda616b | ||
|
|
3eafec9d6e | ||
|
|
a5910016fd | ||
|
|
0a49b93b26 | ||
|
|
7449bea836 | ||
|
|
0fc4478332 | ||
|
|
0df4a6e7ed | ||
|
|
a37cd380c8 | ||
|
|
11b2bd8887 | ||
|
|
8986db0975 | ||
|
|
2921611cb1 | ||
|
|
785fb29513 | ||
|
|
81c3d7fa17 | ||
|
|
8ff963698d | ||
|
|
6da63e0169 | ||
|
|
f84903ae27 | ||
|
|
a0a7859b33 | ||
|
|
af23d6e4bf | ||
|
|
7e9c9beace | ||
|
|
ac2fc2de5c | ||
|
|
45e548873e | ||
|
|
f484eb65df | ||
|
|
027a785ab5 | ||
|
|
25b80cbb57 | ||
|
|
589fa0f9de | ||
|
|
6d2989d15a | ||
|
|
5bb27b29ae | ||
|
|
d17f8a71e6 | ||
|
|
b664cc712a | ||
|
|
d61e8a9204 | ||
|
|
f00012a63e | ||
|
|
bd238f76ce | ||
|
|
703ae97820 | ||
|
|
1a60c5ea64 | ||
|
|
1d3ac5f02f | ||
|
|
8d23d75dfd | ||
|
|
9a32668ee1 | ||
|
|
ca0407a133 | ||
|
|
1de77b0784 | ||
|
|
d0907d3dcf | ||
|
|
81cc4bd768 | ||
|
|
262639e063 | ||
|
|
dedd93fb89 | ||
|
|
45f94aee03 | ||
|
|
d36e7d033f | ||
|
|
b94bd277bf | ||
|
|
e5095185d9 | ||
|
|
d76ce47597 | ||
|
|
58717850c2 | ||
|
|
29d52d4fe5 | ||
|
|
34c9c40ddc | ||
|
|
39d05a6c40 | ||
|
|
b664222c62 | ||
|
|
1ee48a10b5 | ||
|
|
2431a8b767 | ||
|
|
af84354e51 | ||
|
|
b04de880fc | ||
|
|
11f3057f76 | ||
|
|
ba164c16f6 | ||
|
|
7ef766ddfa | ||
|
|
bcafcc7dd8 | ||
|
|
e5b7102abc | ||
|
|
3601dd6bee | ||
|
|
a1d5854fbf | ||
|
|
09544a688d | ||
|
|
58a5892cc0 | ||
|
|
c9af76b46e | ||
|
|
91753935cf | ||
|
|
23a52eb12a | ||
|
|
79ecb231f2 | ||
|
|
08de7f59a3 | ||
|
|
0de3c33bab | ||
|
|
a4ae8b0e66 | ||
|
|
be1bf81298 | ||
|
|
b7528ae1cf | ||
|
|
4f6712ccbe | ||
|
|
939335f94b | ||
|
|
c849276a35 | ||
|
|
8e9f0f07a1 | ||
|
|
389884d191 | ||
|
|
d8c2c82da7 | ||
|
|
c3ed3d4899 | ||
|
|
e9235cd433 | ||
|
|
975b6d800a | ||
|
|
ee260c8231 | ||
|
|
f7fddc05dd | ||
|
|
eaa61c7795 | ||
|
|
d4994258e6 | ||
|
|
9b50ec2d74 | ||
|
|
447b6b7fee | ||
|
|
40f763c999 | ||
|
|
6a3d05be9e | ||
|
|
766447f021 | ||
|
|
5fbeb90f00 | ||
|
|
c01cc85eda | ||
|
|
4a054da6ee | ||
|
|
583a2b6572 | ||
|
|
fbe025afb2 | ||
|
|
66e6191122 | ||
|
|
0f26f0787c | ||
|
|
62a86c9b4a | ||
|
|
07318be4c9 | ||
|
|
3c8ef2c620 | ||
|
|
a858f47220 | ||
|
|
381fa5e1cd | ||
|
|
1539eea664 | ||
|
|
3d41d1331a | ||
|
|
00848b3339 | ||
|
|
d174b11c6a | ||
|
|
a501ce496a | ||
|
|
de277cc959 | ||
|
|
12b3ae81d6 | ||
|
|
fcdb40dda0 | ||
|
|
f65cf8e86a | ||
|
|
12540238b7 | ||
|
|
398a30e33a | ||
|
|
3410640618 | ||
|
|
7b45cfccc2 | ||
|
|
33f503aea1 | ||
|
|
3fd650081b | ||
|
|
b622854be6 | ||
|
|
6d00daa9ee | ||
|
|
f27148998a | ||
|
|
4a0369cc37 | ||
|
|
76aaf61e19 | ||
|
|
dd1e5fa929 | ||
|
|
4a2516e303 | ||
|
|
cf06712eca | ||
|
|
6185d675f0 | ||
|
|
a53cd3abce | ||
|
|
ebe86a17fb | ||
|
|
d189b16ee7 | ||
|
|
d70ce4491a | ||
|
|
607ff48d70 | ||
|
|
4bab44ca85 | ||
|
|
a5cdb485d0 | ||
|
|
282ef792c4 | ||
|
|
6cd888a1dc | ||
|
|
2e5b80c83c | ||
|
|
4511110069 | ||
|
|
1af1d8c658 | ||
|
|
9f6a3f9a6a | ||
|
|
1c03d5d305 | ||
|
|
69a1fccd20 | ||
|
|
2a54aa2d83 |
18
.travis.yml
18
.travis.yml
@@ -13,24 +13,24 @@ services:
|
|||||||
- postgresql
|
- postgresql
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=style
|
env: JOB=style
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
|
||||||
- python: 3.5
|
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.7
|
- python: 3.7
|
||||||
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
|
- python: 3.8
|
||||||
env: JOB=doc-spelling
|
env: JOB=doc-spelling
|
||||||
- python: 3.7
|
- python: 3.8
|
||||||
env: JOB=translation-spelling
|
env: JOB=translation-spelling
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.4"
|
postgresql: "10"
|
||||||
mariadb: '10.3'
|
mariadb: '10.3'
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
|||||||
-v /var/pretix-data:/data \
|
-v /var/pretix-data:/data \
|
||||||
-v /etc/pretix:/etc/pretix \
|
-v /etc/pretix:/etc/pretix \
|
||||||
-v /var/run/redis:/var/run/redis \
|
-v /var/run/redis:/var/run/redis \
|
||||||
--sysctl net.core.somaxconn=4096
|
--sysctl net.core.somaxconn=4096 \
|
||||||
pretix/standalone:stable all
|
pretix/standalone:stable all
|
||||||
ExecStop=/usr/bin/docker stop %n
|
ExecStop=/usr/bin/docker stop %n
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
|
|||||||
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
|
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
|
||||||
offers at `pretix.eu`_.
|
offers at `pretix.eu`_.
|
||||||
|
|
||||||
We tested this guide on the Linux distribution **Debian 8.0** but it should work very similar on other
|
We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
|
||||||
modern distributions, especially on all systemd-based ones.
|
modern distributions, especially on all systemd-based ones.
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
@@ -133,7 +133,7 @@ command if you're running MySQL::
|
|||||||
|
|
||||||
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
||||||
|
|
||||||
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
|
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
|
||||||
|
|
||||||
We also need to create a data directory::
|
We also need to create a data directory::
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ seating_plan integer If reserved sea
|
|||||||
seat_category_mapping object An object mapping categories of the seating plan
|
seat_category_mapping object An object mapping categories of the seating plan
|
||||||
(strings) to items in the event (integers or ``null``).
|
(strings) to items in the event (integers or ``null``).
|
||||||
timezone string Event timezone name
|
timezone string Event timezone name
|
||||||
|
item_meta_properties object Item-specific meta data parameters and default values.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +80,10 @@ timezone string Event timezone
|
|||||||
|
|
||||||
The attribute ``timezone`` has been added.
|
The attribute ``timezone`` has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.7
|
||||||
|
|
||||||
|
The attribute ``item_meta_properties`` has been added.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -133,6 +138,7 @@ Endpoints
|
|||||||
"seating_plan": null,
|
"seating_plan": null,
|
||||||
"seat_category_mapping": {},
|
"seat_category_mapping": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer"
|
"pretix.plugins.banktransfer"
|
||||||
"pretix.plugins.stripe"
|
"pretix.plugins.stripe"
|
||||||
@@ -204,6 +210,7 @@ Endpoints
|
|||||||
"seat_category_mapping": {},
|
"seat_category_mapping": {},
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer"
|
"pretix.plugins.banktransfer"
|
||||||
"pretix.plugins.stripe"
|
"pretix.plugins.stripe"
|
||||||
@@ -256,6 +263,7 @@ Endpoints
|
|||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
@@ -290,6 +298,7 @@ Endpoints
|
|||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
@@ -344,6 +353,7 @@ Endpoints
|
|||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
@@ -378,6 +388,7 @@ Endpoints
|
|||||||
"seat_category_mapping": {},
|
"seat_category_mapping": {},
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
@@ -444,6 +455,7 @@ Endpoints
|
|||||||
"seat_category_mapping": {},
|
"seat_category_mapping": {},
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"timezone": "Europe/Berlin",
|
"timezone": "Europe/Berlin",
|
||||||
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer",
|
"pretix.plugins.banktransfer",
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string secret: Only show gift cards with the given secret.
|
||||||
|
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
||||||
|
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
@@ -94,6 +97,7 @@ Endpoints
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param id: The ``id`` field of the gift card to fetch
|
:param id: The ``id`` field of the gift card to fetch
|
||||||
|
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
@@ -227,6 +231,7 @@ Endpoints
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
:param id: The ``id`` field of the gift card to modify
|
:param id: The ``id`` field of the gift card to modify
|
||||||
|
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 400: The gift card could not be modified due to invalid submitted data
|
:statuscode 400: The gift card could not be modified due to invalid submitted data
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ bundles list of objects Definition of b
|
|||||||
└ designated_price money (string) Designated price of the bundled product. This will be
|
└ designated_price money (string) Designated price of the bundled product. This will be
|
||||||
used to split the price of the base item e.g. for mixed
|
used to split the price of the base item e.g. for mixed
|
||||||
taxation. This is not added to the price.
|
taxation. This is not added to the price.
|
||||||
|
meta_data object Values set for event-specific meta data parameters.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 2.7
|
.. versionchanged:: 2.7
|
||||||
@@ -154,6 +155,10 @@ bundles list of objects Definition of b
|
|||||||
|
|
||||||
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
|
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.7
|
||||||
|
|
||||||
|
The attribute ``meta_data`` has been added.
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -208,6 +213,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
@@ -303,6 +309,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
@@ -379,6 +386,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
@@ -442,6 +450,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
@@ -537,6 +546,7 @@ Endpoints
|
|||||||
"tax_rule": 1,
|
"tax_rule": 1,
|
||||||
"admission": false,
|
"admission": false,
|
||||||
"issue_giftcard": false,
|
"issue_giftcard": false,
|
||||||
|
"meta_data": {},
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ last_modified datetime Last modificati
|
|||||||
|
|
||||||
The ``order.fees.canceled`` attribute has been added.
|
The ``order.fees.canceled`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.8
|
||||||
|
|
||||||
|
The ``reactivate`` operation has been added.
|
||||||
|
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
@@ -173,6 +177,13 @@ price money (string) Price of this p
|
|||||||
attendee_name string Specified attendee name for this position (or ``null``)
|
attendee_name string Specified attendee name for this position (or ``null``)
|
||||||
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
||||||
attendee_email string Specified attendee email address for this position (or ``null``)
|
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||||
|
company string Attendee company name (or ``null``)
|
||||||
|
street string Attendee street (or ``null``)
|
||||||
|
zipcode string Attendee ZIP code (or ``null``)
|
||||||
|
city string Attendee city (or ``null``)
|
||||||
|
country string Attendee country code (or ``null``)
|
||||||
|
state string Attendee state (ISO 3166-2 code). Only supported in
|
||||||
|
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
||||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||||
tax_rate decimal (string) VAT rate applied for this position
|
tax_rate decimal (string) VAT rate applied for this position
|
||||||
tax_value money (string) VAT included in this position
|
tax_value money (string) VAT included in this position
|
||||||
@@ -236,6 +247,10 @@ pdf_data object Data object req
|
|||||||
|
|
||||||
The attribute ``canceled`` has been added.
|
The attribute ``canceled`` has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.8
|
||||||
|
|
||||||
|
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
|
||||||
|
|
||||||
.. _order-payment-resource:
|
.. _order-payment-resource:
|
||||||
|
|
||||||
Order payment resource
|
Order payment resource
|
||||||
@@ -380,6 +395,12 @@ List of all orders
|
|||||||
"full_name": "Peter",
|
"full_name": "Peter",
|
||||||
},
|
},
|
||||||
"attendee_email": null,
|
"attendee_email": null,
|
||||||
|
"company": "Sample company",
|
||||||
|
"street": "Test street 12",
|
||||||
|
"zipcode": "12345",
|
||||||
|
"city": "Testington",
|
||||||
|
"country": "DE",
|
||||||
|
"state": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
@@ -536,6 +557,12 @@ Fetching individual orders
|
|||||||
"full_name": "Peter",
|
"full_name": "Peter",
|
||||||
},
|
},
|
||||||
"attendee_email": null,
|
"attendee_email": null,
|
||||||
|
"company": "Sample company",
|
||||||
|
"street": "Test street 12",
|
||||||
|
"zipcode": "12345",
|
||||||
|
"city": "Testington",
|
||||||
|
"country": "DE",
|
||||||
|
"state": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
@@ -816,9 +843,9 @@ Creating orders
|
|||||||
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
||||||
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
|
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
|
||||||
creation.
|
creation.
|
||||||
* ``email``
|
* ``email`` (optional)
|
||||||
* ``locale``
|
* ``locale``
|
||||||
* ``sales_channel``
|
* ``sales_channel`` (optional)
|
||||||
* ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an
|
* ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an
|
||||||
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
||||||
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
||||||
@@ -851,15 +878,21 @@ Creating orders
|
|||||||
|
|
||||||
* ``positionid`` (optional, see below)
|
* ``positionid`` (optional, see below)
|
||||||
* ``item``
|
* ``item``
|
||||||
* ``variation``
|
* ``variation`` (optional)
|
||||||
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
||||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||||
* ``attendee_name`` **or** ``attendee_name_parts``
|
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||||
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
||||||
* ``attendee_email``
|
* ``attendee_email`` (optional)
|
||||||
|
* ``company`` (optional)
|
||||||
|
* ``street`` (optional)
|
||||||
|
* ``zipcode`` (optional)
|
||||||
|
* ``city`` (optional)
|
||||||
|
* ``country`` (optional)
|
||||||
|
* ``state`` (optional)
|
||||||
* ``secret`` (optional)
|
* ``secret`` (optional)
|
||||||
* ``addon_to`` (optional, see below)
|
* ``addon_to`` (optional, see below)
|
||||||
* ``subevent``
|
* ``subevent`` (optional)
|
||||||
* ``answers``
|
* ``answers``
|
||||||
|
|
||||||
* ``question``
|
* ``question``
|
||||||
@@ -891,6 +924,13 @@ Creating orders
|
|||||||
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
|
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
|
||||||
immediately after the position itself.
|
immediately after the position itself.
|
||||||
|
|
||||||
|
Starting with pretix 3.7, you can add ``"simulate": true`` to the body to do a "dry run" of your order. This will
|
||||||
|
validate your order and return you an order object with the resulting prices, but will not create an actual order.
|
||||||
|
You can use this for testing or to look up prices. In this case, some attributes are ignored, such as whether
|
||||||
|
to send an email or what payment provider will be used. Note that some returned fields will contain empty values
|
||||||
|
(e.g. all ``id`` fields of positions will be zero) and some will contain fake values (e.g. the order code will
|
||||||
|
always be ``PREVIEW``). pretix plugins will not be triggered, so some special behavior might be missing as well.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -1050,6 +1090,42 @@ Order state operations
|
|||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
:statuscode 404: The requested order does not exist.
|
:statuscode 404: The requested order does not exist.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/reactivate/
|
||||||
|
|
||||||
|
Reactivates a canceled order. This will set the order to pending or paid state. Only possible if all products are
|
||||||
|
still available.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/reactivate/ 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
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "ABC12",
|
||||||
|
"status": "n",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param code: The ``code`` field of the order to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The order cannot be reactivated
|
||||||
|
: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 order does not exist.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
|
||||||
|
|
||||||
Marks a paid order as unpaid.
|
Marks a paid order as unpaid.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ event-related views, there is also a signal that allows you to add the view to t
|
|||||||
|
|
||||||
from django.urls import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from pretix.control.signals import nav_event
|
from pretix.control.signals import nav_event
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Order events
|
|||||||
There are multiple signals that will be sent out in the ordering cycle:
|
There are multiple signals that will be sent out in the ordering cycle:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, 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_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||||
|
|
||||||
Check-ins
|
Check-ins
|
||||||
"""""""""
|
"""""""""
|
||||||
@@ -33,11 +33,11 @@ Frontend
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, item_description
|
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: order_info, order_meta_from_request
|
:members: order_info, order_info_top, order_meta_from_request
|
||||||
|
|
||||||
Request flow
|
Request flow
|
||||||
""""""""""""
|
""""""""""""
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ A working example would be::
|
|||||||
from pretix.base.plugins import PluginConfig
|
from pretix.base.plugins import PluginConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class PaypalApp(PluginConfig):
|
class PaypalApp(PluginConfig):
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ We now need a way to translate the action codes like ``pretix.event.changed`` in
|
|||||||
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
|
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
|
||||||
implementation could look like::
|
implementation could look like::
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from pretix.base.signals import logentry_display
|
from pretix.base.signals import logentry_display
|
||||||
|
|
||||||
@receiver(signal=logentry_display)
|
@receiver(signal=logentry_display)
|
||||||
|
|||||||
277
doc/plugins/digital.rst
Normal file
277
doc/plugins/digital.rst
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
Digital content
|
||||||
|
===============
|
||||||
|
|
||||||
|
The digital content plugin provides a HTTP API that allows you to create new digital content for your ticket holders,
|
||||||
|
such as live streams, videos, or material downloads.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The digital content resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal content ID
|
||||||
|
title multi-lingual string The content title (required)
|
||||||
|
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
|
||||||
|
url string The location of the digital content
|
||||||
|
description multi-lingual string A public description of the item. May contain Markdown
|
||||||
|
syntax and is not required.
|
||||||
|
available_from datetime The first date time at which this content will be shown
|
||||||
|
(or ``null``).
|
||||||
|
available_until datetime The last date time at which this content will b e shown
|
||||||
|
(or ``null``).
|
||||||
|
all_products boolean If ``true``, the content is available to all buyers of tickets for this event. The ``limit_products`` field is ignored in this case.
|
||||||
|
limit_products list of integers List of product/item IDs. This content is only shown to buyers of these ticket types.
|
||||||
|
position integer An integer, used for sorting
|
||||||
|
subevent integer Date in an event series this content should be shown for. Should be ``null`` if this is not an event series or if this should be shown to all customers.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||||
|
|
||||||
|
Returns a list of all digital content configured for an event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ 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,
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 the event to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||||
|
|
||||||
|
Returns information on one content item, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/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: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 content to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||||
|
|
||||||
|
Create a new digital content.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 166
|
||||||
|
|
||||||
|
{
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create new content for
|
||||||
|
:param event: The ``slug`` field of the event to create new content for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The content could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create digital contents.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||||
|
|
||||||
|
Update a content. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 34
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "https://mywebsite.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"subevent": null,
|
||||||
|
"title": {
|
||||||
|
"en": "Concert livestream"
|
||||||
|
},
|
||||||
|
"content_type": "link",
|
||||||
|
"url": "https://mywebsite.com",
|
||||||
|
"description": {
|
||||||
|
"en": "Watch our event live here on YouTube!"
|
||||||
|
},
|
||||||
|
"all_products": true,
|
||||||
|
"limit_products": [],
|
||||||
|
"available_from": "2020-03-22T23:00:00Z",
|
||||||
|
"available_until": null,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the content to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The content could not be modified due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||||
|
|
||||||
|
Delete a digital content.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the content to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it
|
||||||
@@ -15,3 +15,4 @@ If you want to **create** a plugin, please go to the
|
|||||||
ticketoutputpdf
|
ticketoutputpdf
|
||||||
badges
|
badges
|
||||||
campaigns
|
campaigns
|
||||||
|
digital
|
||||||
|
|||||||
@@ -114,6 +114,17 @@ If you want to disable voucher input in the widget, you can pass the ``disable-v
|
|||||||
|
|
||||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
|
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
|
||||||
|
|
||||||
|
Filtering products
|
||||||
|
------------------
|
||||||
|
|
||||||
|
You can filter the products shown in the widget by passing in a list of product IDs::
|
||||||
|
|
||||||
|
<pretix-widget event="https://pretix.eu/demo/democon/" items="23,42"></pretix-widget>
|
||||||
|
|
||||||
|
Alternatively, you can select one or more categories to be shown::
|
||||||
|
|
||||||
|
<pretix-widget event="https://pretix.eu/demo/democon/" categories="12,25"></pretix-widget>
|
||||||
|
|
||||||
Multi-event selection
|
Multi-event selection
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|||||||
@@ -14,30 +14,23 @@ and with pretix, you can do this. On this page, you find out the necessary steps
|
|||||||
With the pretix.eu hosted service
|
With the pretix.eu hosted service
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
Step 1: DNS Configuration
|
Go to "Organizers" in the backend and select your organizer account. Then, go to "Settings" and "Custom Domain".
|
||||||
#########################
|
|
||||||
|
This page will show you instructions on how to set up your own domain. Basically, it works like this:
|
||||||
|
|
||||||
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
|
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
|
||||||
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
|
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
|
||||||
domain provider.
|
domain provider.
|
||||||
|
|
||||||
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
|
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
|
||||||
The value of the record should be ``www.pretix.eu``.
|
The value of the record should be the one shown on the "Custom Domain" page in pretix' backend.
|
||||||
|
|
||||||
Step 2: Wait for the DNS entry to propagate
|
|
||||||
###########################################
|
|
||||||
|
|
||||||
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
|
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
|
||||||
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
|
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
|
||||||
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
|
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
|
||||||
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
|
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
|
||||||
|
|
||||||
Step 3: Tell us
|
Now, tell us about your domain on the "Custom Domain" page to get started.
|
||||||
###############
|
|
||||||
|
|
||||||
Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL
|
|
||||||
certificate for you (for free!) and configure the domain.
|
|
||||||
|
|
||||||
|
|
||||||
With a custom pretix installation
|
With a custom pretix installation
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "3.6.0"
|
__version__ = "3.8.0"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from oauth2_provider.generators import (
|
from oauth2_provider.generators import (
|
||||||
generate_client_id, generate_client_secret,
|
generate_client_id, generate_client_secret,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy
|
from django.utils.translation import gettext_lazy
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||||
if len(new_quotas) == 0:
|
if len(new_quotas) == 0:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||||
str(validated_data.get('item'))
|
str(validated_data.get('item'))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -64,8 +64,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
avail = quota.availability()
|
avail = quota.availability()
|
||||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
|
gettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||||
'the operation.').format(
|
'the operation.').format(
|
||||||
quota.name
|
quota.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -88,7 +88,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
validated_data['seat'] = seat
|
validated_data['seat'] = seat
|
||||||
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
|
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
|
||||||
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||||
elif seated:
|
elif seated:
|
||||||
raise ValidationError('The specified product requires to choose a seat.')
|
raise ValidationError('The specified product requires to choose a seat.')
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_countries.serializers import CountryFieldMixin
|
from django_countries.serializers import CountryFieldMixin
|
||||||
from hierarkey.proxy import HierarkeyProxy
|
from hierarkey.proxy import HierarkeyProxy
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
@@ -34,6 +34,19 @@ class MetaDataField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MetaPropertyField(Field):
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return {
|
||||||
|
v.name: v.default for v in value.item_meta_properties.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return {
|
||||||
|
'item_meta_properties': data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SeatCategoryMappingField(Field):
|
class SeatCategoryMappingField(Field):
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
@@ -77,6 +90,7 @@ class TimeZoneField(ChoiceField):
|
|||||||
|
|
||||||
class EventSerializer(I18nAwareModelSerializer):
|
class EventSerializer(I18nAwareModelSerializer):
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
|
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||||
plugins = PluginsField(required=False, source='*')
|
plugins = PluginsField(required=False, source='*')
|
||||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||||
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
||||||
@@ -86,7 +100,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||||
'plugins', 'seat_category_mapping', 'timezone')
|
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties')
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -131,6 +145,12 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def item_meta_props(self):
|
||||||
|
return {
|
||||||
|
p.name: p for p in self.context['request'].event.item_meta_properties.all()
|
||||||
|
}
|
||||||
|
|
||||||
def validate_seating_plan(self, value):
|
def validate_seating_plan(self, value):
|
||||||
if value and value.organizer != self.context['request'].organizer:
|
if value and value.organizer != self.context['request'].organizer:
|
||||||
raise ValidationError('Invalid seating plan.')
|
raise ValidationError('Invalid seating plan.')
|
||||||
@@ -172,6 +192,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
item_meta_properties = validated_data.pop('item_meta_properties', None)
|
||||||
validated_data.pop('seat_category_mapping', None)
|
validated_data.pop('seat_category_mapping', None)
|
||||||
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
||||||
tz = validated_data.pop('timezone', None)
|
tz = validated_data.pop('timezone', None)
|
||||||
@@ -188,6 +209,15 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
value=value
|
value=value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Item Meta properties
|
||||||
|
if item_meta_properties is not None:
|
||||||
|
for key, value in item_meta_properties.items():
|
||||||
|
event.item_meta_properties.create(
|
||||||
|
name=key,
|
||||||
|
default=value,
|
||||||
|
event=event
|
||||||
|
)
|
||||||
|
|
||||||
# Seats
|
# Seats
|
||||||
if event.seating_plan:
|
if event.seating_plan:
|
||||||
generate_seats(event, None, event.seating_plan, {})
|
generate_seats(event, None, event.seating_plan, {})
|
||||||
@@ -202,6 +232,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
item_meta_properties = validated_data.pop('item_meta_properties', None)
|
||||||
plugins = validated_data.pop('plugins', None)
|
plugins = validated_data.pop('plugins', None)
|
||||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||||
tz = validated_data.pop('timezone', None)
|
tz = validated_data.pop('timezone', None)
|
||||||
@@ -228,6 +259,26 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
if prop.name not in meta_data:
|
if prop.name not in meta_data:
|
||||||
current_object.delete()
|
current_object.delete()
|
||||||
|
|
||||||
|
# Item Meta properties
|
||||||
|
if item_meta_properties is not None:
|
||||||
|
current = [imp for imp in event.item_meta_properties.all()]
|
||||||
|
for key, value in item_meta_properties.items():
|
||||||
|
prop = self.item_meta_props.get(key)
|
||||||
|
if prop in current:
|
||||||
|
prop.default = value
|
||||||
|
prop.save()
|
||||||
|
else:
|
||||||
|
prop = event.item_meta_properties.create(
|
||||||
|
name=key,
|
||||||
|
default=value,
|
||||||
|
event=event
|
||||||
|
)
|
||||||
|
current.append(prop)
|
||||||
|
|
||||||
|
for prop in current:
|
||||||
|
if prop.name not in list(item_meta_properties.keys()):
|
||||||
|
prop.delete()
|
||||||
|
|
||||||
# Seats
|
# Seats
|
||||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
||||||
current_mappings = {
|
current_mappings = {
|
||||||
@@ -481,6 +532,9 @@ class EventSettingsSerializer(serializers.Serializer):
|
|||||||
'checkout_email_helptext',
|
'checkout_email_helptext',
|
||||||
'presale_has_ended_text',
|
'presale_has_ended_text',
|
||||||
'voucher_explanation_text',
|
'voucher_explanation_text',
|
||||||
|
'banner_text',
|
||||||
|
'banner_text_bottom',
|
||||||
|
'show_dates_on_frontpage',
|
||||||
'show_date_to',
|
'show_date_to',
|
||||||
'show_times',
|
'show_times',
|
||||||
'show_items_outside_presale_period',
|
'show_items_outside_presale_period',
|
||||||
@@ -506,6 +560,10 @@ class EventSettingsSerializer(serializers.Serializer):
|
|||||||
'attendee_names_required',
|
'attendee_names_required',
|
||||||
'attendee_emails_asked',
|
'attendee_emails_asked',
|
||||||
'attendee_emails_required',
|
'attendee_emails_required',
|
||||||
|
'attendee_addresses_asked',
|
||||||
|
'attendee_addresses_required',
|
||||||
|
'attendee_company_asked',
|
||||||
|
'attendee_company_required',
|
||||||
'confirm_text',
|
'confirm_text',
|
||||||
'order_email_asked_twice',
|
'order_email_asked_twice',
|
||||||
'payment_term_days',
|
'payment_term_days',
|
||||||
@@ -528,6 +586,7 @@ class EventSettingsSerializer(serializers.Serializer):
|
|||||||
'invoice_address_vatid',
|
'invoice_address_vatid',
|
||||||
'invoice_address_company_required',
|
'invoice_address_company_required',
|
||||||
'invoice_address_beneficiary',
|
'invoice_address_beneficiary',
|
||||||
|
'invoice_address_custom_field',
|
||||||
'invoice_name_required',
|
'invoice_name_required',
|
||||||
'invoice_address_not_asked_free',
|
'invoice_address_not_asked_free',
|
||||||
'invoice_show_payments',
|
'invoice_show_payments',
|
||||||
@@ -558,6 +617,10 @@ class EventSettingsSerializer(serializers.Serializer):
|
|||||||
'cancel_allow_user_paid_keep',
|
'cancel_allow_user_paid_keep',
|
||||||
'cancel_allow_user_paid_keep_fees',
|
'cancel_allow_user_paid_keep_fees',
|
||||||
'cancel_allow_user_paid_keep_percentage',
|
'cancel_allow_user_paid_keep_percentage',
|
||||||
|
'cancel_allow_user_paid_adjust_fees',
|
||||||
|
'cancel_allow_user_paid_adjust_fees_explanation',
|
||||||
|
'cancel_allow_user_paid_refund_as_giftcard',
|
||||||
|
'cancel_allow_user_paid_require_approval',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
29
src/pretix/api/serializers/fields.py
Normal file
29
src/pretix/api/serializers/fields.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
def remove_duplicates_from_list(data):
|
||||||
|
return list(OrderedDict.fromkeys(data))
|
||||||
|
|
||||||
|
|
||||||
|
class ListMultipleChoiceField(serializers.MultipleChoiceField):
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if isinstance(data, str) or not hasattr(data, '__iter__'):
|
||||||
|
self.fail('not_a_list', input_type=type(data).__name__)
|
||||||
|
if not self.allow_empty and len(data) == 0:
|
||||||
|
self.fail('empty')
|
||||||
|
|
||||||
|
internal_value_data = [
|
||||||
|
super(serializers.MultipleChoiceField, self).to_internal_value(item)
|
||||||
|
for item in data
|
||||||
|
]
|
||||||
|
|
||||||
|
return remove_duplicates_from_list(internal_value_data)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
representation_data = [
|
||||||
|
self.choice_strings_to_values.get(str(item), item) for item in value
|
||||||
|
]
|
||||||
|
|
||||||
|
return remove_duplicates_from_list(representation_data)
|
||||||
@@ -2,13 +2,15 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.api.serializers.event import MetaDataField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||||
QuestionOption, Quota,
|
Question, QuestionOption, Quota,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -110,6 +112,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||||
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
@@ -119,7 +122,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard')
|
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
|
||||||
read_only_fields = ('has_variations', 'picture')
|
read_only_fields = ('has_variations', 'picture')
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@@ -167,18 +170,65 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
|
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def item_meta_properties(self):
|
||||||
|
return {
|
||||||
|
p.name: p for p in self.context['request'].event.item_meta_properties.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_meta_data(self, value):
|
||||||
|
for key in value['meta_data'].keys():
|
||||||
|
if key not in self.item_meta_properties:
|
||||||
|
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
|
||||||
|
return value
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
||||||
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
||||||
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
||||||
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
item = Item.objects.create(**validated_data)
|
item = Item.objects.create(**validated_data)
|
||||||
|
|
||||||
for variation_data in variations_data:
|
for variation_data in variations_data:
|
||||||
ItemVariation.objects.create(item=item, **variation_data)
|
ItemVariation.objects.create(item=item, **variation_data)
|
||||||
for addon_data in addons_data:
|
for addon_data in addons_data:
|
||||||
ItemAddOn.objects.create(base_item=item, **addon_data)
|
ItemAddOn.objects.create(base_item=item, **addon_data)
|
||||||
for bundle_data in bundles_data:
|
for bundle_data in bundles_data:
|
||||||
ItemBundle.objects.create(base_item=item, **bundle_data)
|
ItemBundle.objects.create(base_item=item, **bundle_data)
|
||||||
|
|
||||||
|
# Meta data
|
||||||
|
if meta_data is not None:
|
||||||
|
for key, value in meta_data.items():
|
||||||
|
ItemMetaValue.objects.create(
|
||||||
|
property=self.item_meta_properties.get(key),
|
||||||
|
value=value,
|
||||||
|
item=item
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
item = super().update(instance, validated_data)
|
||||||
|
|
||||||
|
# Meta data
|
||||||
|
if meta_data is not None:
|
||||||
|
current = {mv.property: mv for mv in item.meta_values.select_related('property')}
|
||||||
|
for key, value in meta_data.items():
|
||||||
|
prop = self.item_meta_properties.get(key)
|
||||||
|
if prop in current:
|
||||||
|
current[prop].value = value
|
||||||
|
current[prop].save()
|
||||||
|
else:
|
||||||
|
item.meta_values.create(
|
||||||
|
property=self.item_meta_properties.get(key),
|
||||||
|
value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
for prop, current_object in current.items():
|
||||||
|
if prop.name not in meta_data:
|
||||||
|
current_object.delete()
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
@@ -237,8 +287,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
if value:
|
if value:
|
||||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||||
if value == self.instance:
|
if value == self.instance:
|
||||||
raise ValidationError('A question cannot depend on itself.')
|
raise ValidationError('A question cannot depend on itself.')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from decimal import Decimal
|
|||||||
import pycountry
|
import pycountry
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy
|
from django.utils.translation import gettext_lazy
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
@@ -25,6 +25,7 @@ from pretix.base.models.orders import (
|
|||||||
)
|
)
|
||||||
from pretix.base.pdf import get_variables
|
from pretix.base.pdf import get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
|
from pretix.base.services.locking import NoLockManager
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
@@ -38,7 +39,7 @@ class CompatibleCountryField(serializers.Field):
|
|||||||
def to_representation(self, instance: InvoiceAddress):
|
def to_representation(self, instance: InvoiceAddress):
|
||||||
if instance.country:
|
if instance.country:
|
||||||
return str(instance.country)
|
return str(instance.country)
|
||||||
else:
|
elif hasattr(instance, 'country_old'):
|
||||||
return instance.country_old
|
return instance.country_old
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +97,11 @@ class AnswerQuestionOptionsIdentifierField(serializers.Field):
|
|||||||
return [o.identifier for o in instance.options.all()]
|
return [o.identifier for o in instance.options.all()]
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerQuestionOptionsField(serializers.Field):
|
||||||
|
def to_representation(self, instance: QuestionAnswer):
|
||||||
|
return [o.pk for o in instance.options.all()]
|
||||||
|
|
||||||
|
|
||||||
class InlineSeatSerializer(I18nAwareModelSerializer):
|
class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -106,6 +112,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
|||||||
class AnswerSerializer(I18nAwareModelSerializer):
|
class AnswerSerializer(I18nAwareModelSerializer):
|
||||||
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
||||||
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
||||||
|
options = AnswerQuestionOptionsField(source='*', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = QuestionAnswer
|
model = QuestionAnswer
|
||||||
@@ -189,6 +196,11 @@ class PdfDataSerializer(serializers.Field):
|
|||||||
for k, v in ev._cached_meta_data.items():
|
for k, v in ev._cached_meta_data.items():
|
||||||
res['meta:' + k] = v
|
res['meta:' + k] = v
|
||||||
|
|
||||||
|
if not hasattr(instance.item, '_cached_meta_data'):
|
||||||
|
instance.item._cached_meta_data = instance.item.meta_data
|
||||||
|
for k, v in instance.item._cached_meta_data.items():
|
||||||
|
res['itemmeta:' + k] = v
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@@ -199,10 +211,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||||
pdf_data = PdfDataSerializer(source='*')
|
pdf_data = PdfDataSerializer(source='*')
|
||||||
seat = InlineSeatSerializer(read_only=True)
|
seat = InlineSeatSerializer(read_only=True)
|
||||||
|
country = CompatibleCountryField(source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
|
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||||
|
|
||||||
@@ -504,12 +518,22 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
max_digits=10)
|
max_digits=10)
|
||||||
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
||||||
required=False, allow_null=True)
|
required=False, allow_null=True)
|
||||||
|
country = CompatibleCountryField(source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
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',
|
||||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for k, v in self.fields.items():
|
||||||
|
if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'):
|
||||||
|
v.required = False
|
||||||
|
v.allow_blank = True
|
||||||
|
v.allow_null = True
|
||||||
|
|
||||||
def validate_secret(self, secret):
|
def validate_secret(self, secret):
|
||||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@@ -564,6 +588,24 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||||
|
|
||||||
|
if data.get('country'):
|
||||||
|
if not pycountry.countries.get(alpha_2=data.get('country')):
|
||||||
|
raise ValidationError(
|
||||||
|
{'country': ['Invalid country code.']}
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.get('state'):
|
||||||
|
cc = str(data.get('country') or self.instance.country or '')
|
||||||
|
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
|
raise ValidationError(
|
||||||
|
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||||
|
)
|
||||||
|
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||||
|
raise ValidationError(
|
||||||
|
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||||
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -580,6 +622,28 @@ class CompatibleJSONField(serializers.JSONField):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class WrappedList:
|
||||||
|
def __init__(self, data):
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def all(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
|
class WrappedModel:
|
||||||
|
def __init__(self, model):
|
||||||
|
self._wrapped = model
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
return getattr(self._wrapped, item)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class OrderCreateSerializer(I18nAwareModelSerializer):
|
class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||||
invoice_address = InvoiceAddressSerializer(required=False)
|
invoice_address = InvoiceAddressSerializer(required=False)
|
||||||
positions = OrderPositionCreateSerializer(many=True, required=True)
|
positions = OrderPositionCreateSerializer(many=True, required=True)
|
||||||
@@ -600,6 +664,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
force = serializers.BooleanField(default=False, required=False)
|
force = serializers.BooleanField(default=False, required=False)
|
||||||
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
||||||
send_mail = serializers.BooleanField(default=False, required=False)
|
send_mail = serializers.BooleanField(default=False, required=False)
|
||||||
|
simulate = serializers.BooleanField(default=False, required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -609,7 +674,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
model = Order
|
model = Order
|
||||||
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
||||||
'force', 'send_mail')
|
'force', 'send_mail', 'simulate')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -701,6 +766,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
payment_info = validated_data.pop('payment_info', '{}')
|
payment_info = validated_data.pop('payment_info', '{}')
|
||||||
payment_date = validated_data.pop('payment_date', now())
|
payment_date = validated_data.pop('payment_date', now())
|
||||||
force = validated_data.pop('force', False)
|
force = validated_data.pop('force', False)
|
||||||
|
simulate = validated_data.pop('simulate', False)
|
||||||
self._send_mail = validated_data.pop('send_mail', False)
|
self._send_mail = validated_data.pop('send_mail', False)
|
||||||
|
|
||||||
if 'invoice_address' in validated_data:
|
if 'invoice_address' in validated_data:
|
||||||
@@ -714,7 +780,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
ia = None
|
ia = None
|
||||||
|
|
||||||
with self.context['event'].lock() as now_dt:
|
lockfn = self.context['event'].lock
|
||||||
|
if simulate:
|
||||||
|
lockfn = NoLockManager
|
||||||
|
with lockfn() as now_dt:
|
||||||
free_seats = set()
|
free_seats = set()
|
||||||
seats_seen = set()
|
seats_seen = set()
|
||||||
consume_carts = validated_data.pop('consume_carts', [])
|
consume_carts = validated_data.pop('consume_carts', [])
|
||||||
@@ -823,7 +892,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
pos_data['seat'] = seat
|
pos_data['seat'] = seat
|
||||||
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
|
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
|
||||||
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||||
seats_seen.add(seat)
|
seats_seen.add(seat)
|
||||||
elif seated:
|
elif seated:
|
||||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||||
@@ -838,7 +907,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
if pos_data.get('variation')
|
if pos_data.get('variation')
|
||||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||||
if len(new_quotas) == 0:
|
if len(new_quotas) == 0:
|
||||||
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||||
str(pos_data.get('item'))
|
str(pos_data.get('item'))
|
||||||
)]
|
)]
|
||||||
else:
|
else:
|
||||||
@@ -850,7 +919,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
quota_avail_cache[quota][1] -= 1
|
quota_avail_cache[quota][1] -= 1
|
||||||
if quota_avail_cache[quota][1] < 0:
|
if quota_avail_cache[quota][1] < 0:
|
||||||
errs[i]['item'] = [
|
errs[i]['item'] = [
|
||||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||||
quota.name
|
quota.name
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -864,11 +933,20 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||||
order.meta_info = "{}"
|
order.meta_info = "{}"
|
||||||
order.total = Decimal('0.00')
|
order.total = Decimal('0.00')
|
||||||
order.save()
|
if simulate:
|
||||||
|
order = WrappedModel(order)
|
||||||
|
order.last_modified = now()
|
||||||
|
order.code = 'PREVIEW'
|
||||||
|
else:
|
||||||
|
order.save()
|
||||||
|
|
||||||
if ia:
|
if ia:
|
||||||
ia.order = order
|
if not simulate:
|
||||||
ia.save()
|
ia.order = order
|
||||||
|
ia.save()
|
||||||
|
else:
|
||||||
|
order.invoice_address = ia
|
||||||
|
ia.last_modified = now()
|
||||||
|
|
||||||
pos_map = {}
|
pos_map = {}
|
||||||
for pos_data in positions_data:
|
for pos_data in positions_data:
|
||||||
@@ -880,7 +958,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
'_legacy': attendee_name
|
'_legacy': attendee_name
|
||||||
}
|
}
|
||||||
pos = OrderPosition(**pos_data)
|
pos = OrderPosition(**pos_data)
|
||||||
pos.order = order
|
if simulate:
|
||||||
|
pos.order = order._wrapped
|
||||||
|
else:
|
||||||
|
pos.order = order
|
||||||
if addon_to:
|
if addon_to:
|
||||||
pos.addon_to = pos_map[addon_to]
|
pos.addon_to = pos_map[addon_to]
|
||||||
|
|
||||||
@@ -911,19 +992,33 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
invoice_address=ia,
|
invoice_address=ia,
|
||||||
).gross
|
).gross
|
||||||
|
|
||||||
if pos.voucher:
|
if simulate:
|
||||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
pos = WrappedModel(pos)
|
||||||
pos.save()
|
pos.id = 0
|
||||||
|
answers = []
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options', [])
|
||||||
|
answ = WrappedModel(QuestionAnswer(**answ_data))
|
||||||
|
answ.options = WrappedList(options)
|
||||||
|
answers.append(answ)
|
||||||
|
pos.answers = answers
|
||||||
|
pos.pseudonymization_id = "PREVIEW"
|
||||||
|
else:
|
||||||
|
if pos.voucher:
|
||||||
|
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||||
|
pos.save()
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options', [])
|
||||||
|
answ = pos.answers.create(**answ_data)
|
||||||
|
answ.options.add(*options)
|
||||||
pos_map[pos.positionid] = pos
|
pos_map[pos.positionid] = pos
|
||||||
for answ_data in answers_data:
|
|
||||||
options = answ_data.pop('options', [])
|
|
||||||
answ = pos.answers.create(**answ_data)
|
|
||||||
answ.options.add(*options)
|
|
||||||
|
|
||||||
for cp in delete_cps:
|
if not simulate:
|
||||||
cp.delete()
|
for cp in delete_cps:
|
||||||
|
cp.delete()
|
||||||
|
|
||||||
order.total = sum([p.price for p in order.positions.all()])
|
order.total = sum([p.price for p in pos_map.values()])
|
||||||
|
fees = []
|
||||||
for fee_data in fees_data:
|
for fee_data in fees_data:
|
||||||
is_percentage = fee_data.pop('_treat_value_as_percentage', False)
|
is_percentage = fee_data.pop('_treat_value_as_percentage', False)
|
||||||
if is_percentage:
|
if is_percentage:
|
||||||
@@ -955,17 +1050,26 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
fee_data['tax_rule'] = tr
|
fee_data['tax_rule'] = tr
|
||||||
fee_data['value'] = val
|
fee_data['value'] = val
|
||||||
f = OrderFee(**fee_data)
|
f = OrderFee(**fee_data)
|
||||||
f.order = order
|
f.order = order._wrapped if simulate else order
|
||||||
f._calculate_tax()
|
f._calculate_tax()
|
||||||
f.save()
|
fees.append(f)
|
||||||
|
if not simulate:
|
||||||
|
f.save()
|
||||||
else:
|
else:
|
||||||
f = OrderFee(**fee_data)
|
f = OrderFee(**fee_data)
|
||||||
f.order = order
|
f.order = order._wrapped if simulate else order
|
||||||
f._calculate_tax()
|
f._calculate_tax()
|
||||||
f.save()
|
fees.append(f)
|
||||||
|
if not simulate:
|
||||||
|
f.save()
|
||||||
|
|
||||||
order.total += sum([f.value for f in order.fees.all()])
|
order.total += sum([f.value for f in fees])
|
||||||
order.save(update_fields=['total'])
|
if simulate:
|
||||||
|
order.fees = fees
|
||||||
|
order.positions = pos_map.values()
|
||||||
|
return order # ignore payments
|
||||||
|
else:
|
||||||
|
order.save(update_fields=['total'])
|
||||||
|
|
||||||
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
||||||
payment_provider = 'free'
|
payment_provider = 'free'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import get_language, ugettext_lazy as _
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from oauth2_provider.exceptions import OAuthToolkitError
|
from oauth2_provider.exceptions import OAuthToolkitError
|
||||||
from oauth2_provider.forms import AllowForm
|
from oauth2_provider.forms import AllowForm
|
||||||
from oauth2_provider.views import (
|
from oauth2_provider.views import (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.db.models.functions import Coalesce, Concat
|
|||||||
from django.http import FileResponse, HttpResponse
|
from django.http import FileResponse, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
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 rest_framework import mixins, serializers, status, viewsets
|
from rest_framework import mixins, serializers, status, viewsets
|
||||||
@@ -44,7 +44,7 @@ from pretix.base.services.mail import SendMailException
|
|||||||
from pretix.base.services.orders import (
|
from pretix.base.services.orders import (
|
||||||
OrderChangeManager, OrderError, _order_placed_email,
|
OrderChangeManager, OrderError, _order_placed_email,
|
||||||
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
||||||
extend_order, mark_order_expired, mark_order_refunded,
|
extend_order, mark_order_expired, mark_order_refunded, reactivate_order,
|
||||||
)
|
)
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
from pretix.base.services.tickets import generate
|
from pretix.base.services.tickets import generate
|
||||||
@@ -261,6 +261,29 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def reactivate(self, request, **kwargs):
|
||||||
|
|
||||||
|
order = self.get_object()
|
||||||
|
if order.status != Order.STATUS_CANCELED:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'The order is not allowed to be reactivated.'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
reactivate_order(
|
||||||
|
order,
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
|
||||||
|
)
|
||||||
|
except OrderError as e:
|
||||||
|
return Response(
|
||||||
|
{'detail': str(e)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def approve(self, request, **kwargs):
|
def approve(self, request, **kwargs):
|
||||||
send_mail = request.data.get('send_email', True)
|
send_mail = request.data.get('send_email', True)
|
||||||
@@ -466,6 +489,9 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
send_mail = serializer._send_mail
|
send_mail = serializer._send_mail
|
||||||
order = serializer.instance
|
order = serializer.instance
|
||||||
serializer = OrderSerializer(order, context=serializer.context)
|
serializer = OrderSerializer(order, context=serializer.context)
|
||||||
|
if not order.pk:
|
||||||
|
# Simulation
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
order.log_action(
|
order.log_action(
|
||||||
'pretix.event.order.placed',
|
'pretix.event.order.placed',
|
||||||
@@ -1078,11 +1104,14 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
auth=request.auth
|
auth=request.auth
|
||||||
)
|
)
|
||||||
if mark_refunded:
|
if mark_refunded:
|
||||||
mark_order_refunded(
|
try:
|
||||||
r.order,
|
mark_order_refunded(
|
||||||
user=request.user if request.user.is_authenticated else None,
|
r.order,
|
||||||
auth=(request.auth if request.auth else None),
|
user=request.user if request.user.is_authenticated else None,
|
||||||
)
|
auth=(request.auth if request.auth else None),
|
||||||
|
)
|
||||||
|
except OrderError as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
elif mark_pending:
|
elif mark_pending:
|
||||||
if r.order.status == Order.STATUS_PAID and r.order.pending_sum > 0:
|
if r.order.status == Order.STATUS_PAID and r.order.pending_sum > 0:
|
||||||
r.order.status = Order.STATUS_PENDING
|
r.order.status = Order.STATUS_PENDING
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import django_filters
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import filters, serializers, status, viewsets
|
from rest_framework import filters, serializers, status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
|
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
|
||||||
@@ -55,7 +58,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|||||||
write_permission = 'can_change_organizer_settings'
|
write_permission = 'can_change_organizer_settings'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.organizer.seating_plans.all()
|
return self.request.organizer.seating_plans.order_by('name')
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
@@ -98,14 +101,29 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|||||||
instance.delete()
|
instance.delete()
|
||||||
|
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
class GiftCardFilter(FilterSet):
|
||||||
|
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GiftCard
|
||||||
|
fields = ['secret', 'testmode']
|
||||||
|
|
||||||
|
|
||||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = GiftCardSerializer
|
serializer_class = GiftCardSerializer
|
||||||
queryset = GiftCard.objects.none()
|
queryset = GiftCard.objects.none()
|
||||||
permission = 'can_manage_gift_cards'
|
permission = 'can_manage_gift_cards'
|
||||||
write_permission = 'can_manage_gift_cards'
|
write_permission = 'can_manage_gift_cards'
|
||||||
|
filter_backends = (DjangoFilterBackend,)
|
||||||
|
filterset_class = GiftCardFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.organizer.issued_gift_cards.all()
|
if self.request.GET.get('include_accepted') == 'true':
|
||||||
|
qs = self.request.organizer.accepted_gift_cards
|
||||||
|
else:
|
||||||
|
qs = self.request.organizer.issued_gift_cards.all()
|
||||||
|
return qs
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
@@ -126,6 +144,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
@transaction.atomic()
|
@transaction.atomic()
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
|
if 'include_accepted' in self.request.GET:
|
||||||
|
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
|
||||||
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
|
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
|
||||||
old_value = serializer.instance.value
|
old_value = serializer.instance.value
|
||||||
value = serializer.validated_data.pop('value')
|
value = serializer.validated_data.pop('value')
|
||||||
@@ -148,16 +168,19 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
||||||
request.data.get('value')
|
request.data.get('value')
|
||||||
)
|
)
|
||||||
|
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
|
||||||
|
request.data.get('text', '')
|
||||||
|
)
|
||||||
if gc.value + value < Decimal('0.00'):
|
if gc.value + value < Decimal('0.00'):
|
||||||
return Response({
|
return Response({
|
||||||
'value': ['The gift card does not have sufficient credit for this operation.']
|
'value': ['The gift card does not have sufficient credit for this operation.']
|
||||||
}, status=status.HTTP_409_CONFLICT)
|
}, status=status.HTTP_409_CONFLICT)
|
||||||
gc.transactions.create(value=value)
|
gc.transactions.create(value=value, text=text)
|
||||||
gc.log_action(
|
gc.log_action(
|
||||||
'pretix.giftcards.transaction.manual',
|
'pretix.giftcards.transaction.manual',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data={'value': value}
|
data={'value': value, 'text': text}
|
||||||
)
|
)
|
||||||
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
|
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@@ -172,7 +195,7 @@ class TeamViewSet(viewsets.ModelViewSet):
|
|||||||
write_permission = 'can_change_teams'
|
write_permission = 'can_change_teams'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.organizer.teams.all()
|
return self.request.organizer.teams.order_by('pk')
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
@@ -245,7 +268,7 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
|
|||||||
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.team.invites.all()
|
return self.team.invites.order_by('email')
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
@@ -282,7 +305,7 @@ class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
|||||||
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.team.tokens.all()
|
return self.team.tokens.order_by('name')
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import requests
|
|||||||
from celery.exceptions import MaxRetriesExceededError
|
from celery.exceptions import MaxRetriesExceededError
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
@@ -125,6 +125,10 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.order.canceled',
|
'pretix.event.order.canceled',
|
||||||
_('Order canceled'),
|
_('Order canceled'),
|
||||||
),
|
),
|
||||||
|
ParametrizedOrderWebhookEvent(
|
||||||
|
'pretix.event.order.reactivated',
|
||||||
|
_('Order reactivated'),
|
||||||
|
),
|
||||||
ParametrizedOrderWebhookEvent(
|
ParametrizedOrderWebhookEvent(
|
||||||
'pretix.event.order.expired',
|
'pretix.event.order.expired',
|
||||||
_('Order expired'),
|
_('Order expired'),
|
||||||
|
|||||||
@@ -85,6 +85,16 @@ class BaseAuthBackend:
|
|||||||
"""
|
"""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get_next_url(self, request):
|
||||||
|
"""
|
||||||
|
This method will be called after a successful login to determine the next URL. Pretix in general uses the
|
||||||
|
``'next'`` query parameter. However, external authentication methods could use custom attributes with hardcoded
|
||||||
|
names for security purposes. For example, OAuth uses ``'state'`` for keeping track of application state.
|
||||||
|
"""
|
||||||
|
if "next" in request.GET:
|
||||||
|
return request.GET.get("next")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class NativeAuthBackend(BaseAuthBackend):
|
class NativeAuthBackend(BaseAuthBackend):
|
||||||
identifier = 'native'
|
identifier = 'native'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.signals import register_sales_channels
|
from pretix.base.signals import register_sales_channels
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.core.mail.backends.smtp import EmailBackend
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import get_language, ugettext_lazy as _
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
from inlinestyler.utils import inline_css
|
from inlinestyler.utils import inline_css
|
||||||
|
|
||||||
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
||||||
@@ -267,6 +267,10 @@ def base_placeholders(sender, **kwargs):
|
|||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
'event', ['event'], lambda event: event.name, lambda event: event.name
|
'event', ['event'], lambda event: event.name, lambda event: event.name
|
||||||
),
|
),
|
||||||
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
|
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
||||||
|
lambda event_or_subevent: event_or_subevent.name
|
||||||
|
),
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
||||||
),
|
),
|
||||||
@@ -279,6 +283,11 @@ def base_placeholders(sender, **kwargs):
|
|||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
||||||
),
|
),
|
||||||
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
|
'refund_amount', ['event_or_subevent', 'refund_amount'],
|
||||||
|
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
|
||||||
|
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
|
||||||
|
),
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
||||||
event.currency),
|
event.currency),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import Tuple
|
|||||||
from defusedcsv import csv
|
from defusedcsv import csv
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from openpyxl.cell.cell import KNOWN_TYPES
|
from openpyxl.cell.cell import KNOWN_TYPES
|
||||||
|
|
||||||
@@ -180,9 +180,9 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
]
|
]
|
||||||
for s, l in self.sheets:
|
for s, l in self.sheets:
|
||||||
choices += [
|
choices += [
|
||||||
(s + ':default', str(l) + ' – ' + ugettext('CSV (with commas)')),
|
(s + ':default', str(l) + ' – ' + gettext('CSV (with commas)')),
|
||||||
(s + ':excel', str(l) + ' – ' + ugettext('CSV (Excel-style)')),
|
(s + ':excel', str(l) + ' – ' + gettext('CSV (Excel-style)')),
|
||||||
(s + ':semicolon', str(l) + ' – ' + ugettext('CSV (with semicolons)')),
|
(s + ':semicolon', str(l) + ' – ' + gettext('CSV (with semicolons)')),
|
||||||
]
|
]
|
||||||
ff = OrderedDict(
|
ff = OrderedDict(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from zipfile import ZipFile
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import QuestionAnswer
|
from pretix.base.models import QuestionAnswer
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dateutil
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext, ugettext_lazy
|
from django.utils.translation import gettext, gettext_lazy
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Invoice, OrderPayment
|
from pretix.base.models import Invoice, OrderPayment
|
||||||
@@ -79,7 +79,7 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
payments.append({
|
payments.append({
|
||||||
'PTID': '5',
|
'PTID': '5',
|
||||||
'PTN': 'Lastschrift',
|
'PTN': 'Lastschrift',
|
||||||
'PTNo4': ugettext('Event ticket {event}-{code}').format(
|
'PTNo4': gettext('Event ticket {event}-{code}').format(
|
||||||
event=self.event.slug.upper(),
|
event=self.event.slug.upper(),
|
||||||
code=invoice.order.code
|
code=invoice.order.code
|
||||||
),
|
),
|
||||||
@@ -199,19 +199,19 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
[
|
[
|
||||||
('date_from',
|
('date_from',
|
||||||
forms.DateField(
|
forms.DateField(
|
||||||
label=ugettext_lazy('Start date'),
|
label=gettext_lazy('Start date'),
|
||||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=ugettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
|
help_text=gettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
|
||||||
'not always correspond to the order or payment date.')
|
'not always correspond to the order or payment date.')
|
||||||
)),
|
)),
|
||||||
('date_to',
|
('date_to',
|
||||||
forms.DateField(
|
forms.DateField(
|
||||||
label=ugettext_lazy('End date'),
|
label=gettext_lazy('End date'),
|
||||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=ugettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
|
help_text=gettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
|
||||||
'does not always correspond to the order or payment date.')
|
'does not always correspond to the order or payment date.')
|
||||||
)),
|
)),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import dateutil.parser
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import OrderPayment
|
from pretix.base.models import OrderPayment
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import OrderPosition
|
from pretix.base.models import OrderPosition
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
|
from django.utils.translation import gettext as _, gettext_lazy, pgettext
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
@@ -22,7 +22,7 @@ from ..signals import register_data_exporters
|
|||||||
|
|
||||||
class OrderListExporter(MultiSheetListExporter):
|
class OrderListExporter(MultiSheetListExporter):
|
||||||
identifier = 'orderlist'
|
identifier = 'orderlist'
|
||||||
verbose_name = ugettext_lazy('Order data')
|
verbose_name = gettext_lazy('Order data')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sheets(self):
|
def sheets(self):
|
||||||
@@ -305,6 +305,12 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers.append(_('Attendee name') + ': ' + str(label))
|
headers.append(_('Attendee name') + ': ' + str(label))
|
||||||
headers += [
|
headers += [
|
||||||
_('Attendee email'),
|
_('Attendee email'),
|
||||||
|
_('Company'),
|
||||||
|
_('Address'),
|
||||||
|
_('ZIP code'),
|
||||||
|
_('City'),
|
||||||
|
_('Country'),
|
||||||
|
pgettext('address', 'State'),
|
||||||
_('Voucher'),
|
_('Voucher'),
|
||||||
_('Pseudonymization ID'),
|
_('Pseudonymization ID'),
|
||||||
]
|
]
|
||||||
@@ -364,6 +370,12 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
)
|
)
|
||||||
row += [
|
row += [
|
||||||
op.attendee_email,
|
op.attendee_email,
|
||||||
|
op.company or '',
|
||||||
|
op.street or '',
|
||||||
|
op.zipcode or '',
|
||||||
|
op.city or '',
|
||||||
|
op.country if op.country else '',
|
||||||
|
op.state or '',
|
||||||
op.voucher.code if op.voucher else '',
|
op.voucher.code if op.voucher else '',
|
||||||
op.pseudonymization_id,
|
op.pseudonymization_id,
|
||||||
]
|
]
|
||||||
@@ -414,7 +426,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
|
|
||||||
class PaymentListExporter(ListExporter):
|
class PaymentListExporter(ListExporter):
|
||||||
identifier = 'paymentlist'
|
identifier = 'paymentlist'
|
||||||
verbose_name = ugettext_lazy('Order payments and refunds')
|
verbose_name = gettext_lazy('Order payments and refunds')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def additional_form_fields(self):
|
def additional_form_fields(self):
|
||||||
@@ -485,7 +497,7 @@ class PaymentListExporter(ListExporter):
|
|||||||
|
|
||||||
class QuotaListExporter(ListExporter):
|
class QuotaListExporter(ListExporter):
|
||||||
identifier = 'quotalist'
|
identifier = 'quotalist'
|
||||||
verbose_name = ugettext_lazy('Quota availabilities')
|
verbose_name = gettext_lazy('Quota availabilities')
|
||||||
|
|
||||||
def iterate_list(self, form_data):
|
def iterate_list(self, form_data):
|
||||||
headers = [
|
headers = [
|
||||||
@@ -514,7 +526,7 @@ class QuotaListExporter(ListExporter):
|
|||||||
|
|
||||||
class InvoiceDataExporter(MultiSheetListExporter):
|
class InvoiceDataExporter(MultiSheetListExporter):
|
||||||
identifier = 'invoicedata'
|
identifier = 'invoicedata'
|
||||||
verbose_name = ugettext_lazy('Invoice data')
|
verbose_name = gettext_lazy('Invoice data')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sheets(self):
|
def sheets(self):
|
||||||
@@ -698,6 +710,45 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
|||||||
return '{}_invoices'.format(self.event.slug)
|
return '{}_invoices'.format(self.event.slug)
|
||||||
|
|
||||||
|
|
||||||
|
class GiftcardRedemptionListExporter(ListExporter):
|
||||||
|
identifier = 'giftcardredemptionlist'
|
||||||
|
verbose_name = gettext_lazy('Giftcard Redemptions')
|
||||||
|
|
||||||
|
def iterate_list(self, form_data):
|
||||||
|
tz = pytz.timezone(self.event.settings.timezone)
|
||||||
|
|
||||||
|
payments = OrderPayment.objects.filter(
|
||||||
|
order__event=self.event,
|
||||||
|
provider='giftcard'
|
||||||
|
).order_by('created')
|
||||||
|
refunds = OrderRefund.objects.filter(
|
||||||
|
order__event=self.event,
|
||||||
|
provider='giftcard'
|
||||||
|
).order_by('created')
|
||||||
|
|
||||||
|
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
_('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
|
||||||
|
]
|
||||||
|
yield headers
|
||||||
|
|
||||||
|
for obj in objs:
|
||||||
|
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
|
||||||
|
row = [
|
||||||
|
obj.order.code,
|
||||||
|
obj.full_id,
|
||||||
|
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
|
||||||
|
gc.secret,
|
||||||
|
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
|
||||||
|
gc.issuer
|
||||||
|
]
|
||||||
|
yield row
|
||||||
|
|
||||||
|
def get_filename(self):
|
||||||
|
return '{}_giftcardredemptions'.format(self.event.slug)
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
|
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
|
||||||
def register_orderlist_exporter(sender, **kwargs):
|
def register_orderlist_exporter(sender, **kwargs):
|
||||||
return OrderListExporter
|
return OrderListExporter
|
||||||
@@ -716,3 +767,8 @@ def register_quotalist_exporter(sender, **kwargs):
|
|||||||
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
|
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
|
||||||
def register_invoicedata_exporter(sender, **kwargs):
|
def register_invoicedata_exporter(sender, **kwargs):
|
||||||
return InvoiceDataExporter
|
return InvoiceDataExporter
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist")
|
||||||
|
def register_giftcardredemptionlist_exporter(sender, **kwargs):
|
||||||
|
return GiftcardRedemptionListExporter
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import logging
|
|||||||
import i18nfield.forms
|
import i18nfield.forms
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.models import ModelFormMetaclass
|
from django.forms.models import ModelFormMetaclass
|
||||||
from django.utils import six
|
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from formtools.wizard.views import SessionWizardView
|
from formtools.wizard.views import SessionWizardView
|
||||||
from hierarkey.forms import HierarkeyForm
|
from hierarkey.forms import HierarkeyForm
|
||||||
@@ -25,7 +24,7 @@ class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class I18nModelForm(six.with_metaclass(ModelFormMetaclass, BaseI18nModelForm)):
|
class I18nModelForm(BaseI18nModelForm, metaclass=ModelFormMetaclass):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.password_validation import (
|
from django.contrib.auth.password_validation import (
|
||||||
password_validators_help_texts, validate_password,
|
password_validators_help_texts, validate_password,
|
||||||
)
|
)
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
|
from pretix.helpers.dicts import move_to_end
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(forms.Form):
|
class LoginForm(forms.Form):
|
||||||
@@ -36,7 +37,7 @@ class LoginForm(forms.Form):
|
|||||||
if not settings.PRETIX_LONG_SESSIONS or backend.url:
|
if not settings.PRETIX_LONG_SESSIONS or backend.url:
|
||||||
del self.fields['keep_logged_in']
|
del self.fields['keep_logged_in']
|
||||||
else:
|
else:
|
||||||
self.fields.move_to_end('keep_logged_in')
|
move_to_end(self.fields, 'keep_logged_in')
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from django.forms import Select
|
|||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import (
|
from django.utils.translation import (
|
||||||
get_language, pgettext_lazy, ugettext_lazy as _,
|
get_language, gettext_lazy as _, pgettext_lazy,
|
||||||
)
|
)
|
||||||
from django_countries import countries
|
from django_countries import countries
|
||||||
from django_countries.fields import Country, CountryField
|
from django_countries.fields import Country, CountryField
|
||||||
@@ -41,6 +41,7 @@ from pretix.base.settings import (
|
|||||||
)
|
)
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
from pretix.control.forms import SplitDateTimeField
|
from pretix.control.forms import SplitDateTimeField
|
||||||
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.helpers.escapejson import escapejson_attr
|
from pretix.helpers.escapejson import escapejson_attr
|
||||||
from pretix.helpers.i18n import get_format_without_seconds
|
from pretix.helpers.i18n import get_format_without_seconds
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
@@ -214,6 +215,10 @@ def guess_country(event):
|
|||||||
return country
|
return country
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||||
|
option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html'
|
||||||
|
|
||||||
|
|
||||||
class BaseQuestionsForm(forms.Form):
|
class BaseQuestionsForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
This form class is responsible for asking order-related questions. This includes
|
This form class is responsible for asking order-related questions. This includes
|
||||||
@@ -241,7 +246,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
if item.admission and event.settings.attendee_names_asked:
|
if item.admission and event.settings.attendee_names_asked:
|
||||||
self.fields['attendee_name_parts'] = NamePartsFormField(
|
self.fields['attendee_name_parts'] = NamePartsFormField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
required=event.settings.attendee_names_required,
|
required=event.settings.attendee_names_required and not self.all_optional,
|
||||||
scheme=event.settings.name_scheme,
|
scheme=event.settings.name_scheme,
|
||||||
titles=event.settings.name_scheme_titles,
|
titles=event.settings.name_scheme_titles,
|
||||||
label=_('Attendee name'),
|
label=_('Attendee name'),
|
||||||
@@ -249,7 +254,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
)
|
)
|
||||||
if item.admission and event.settings.attendee_emails_asked:
|
if item.admission and event.settings.attendee_emails_asked:
|
||||||
self.fields['attendee_email'] = forms.EmailField(
|
self.fields['attendee_email'] = forms.EmailField(
|
||||||
required=event.settings.attendee_emails_required,
|
required=event.settings.attendee_emails_required and not self.all_optional,
|
||||||
label=_('Attendee email'),
|
label=_('Attendee email'),
|
||||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
||||||
widget=forms.EmailInput(
|
widget=forms.EmailInput(
|
||||||
@@ -258,6 +263,75 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if item.admission and event.settings.attendee_company_asked:
|
||||||
|
self.fields['company'] = forms.CharField(
|
||||||
|
required=event.settings.attendee_company_required and not self.all_optional,
|
||||||
|
label=_('Company'),
|
||||||
|
initial=(cartpos.company if cartpos else orderpos.company),
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.admission and event.settings.attendee_addresses_asked:
|
||||||
|
self.fields['street'] = forms.CharField(
|
||||||
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
|
label=_('Address'),
|
||||||
|
widget=forms.Textarea(attrs={
|
||||||
|
'rows': 2,
|
||||||
|
'placeholder': _('Street and Number'),
|
||||||
|
'autocomplete': 'street-address'
|
||||||
|
}),
|
||||||
|
initial=(cartpos.street if cartpos else orderpos.street),
|
||||||
|
)
|
||||||
|
self.fields['zipcode'] = forms.CharField(
|
||||||
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
|
label=_('ZIP code'),
|
||||||
|
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
||||||
|
widget=forms.TextInput(attrs={
|
||||||
|
'autocomplete': 'postal-code',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
self.fields['city'] = forms.CharField(
|
||||||
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
|
label=_('City'),
|
||||||
|
initial=(cartpos.city if cartpos else orderpos.city),
|
||||||
|
widget=forms.TextInput(attrs={
|
||||||
|
'autocomplete': 'address-level2',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
|
||||||
|
self.fields['country'] = CountryField(
|
||||||
|
countries=CachedCountries
|
||||||
|
).formfield(
|
||||||
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
|
label=_('Country'),
|
||||||
|
initial=country,
|
||||||
|
widget=forms.Select(attrs={
|
||||||
|
'autocomplete': 'country',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
|
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
|
||||||
|
cc = None
|
||||||
|
if fprefix + 'country' in self.data:
|
||||||
|
cc = str(self.data[fprefix + 'country'])
|
||||||
|
elif country:
|
||||||
|
cc = str(country)
|
||||||
|
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
|
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||||
|
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||||
|
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
|
||||||
|
elif fprefix + 'state' in self.data:
|
||||||
|
self.data = self.data.copy()
|
||||||
|
del self.data[fprefix + 'state']
|
||||||
|
|
||||||
|
self.fields['state'] = forms.ChoiceField(
|
||||||
|
label=pgettext_lazy('address', 'State'),
|
||||||
|
required=False,
|
||||||
|
choices=c,
|
||||||
|
widget=forms.Select(attrs={
|
||||||
|
'autocomplete': 'address-level1',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
self.fields['state'].widget.is_required = True
|
||||||
|
|
||||||
for q in questions:
|
for q in questions:
|
||||||
# Do we already have an answer? Provide it as the initial value
|
# Do we already have an answer? Provide it as the initial value
|
||||||
@@ -309,12 +383,14 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
initial=initial.answer if initial else None,
|
initial=initial.answer if initial else None,
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_COUNTRYCODE:
|
elif q.type == Question.TYPE_COUNTRYCODE:
|
||||||
field = CountryField().formfield(
|
field = CountryField(
|
||||||
|
countries=CachedCountries
|
||||||
|
).formfield(
|
||||||
label=label, required=required,
|
label=label, required=required,
|
||||||
help_text=help_text,
|
help_text=help_text,
|
||||||
widget=forms.Select,
|
widget=forms.Select,
|
||||||
empty_label='',
|
empty_label='',
|
||||||
initial=initial.answer if initial else None,
|
initial=initial.answer if initial else guess_country(event),
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_CHOICE:
|
elif q.type == Question.TYPE_CHOICE:
|
||||||
field = forms.ModelChoiceField(
|
field = forms.ModelChoiceField(
|
||||||
@@ -332,7 +408,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
label=label, required=required,
|
label=label, required=required,
|
||||||
help_text=help_text,
|
help_text=help_text,
|
||||||
to_field_name='identifier',
|
to_field_name='identifier',
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=QuestionCheckboxSelectMultiple,
|
||||||
initial=initial.options.all() if initial else None,
|
initial=initial.options.all() if initial else None,
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_FILE:
|
elif q.type == Question.TYPE_FILE:
|
||||||
@@ -419,6 +495,10 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
|
|
||||||
|
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
|
if not d.get('state'):
|
||||||
|
self.add_error('state', _('This field is required.'))
|
||||||
|
|
||||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||||
|
|
||||||
def question_is_visible(parentid, qvals):
|
def question_is_visible(parentid, qvals):
|
||||||
@@ -457,7 +537,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceAddress
|
model = InvoiceAddress
|
||||||
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
|
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'vat_id', 'internal_reference', 'beneficiary')
|
'vat_id', 'internal_reference', 'beneficiary', 'custom_field')
|
||||||
widgets = {
|
widgets = {
|
||||||
'is_business': BusinessBooleanRadio,
|
'is_business': BusinessBooleanRadio,
|
||||||
'street': forms.Textarea(attrs={
|
'street': forms.Textarea(attrs={
|
||||||
@@ -500,6 +580,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
if not event.settings.invoice_address_vatid:
|
if not event.settings.invoice_address_vatid:
|
||||||
del self.fields['vat_id']
|
del self.fields['vat_id']
|
||||||
|
|
||||||
|
self.fields['country'].choices = CachedCountries()
|
||||||
|
|
||||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
fprefix = self.prefix + '-' if self.prefix else ''
|
fprefix = self.prefix + '-' if self.prefix else ''
|
||||||
cc = None
|
cc = None
|
||||||
@@ -561,6 +643,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
if not event.settings.invoice_address_beneficiary:
|
if not event.settings.invoice_address_beneficiary:
|
||||||
del self.fields['beneficiary']
|
del self.fields['beneficiary']
|
||||||
|
|
||||||
|
if event.settings.invoice_address_custom_field:
|
||||||
|
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
||||||
|
else:
|
||||||
|
del self.fields['custom_field']
|
||||||
|
|
||||||
for k, v in self.fields.items():
|
for k, v in self.fields.items():
|
||||||
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
|
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
|
||||||
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
|
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django.contrib.auth.password_validation import (
|
|||||||
password_validators_help_texts, validate_password,
|
password_validators_help_texts, validate_password,
|
||||||
)
|
)
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import re
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import BaseValidator
|
from django.core.validators import BaseValidator
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django import forms
|
|||||||
from django.utils.formats import get_format
|
from django.utils.formats import get_format
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class DatePickerWidget(forms.DateInput):
|
class DatePickerWidget(forms.DateInput):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from contextlib import contextmanager
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.formats import date_format, number_format
|
from django.utils.formats import date_format, number_format
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import gettext
|
||||||
from i18nfield.fields import ( # noqa
|
from i18nfield.fields import ( # noqa
|
||||||
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
|
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
|
||||||
)
|
)
|
||||||
@@ -69,6 +69,6 @@ class LazyLocaleException(Exception):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.msgargs:
|
if self.msgargs:
|
||||||
return ugettext(self.msg) % self.msgargs
|
return gettext(self.msg) % self.msgargs
|
||||||
else:
|
else:
|
||||||
return ugettext(self.msg)
|
return gettext(self.msg)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from django.contrib.staticfiles import finders
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.formats import date_format, localize
|
from django.utils.formats import date_format, localize
|
||||||
from django.utils.translation import (
|
from django.utils.translation import (
|
||||||
get_language, pgettext, ugettext, ugettext_lazy,
|
get_language, gettext, gettext_lazy, pgettext,
|
||||||
)
|
)
|
||||||
from PIL.Image import BICUBIC
|
from PIL.Image import BICUBIC
|
||||||
from reportlab.lib import pagesizes
|
from reportlab.lib import pagesizes
|
||||||
@@ -264,7 +264,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
invoice_to_top = 52 * mm
|
invoice_to_top = 52 * mm
|
||||||
|
|
||||||
def _draw_invoice_to(self, canvas):
|
def _draw_invoice_to(self, canvas):
|
||||||
p = Paragraph(self.invoice.address_invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
p = Paragraph(bleach.clean(self.invoice.address_invoice_to, tags=[]).strip().replace('\n', '<br />\n'),
|
||||||
|
style=self.stylesheet['Normal'])
|
||||||
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||||
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
||||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
||||||
@@ -422,7 +423,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
canvas.saveState()
|
canvas.saveState()
|
||||||
canvas.setFont('OpenSansBd', 30)
|
canvas.setFont('OpenSansBd', 30)
|
||||||
canvas.setFillColorRGB(32, 0, 0)
|
canvas.setFillColorRGB(32, 0, 0)
|
||||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
|
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
|
||||||
canvas.restoreState()
|
canvas.restoreState()
|
||||||
|
|
||||||
def _on_first_page(self, canvas: Canvas, doc):
|
def _on_first_page(self, canvas: Canvas, doc):
|
||||||
@@ -459,6 +460,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
def _get_intro(self):
|
def _get_intro(self):
|
||||||
story = []
|
story = []
|
||||||
|
if self.invoice.custom_field:
|
||||||
|
story.append(Paragraph(
|
||||||
|
'{}: {}'.format(self.invoice.event.settings.invoice_address_custom_field, self.invoice.custom_field),
|
||||||
|
self.stylesheet['Normal']
|
||||||
|
))
|
||||||
|
|
||||||
if self.invoice.internal_reference:
|
if self.invoice.internal_reference:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
|
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
|
||||||
@@ -681,7 +688,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
class Modern1Renderer(ClassicInvoiceRenderer):
|
class Modern1Renderer(ClassicInvoiceRenderer):
|
||||||
identifier = 'modern1'
|
identifier = 'modern1'
|
||||||
verbose_name = ugettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
||||||
bottom_margin = 16.9 * mm
|
bottom_margin = 16.9 * mm
|
||||||
top_margin = 16.9 * mm
|
top_margin = 16.9 * mm
|
||||||
right_margin = 20 * mm
|
right_margin = 20 * mm
|
||||||
|
|||||||
@@ -13,11 +13,17 @@ class Command(BaseCommand):
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
from django_extensions.management.commands import shell_plus # noqa
|
||||||
|
cmd = 'shell_plus'
|
||||||
|
except ImportError:
|
||||||
|
cmd = 'shell'
|
||||||
|
|
||||||
parser = self.create_parser(sys.argv[0], sys.argv[1])
|
parser = self.create_parser(sys.argv[0], sys.argv[1])
|
||||||
flags = parser.parse_known_args(sys.argv[2:])[1]
|
flags = parser.parse_known_args(sys.argv[2:])[1]
|
||||||
if "--override" in flags:
|
if "--override" in flags:
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
return call_command("shell_plus", *args, **options)
|
return call_command(cmd, *args, **options)
|
||||||
|
|
||||||
lookups = {}
|
lookups = {}
|
||||||
for flag in flags:
|
for flag in flags:
|
||||||
@@ -36,4 +42,4 @@ class Command(BaseCommand):
|
|||||||
for app_name, app_value in lookups.items()
|
for app_name, app_value in lookups.items()
|
||||||
}
|
}
|
||||||
with scope(**scope_options):
|
with scope(**scope_options):
|
||||||
return call_command("shell_plus", *args, **options)
|
return call_command(cmd, *args, **options)
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ from django.utils.translation.trans_real import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
from pretix.multidomain.urlreverse import get_domain
|
from pretix.multidomain.urlreverse import (
|
||||||
|
get_event_domain, get_organizer_domain,
|
||||||
|
)
|
||||||
|
|
||||||
_supported = None
|
_supported = None
|
||||||
|
|
||||||
@@ -231,7 +233,10 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
dynamicdomain += " " + settings.SITE_URL
|
dynamicdomain += " " + settings.SITE_URL
|
||||||
|
|
||||||
if hasattr(request, 'organizer') and request.organizer:
|
if hasattr(request, 'organizer') and request.organizer:
|
||||||
domain = get_domain(request.organizer)
|
if hasattr(request, 'event') and request.event:
|
||||||
|
domain = get_event_domain(request.event, fallback=True)
|
||||||
|
else:
|
||||||
|
domain = get_organizer_domain(request.organizer)
|
||||||
if domain:
|
if domain:
|
||||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import django.core.validators
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
import pretix.base.validators
|
import pretix.base.validators
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.db import migrations, models
|
|||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
import pretix.base.models.auth
|
import pretix.base.models.auth
|
||||||
import pretix.base.validators
|
import pretix.base.validators
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 2.2.9 on 2020-02-18 08:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0143_auto_20200217_1211'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoiceaddress',
|
||||||
|
name='custom_field',
|
||||||
|
field=models.CharField(max_length=255, null=True, blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='custom_field',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
42
src/pretix/base/migrations/0145_auto_20200210_1038.py
Normal file
42
src/pretix/base/migrations/0145_auto_20200210_1038.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 2.2.8 on 2020-02-10 10:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.base
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0144_invoiceaddress_custom_field'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ItemMetaProperty',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(db_index=True, max_length=50)),
|
||||||
|
('default', models.TextField()),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_meta_properties', to='pretixbase.Event')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ItemMetaValue',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('value', models.TextField()),
|
||||||
|
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.Item')),
|
||||||
|
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_values', to='pretixbase.ItemMetaProperty')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('item', 'property')},
|
||||||
|
},
|
||||||
|
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
src/pretix/base/migrations/0146_giftcardtransaction_text.py
Normal file
18
src/pretix/base/migrations/0146_giftcardtransaction_text.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2020-03-02 11:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0145_auto_20200210_1038'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='giftcardtransaction',
|
||||||
|
name='text',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
src/pretix/base/migrations/0147_user_session_token.py
Normal file
20
src/pretix/base/migrations/0147_user_session_token.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 2.2.9 on 2020-03-21 15:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.auth
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0146_giftcardtransaction_text'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='session_token',
|
||||||
|
field=models.CharField(default=pretix.base.models.auth.generate_session_token, max_length=32),
|
||||||
|
),
|
||||||
|
]
|
||||||
24
src/pretix/base/migrations/0148_cancellationrequest.py
Normal file
24
src/pretix/base/migrations/0148_cancellationrequest.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.0.4 on 2020-03-25 10:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0147_user_session_token'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CancellationRequest',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('cancellation_fee', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('refund_as_giftcard', models.BooleanField(default=False)),
|
||||||
|
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cancellation_requests', to='pretixbase.Order')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
46
src/pretix/base/migrations/0149_order_cancellation_date.py
Normal file
46
src/pretix/base/migrations/0149_order_cancellation_date.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 3.0.4 on 2020-03-25 14:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.models import Count, OuterRef, Q, Subquery
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
|
||||||
|
def fill_cancellation_date(apps, schema_editor):
|
||||||
|
Order = apps.get_model('pretixbase', 'Order')
|
||||||
|
LogEntry = apps.get_model('pretixbase', 'LogEntry')
|
||||||
|
OrderPosition = apps.get_model('pretixbase', 'OrderPosition')
|
||||||
|
|
||||||
|
s = OrderPosition.all.filter(
|
||||||
|
order=OuterRef('pk'),
|
||||||
|
canceled=False,
|
||||||
|
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||||
|
for o in Order.objects.annotate(
|
||||||
|
pcnt=Subquery(s)
|
||||||
|
).filter(
|
||||||
|
Q(pcnt=0) | Q(pcnt__isnull=True) | Q(status="c")
|
||||||
|
).values('id').iterator():
|
||||||
|
le = LogEntry.objects.filter(
|
||||||
|
content_type__model="order",
|
||||||
|
object_id=o['id'],
|
||||||
|
action_type='pretix.event.order.canceled'
|
||||||
|
).order_by('-datetime').only('datetime').first()
|
||||||
|
if le:
|
||||||
|
Order.objects.filter(pk=o['id']).update(
|
||||||
|
cancellation_date=le.datetime,
|
||||||
|
last_modified=now()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0148_cancellationrequest'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='cancellation_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(fill_cancellation_date, migrations.RunPython.noop)
|
||||||
|
]
|
||||||
74
src/pretix/base/migrations/0150_auto_20200401_1123.py
Normal file
74
src/pretix/base/migrations/0150_auto_20200401_1123.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Generated by Django 3.0.4 on 2020-04-01 11:24
|
||||||
|
|
||||||
|
import django_countries.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0149_order_cancellation_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='city',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='company',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='country',
|
||||||
|
field=django_countries.fields.CountryField(max_length=2, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='street',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='zipcode',
|
||||||
|
field=models.CharField(max_length=30, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='city',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='company',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='country',
|
||||||
|
field=django_countries.fields.CountryField(max_length=2, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='street',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='zipcode',
|
||||||
|
field=models.CharField(max_length=30, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -10,9 +10,9 @@ from .event import (
|
|||||||
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
|
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
|
||||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||||
from .items import (
|
from .items import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
|
||||||
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
|
ItemVariation, Question, QuestionOption, Quota, SubEventItem,
|
||||||
itempicture_upload_to,
|
SubEventItemVariation, itempicture_upload_to,
|
||||||
)
|
)
|
||||||
from .log import LogEntry
|
from .log import LogEntry
|
||||||
from .notifications import NotificationSetting
|
from .notifications import NotificationSetting
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ from django.contrib.auth.tokens import default_token_generator
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
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, salted_hmac
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_otp.models import Device
|
from django_otp.models import Device
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from u2flib_server.utils import (
|
from u2flib_server.utils import (
|
||||||
@@ -54,6 +54,10 @@ def generate_notifications_token():
|
|||||||
return get_random_string(length=32)
|
return get_random_string(length=32)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_session_token():
|
||||||
|
return get_random_string(length=32)
|
||||||
|
|
||||||
|
|
||||||
class SuperuserPermissionSet:
|
class SuperuserPermissionSet:
|
||||||
def __contains__(self, item):
|
def __contains__(self, item):
|
||||||
return True
|
return True
|
||||||
@@ -110,6 +114,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
)
|
)
|
||||||
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
|
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
|
||||||
auth_backend = models.CharField(max_length=255, default='native')
|
auth_backend = models.CharField(max_length=255, default='native')
|
||||||
|
session_token = models.CharField(max_length=32, default=generate_session_token)
|
||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
@@ -382,6 +387,20 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
self._staff_session_cache[session_key] = sess
|
self._staff_session_cache[session_key] = sess
|
||||||
return self._staff_session_cache[session_key]
|
return self._staff_session_cache[session_key]
|
||||||
|
|
||||||
|
def get_session_auth_hash(self):
|
||||||
|
"""
|
||||||
|
Return an HMAC that needs to
|
||||||
|
"""
|
||||||
|
key_salt = "pretix.base.models.User.get_session_auth_hash"
|
||||||
|
payload = self.password
|
||||||
|
payload += self.email
|
||||||
|
payload += self.session_token
|
||||||
|
return salted_hmac(key_salt, payload).hexdigest()
|
||||||
|
|
||||||
|
def update_session_token(self):
|
||||||
|
self.session_token = generate_session_token()
|
||||||
|
self.save(update_fields=['session_token'])
|
||||||
|
|
||||||
|
|
||||||
class StaffSession(models.Model):
|
class StaffSession(models.Model):
|
||||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Exists, OuterRef
|
from django.db.models import Exists, OuterRef
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import string
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
@@ -94,6 +94,7 @@ class Device(LoggedModel):
|
|||||||
return {
|
return {
|
||||||
'can_view_orders',
|
'can_view_orders',
|
||||||
'can_change_orders',
|
'can_change_orders',
|
||||||
|
'can_manage_gift_cards'
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_event_permission_set(self, organizer, event) -> set:
|
def get_event_permission_set(self, organizer, event) -> set:
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ from django.db import models
|
|||||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||||
from django.template.defaultfilters import date as _date
|
from django.template.defaultfilters import date as _date
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
|
|
||||||
@@ -293,7 +294,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
|
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
|
||||||
validators=[
|
validators=[
|
||||||
RegexValidator(
|
RegexValidator(
|
||||||
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]+$",
|
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*$",
|
||||||
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
||||||
),
|
),
|
||||||
EventSlugBanlistValidator()
|
EventSlugBanlistValidator()
|
||||||
@@ -370,6 +371,8 @@ class Event(EventMixin, LoggedModel):
|
|||||||
"""
|
"""
|
||||||
self.settings.invoice_renderer = 'modern1'
|
self.settings.invoice_renderer = 'modern1'
|
||||||
self.settings.invoice_include_expire_date = True
|
self.settings.invoice_include_expire_date = True
|
||||||
|
self.settings.ticketoutput_pdf__enabled = True
|
||||||
|
self.settings.ticketoutput_passbook__enabled = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def social_image(self):
|
def social_image(self):
|
||||||
@@ -385,7 +388,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if img:
|
if img:
|
||||||
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
||||||
|
|
||||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||||
from .orders import CartPosition, Order, OrderPosition
|
from .orders import CartPosition, Order, OrderPosition
|
||||||
from .vouchers import Voucher
|
from .vouchers import Voucher
|
||||||
vqs = Voucher.objects.filter(
|
vqs = Voucher.objects.filter(
|
||||||
@@ -416,7 +419,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
vqs
|
vqs
|
||||||
)
|
)
|
||||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
|
||||||
qs = qs.filter(blocked=False)
|
qs = qs.filter(blocked=False)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -515,7 +518,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
), tz)
|
), tz)
|
||||||
|
|
||||||
def copy_data_from(self, other):
|
def copy_data_from(self, other):
|
||||||
from . import ItemAddOn, ItemCategory, Item, Question, Quota
|
from . import ItemAddOn, ItemCategory, Item, Question, Quota, ItemMetaValue
|
||||||
from ..signals import event_copy_data
|
from ..signals import event_copy_data
|
||||||
|
|
||||||
self.plugins = other.plugins
|
self.plugins = other.plugins
|
||||||
@@ -540,6 +543,14 @@ class Event(EventMixin, LoggedModel):
|
|||||||
c.save()
|
c.save()
|
||||||
c.log_action('pretix.object.cloned')
|
c.log_action('pretix.object.cloned')
|
||||||
|
|
||||||
|
item_meta_properties_map = {}
|
||||||
|
for imp in other.item_meta_properties.all():
|
||||||
|
item_meta_properties_map[imp.pk] = imp
|
||||||
|
imp.pk = None
|
||||||
|
imp.event = self
|
||||||
|
imp.save()
|
||||||
|
imp.log_action('pretix.object.cloned')
|
||||||
|
|
||||||
item_map = {}
|
item_map = {}
|
||||||
variation_map = {}
|
variation_map = {}
|
||||||
for i in Item.objects.filter(event=other).prefetch_related('variations'):
|
for i in Item.objects.filter(event=other).prefetch_related('variations'):
|
||||||
@@ -561,6 +572,12 @@ class Event(EventMixin, LoggedModel):
|
|||||||
v.item = i
|
v.item = i
|
||||||
v.save()
|
v.save()
|
||||||
|
|
||||||
|
for imv in ItemMetaValue.objects.filter(item__event=other).prefetch_related('item', 'property'):
|
||||||
|
imv.pk = None
|
||||||
|
imv.property = item_meta_properties_map[imv.property.pk]
|
||||||
|
imv.item = item_map[imv.item.pk]
|
||||||
|
imv.save()
|
||||||
|
|
||||||
for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'):
|
for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'):
|
||||||
ia.pk = None
|
ia.pk = None
|
||||||
ia.base_item = item_map[ia.base_item.pk]
|
ia.base_item = item_map[ia.base_item.pk]
|
||||||
@@ -608,8 +625,10 @@ class Event(EventMixin, LoggedModel):
|
|||||||
q.dependency_question = question_map[q.dependency_question_id]
|
q.dependency_question = question_map[q.dependency_question_id]
|
||||||
q.save(update_fields=['dependency_question'])
|
q.save(update_fields=['dependency_question'])
|
||||||
|
|
||||||
|
checkin_list_map = {}
|
||||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
||||||
items = list(cl.limit_products.all())
|
items = list(cl.limit_products.all())
|
||||||
|
checkin_list_map[cl.pk] = cl
|
||||||
cl.pk = None
|
cl.pk = None
|
||||||
cl.event = self
|
cl.event = self
|
||||||
cl.save()
|
cl.save()
|
||||||
@@ -664,7 +683,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
event_copy_data.send(
|
event_copy_data.send(
|
||||||
sender=self, other=other,
|
sender=self, other=other,
|
||||||
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
|
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
|
||||||
question_map=question_map
|
question_map=question_map, checkin_list_map=checkin_list_map
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_payment_providers(self, cached=False) -> dict:
|
def get_payment_providers(self, cached=False) -> dict:
|
||||||
@@ -1016,9 +1035,13 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
ordering = ("date_from", "name")
|
ordering = ("date_from", "name")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} - {}'.format(self.name, self.get_date_range_display())
|
return '{} - {} {}'.format(
|
||||||
|
self.name,
|
||||||
|
self.get_date_range_display(),
|
||||||
|
date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") if self.settings.show_times else ""
|
||||||
|
).strip()
|
||||||
|
|
||||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||||
from .orders import CartPosition, Order, OrderPosition
|
from .orders import CartPosition, Order, OrderPosition
|
||||||
from .vouchers import Voucher
|
from .vouchers import Voucher
|
||||||
vqs = Voucher.objects.filter(
|
vqs = Voucher.objects.filter(
|
||||||
@@ -1052,7 +1075,7 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
vqs
|
vqs
|
||||||
)
|
)
|
||||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
|
||||||
qs = qs.filter(blocked=False)
|
qs = qs.filter(blocked=False)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.core.validators import RegexValidator
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.banlist import banned
|
from pretix.base.banlist import banned
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
@@ -83,6 +83,7 @@ class GiftCard(LoggedModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (('secret', 'issuer'),)
|
unique_together = (('secret', 'issuer'),)
|
||||||
|
ordering = ("issuance",)
|
||||||
|
|
||||||
|
|
||||||
class GiftCardTransaction(models.Model):
|
class GiftCardTransaction(models.Model):
|
||||||
@@ -119,6 +120,7 @@ class GiftCardTransaction(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
|
text = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("datetime",)
|
ordering = ("datetime",)
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class Invoice(models.Model):
|
|||||||
|
|
||||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
||||||
internal_reference = models.TextField(blank=True)
|
internal_reference = models.TextField(blank=True)
|
||||||
|
custom_field = models.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
@@ -120,13 +121,19 @@ class Invoice(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def full_invoice_from(self):
|
def full_invoice_from(self):
|
||||||
|
taxidrow = ""
|
||||||
|
if self.invoice_from_tax_id:
|
||||||
|
if str(self.invoice_from_country) == "AU":
|
||||||
|
taxidrow = "ABN: %s" % self.invoice_from_tax_id
|
||||||
|
else:
|
||||||
|
taxidrow = pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id
|
||||||
parts = [
|
parts = [
|
||||||
self.invoice_from_name,
|
self.invoice_from_name,
|
||||||
self.invoice_from,
|
self.invoice_from,
|
||||||
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
|
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
|
||||||
self.invoice_from_country.name if self.invoice_from_country else "",
|
self.invoice_from_country.name if self.invoice_from_country else "",
|
||||||
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
|
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
|
||||||
pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id if self.invoice_from_tax_id else "",
|
taxidrow,
|
||||||
]
|
]
|
||||||
return '\n'.join([p.strip() for p in parts if p and p.strip()])
|
return '\n'.join([p.strip() for p in parts if p and p.strip()])
|
||||||
|
|
||||||
@@ -150,9 +157,12 @@ class Invoice(models.Model):
|
|||||||
state_name = self.invoice_to_state
|
state_name = self.invoice_to_state
|
||||||
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
|
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
|
||||||
state_name = pycountry.subdivisions.get(
|
try:
|
||||||
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
|
state_name = pycountry.subdivisions.get(
|
||||||
).name
|
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
|
||||||
|
).name
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
parts = [
|
parts = [
|
||||||
self.invoice_to_company,
|
self.invoice_to_company,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
from collections import Counter
|
from collections import Counter, OrderedDict
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
from decimal import Decimal, DecimalException
|
from decimal import Decimal, DecimalException
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
@@ -9,13 +9,14 @@ import dateutil.parser
|
|||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Func, Q, Sum
|
from django.db.models import F, Func, Q, Sum
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import is_naive, make_aware, now
|
from django.utils.timezone import is_naive, make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
@@ -454,7 +455,8 @@ class Item(LoggedModel):
|
|||||||
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||||
rate=Decimal('0.00'), name='')
|
rate=Decimal('0.00'), name='')
|
||||||
else:
|
else:
|
||||||
t = self.tax_rule.tax(price, base_price_is=base_price_is, currency=currency)
|
t = self.tax_rule.tax(price, base_price_is=base_price_is,
|
||||||
|
currency=currency or self.event.currency)
|
||||||
|
|
||||||
if include_bundled:
|
if include_bundled:
|
||||||
for b in self.bundles.all():
|
for b in self.bundles.all():
|
||||||
@@ -591,6 +593,16 @@ class Item(LoggedModel):
|
|||||||
if from_date > until_date:
|
if from_date > until_date:
|
||||||
raise ValidationError(_('The item\'s availability cannot end before it starts.'))
|
raise ValidationError(_('The item\'s availability cannot end before it starts.'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def meta_data(self):
|
||||||
|
data = {p.name: p.default for p in self.event.item_meta_properties.all()}
|
||||||
|
if hasattr(self, 'meta_values_cached'):
|
||||||
|
data.update({v.property.name: v.value for v in self.meta_values_cached})
|
||||||
|
else:
|
||||||
|
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
|
||||||
|
|
||||||
|
return OrderedDict((k, v) for k, v in sorted(data.items(), key=lambda k: k[0]))
|
||||||
|
|
||||||
|
|
||||||
class ItemVariation(models.Model):
|
class ItemVariation(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -1105,10 +1117,13 @@ class Question(LoggedModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if self.type == Question.TYPE_CHOICE:
|
if self.type == Question.TYPE_CHOICE:
|
||||||
try:
|
q = Q(identifier=answer)
|
||||||
return self.options.get(Q(pk=answer) | Q(identifier=answer))
|
if isinstance(answer, int) or answer.isdigit():
|
||||||
except:
|
q |= Q(pk=answer)
|
||||||
|
o = self.options.filter(q).first()
|
||||||
|
if not o:
|
||||||
raise ValidationError(_('Invalid option selected.'))
|
raise ValidationError(_('Invalid option selected.'))
|
||||||
|
return o
|
||||||
elif self.type == Question.TYPE_CHOICE_MULTIPLE:
|
elif self.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||||
if isinstance(answer, str):
|
if isinstance(answer, str):
|
||||||
l_ = list(self.options.filter(
|
l_ = list(self.options.filter(
|
||||||
@@ -1541,3 +1556,57 @@ class Quota(LoggedModel):
|
|||||||
else:
|
else:
|
||||||
if subevent:
|
if subevent:
|
||||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||||
|
|
||||||
|
|
||||||
|
class ItemMetaProperty(LoggedModel):
|
||||||
|
"""
|
||||||
|
An event can have ItemMetaProperty objects attached to define meta information fields
|
||||||
|
for its items. This information can be re-used for example in ticket layouts.
|
||||||
|
|
||||||
|
:param event: The event this property is defined for.
|
||||||
|
:type event: Event
|
||||||
|
:param name: Name
|
||||||
|
:type name: Name of the property, used in various places
|
||||||
|
:param default: Default value
|
||||||
|
:type default: str
|
||||||
|
"""
|
||||||
|
event = models.ForeignKey(Event, related_name="item_meta_properties", on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50, db_index=True,
|
||||||
|
help_text=_(
|
||||||
|
"Can not contain spaces or special characters except underscores"
|
||||||
|
),
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
regex="^[a-zA-Z0-9_]+$",
|
||||||
|
message=_("The property name may only contain letters, numbers and underscores."),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verbose_name=_("Name"),
|
||||||
|
)
|
||||||
|
default = models.TextField(blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemMetaValue(LoggedModel):
|
||||||
|
"""
|
||||||
|
A meta-data value assigned to an item.
|
||||||
|
|
||||||
|
:param item: The item this metadata is valid for
|
||||||
|
:type item: Item
|
||||||
|
:param property: The property this value belongs to
|
||||||
|
:type property: ItemMetaProperty
|
||||||
|
:param value: The actual value
|
||||||
|
:type value: str
|
||||||
|
"""
|
||||||
|
item = models.ForeignKey('Item', on_delete=models.CASCADE, related_name='meta_values')
|
||||||
|
property = models.ForeignKey('ItemMetaProperty', on_delete=models.CASCADE, related_name='item_values')
|
||||||
|
value = models.TextField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('item', 'property')
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
from pretix.base.signals import logentry_object_link
|
from pretix.base.signals import logentry_object_link
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotificationSetting(models.Model):
|
class NotificationSetting(models.Model):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
|
from collections import Counter
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
@@ -25,7 +26,7 @@ from django.utils.encoding import escape_uri_path
|
|||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries.fields import Country, CountryField
|
from django_countries.fields import Country, CountryField
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
@@ -43,6 +44,7 @@ from pretix.base.services.locking import NoLockManager
|
|||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import order_gracefully_delete
|
from pretix.base.signals import order_gracefully_delete
|
||||||
|
|
||||||
|
from ...helpers.countries import CachedCountries
|
||||||
from .base import LockModel, LoggedModel
|
from .base import LockModel, LoggedModel
|
||||||
from .event import Event, SubEvent
|
from .event import Event, SubEvent
|
||||||
from .items import Item, ItemVariation, Question, QuestionOption, Quota
|
from .items import Item, ItemVariation, Question, QuestionOption, Quota
|
||||||
@@ -151,6 +153,9 @@ class Order(LockModel, LoggedModel):
|
|||||||
datetime = models.DateTimeField(
|
datetime = models.DateTimeField(
|
||||||
verbose_name=_("Date"), db_index=True
|
verbose_name=_("Date"), db_index=True
|
||||||
)
|
)
|
||||||
|
cancellation_date = models.DateTimeField(
|
||||||
|
null=True, blank=True
|
||||||
|
)
|
||||||
expires = models.DateTimeField(
|
expires = models.DateTimeField(
|
||||||
verbose_name=_("Expiration date")
|
verbose_name=_("Expiration date")
|
||||||
)
|
)
|
||||||
@@ -426,7 +431,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
def cancel_allowed(self):
|
def cancel_allowed(self):
|
||||||
return (
|
return (
|
||||||
self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and self.count_positions
|
self.status in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED) and self.count_positions
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@@ -448,16 +453,17 @@ class Order(LockModel, LoggedModel):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def user_cancel_fee(self):
|
def user_cancel_fee(self):
|
||||||
fee = Decimal('0.00')
|
fee = Decimal('0.00')
|
||||||
if self.event.settings.cancel_allow_user_paid_keep:
|
|
||||||
fee += self.event.settings.cancel_allow_user_paid_keep
|
|
||||||
if self.event.settings.cancel_allow_user_paid_keep_percentage:
|
|
||||||
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * self.total
|
|
||||||
if self.event.settings.cancel_allow_user_paid_keep_fees:
|
if self.event.settings.cancel_allow_user_paid_keep_fees:
|
||||||
fee += self.fees.filter(
|
fee += self.fees.filter(
|
||||||
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE)
|
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
|
||||||
|
OrderFee.FEE_TYPE_CANCELLATION)
|
||||||
).aggregate(
|
).aggregate(
|
||||||
s=Sum('value')
|
s=Sum('value')
|
||||||
)['s'] or 0
|
)['s'] or 0
|
||||||
|
if self.event.settings.cancel_allow_user_paid_keep_percentage:
|
||||||
|
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * (self.total - fee)
|
||||||
|
if self.event.settings.cancel_allow_user_paid_keep:
|
||||||
|
fee += self.event.settings.cancel_allow_user_paid_keep
|
||||||
return round_decimal(fee, self.event.currency)
|
return round_decimal(fee, self.event.currency)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -468,6 +474,8 @@ class Order(LockModel, LoggedModel):
|
|||||||
"""
|
"""
|
||||||
from .checkin import Checkin
|
from .checkin import Checkin
|
||||||
|
|
||||||
|
if self.cancellation_requests.exists():
|
||||||
|
return False
|
||||||
positions = list(
|
positions = list(
|
||||||
self.positions.all().annotate(
|
self.positions.all().annotate(
|
||||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
||||||
@@ -693,16 +701,19 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
|
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
|
||||||
|
|
||||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False) -> Union[bool, str]:
|
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False,
|
||||||
|
check_voucher_usage=False) -> Union[bool, str]:
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
||||||
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
|
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
|
||||||
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
||||||
|
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
|
||||||
}
|
}
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or now()
|
||||||
positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
|
positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
|
||||||
quota_cache = {}
|
quota_cache = {}
|
||||||
v_budget = {}
|
v_budget = {}
|
||||||
|
v_usage = Counter()
|
||||||
try:
|
try:
|
||||||
for i, op in enumerate(positions):
|
for i, op in enumerate(positions):
|
||||||
if op.seat:
|
if op.seat:
|
||||||
@@ -721,6 +732,13 @@ class Order(LockModel, LoggedModel):
|
|||||||
))
|
))
|
||||||
v_budget[op.voucher] -= disc
|
v_budget[op.voucher] -= disc
|
||||||
|
|
||||||
|
if op.voucher and check_voucher_usage:
|
||||||
|
v_usage[op.voucher.pk] += 1
|
||||||
|
if v_usage[op.voucher.pk] + op.voucher.redeemed > op.voucher.max_usages:
|
||||||
|
raise Quota.QuotaExceededException(error_messages['voucher_usages'].format(
|
||||||
|
voucher=op.voucher.code
|
||||||
|
))
|
||||||
|
|
||||||
quotas = list(op.quotas)
|
quotas = list(op.quotas)
|
||||||
if len(quotas) == 0:
|
if len(quotas) == 0:
|
||||||
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
||||||
@@ -1048,6 +1066,13 @@ class AbstractPosition(models.Model):
|
|||||||
'Seat', null=True, blank=True, on_delete=models.PROTECT
|
'Seat', null=True, blank=True, on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
|
||||||
|
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
|
||||||
|
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
|
||||||
|
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
|
||||||
|
country = CountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
|
||||||
|
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@@ -2089,11 +2114,13 @@ class InvoiceAddress(models.Model):
|
|||||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
||||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
||||||
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
||||||
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
|
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
|
||||||
|
countries=CachedCountries)
|
||||||
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
|
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
|
||||||
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
|
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
|
||||||
help_text=_('Only for business customers within the EU.'))
|
help_text=_('Only for business customers within the EU.'))
|
||||||
vat_id_validated = models.BooleanField(default=False)
|
vat_id_validated = models.BooleanField(default=False)
|
||||||
|
custom_field = models.CharField(max_length=255, null=True, blank=True)
|
||||||
internal_reference = models.TextField(
|
internal_reference = models.TextField(
|
||||||
verbose_name=_('Internal reference'),
|
verbose_name=_('Internal reference'),
|
||||||
help_text=_('This reference will be printed on your invoice for your convenience.'),
|
help_text=_('This reference will be printed on your invoice for your convenience.'),
|
||||||
@@ -2195,6 +2222,13 @@ class CachedCombinedTicket(models.Model):
|
|||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CancellationRequest(models.Model):
|
||||||
|
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='cancellation_requests')
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
cancellation_fee = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
refund_as_giftcard = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=CachedTicket)
|
@receiver(post_delete, sender=CachedTicket)
|
||||||
def cachedticket_delete(sender, instance, **kwargs):
|
def cachedticket_delete(sender, instance, **kwargs):
|
||||||
if instance.file:
|
if instance.file:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.db import models
|
|||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.validators import OrganizerSlugBanlistValidator
|
from pretix.base.validators import OrganizerSlugBanlistValidator
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.db import models
|
|||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext, ugettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent
|
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from decimal import Decimal
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
from i18nfield.fields import I18nCharField
|
from i18nfield.fields import I18nCharField
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.db.models import F, OuterRef, Q, Subquery, Sum
|
|||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
|
|
||||||
from pretix.base.banlist import banned
|
from pretix.base.banlist import banned
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from pretix.base.email import get_email_context
|
from pretix.base.email import get_email_context
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from collections import OrderedDict, namedtuple
|
|||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
from pretix.base.models import Event, LogEntry
|
from pretix.base.models import Event, LogEntry
|
||||||
from pretix.base.signals import register_notification_types
|
from pretix.base.signals import register_notification_types
|
||||||
@@ -223,6 +223,12 @@ def register_default_notification_types(sender, **kwargs):
|
|||||||
_('Order canceled'),
|
_('Order canceled'),
|
||||||
_('Order {order.code} has been canceled.')
|
_('Order {order.code} has been canceled.')
|
||||||
),
|
),
|
||||||
|
ParametrizedOrderNotificationType(
|
||||||
|
sender,
|
||||||
|
'pretix.event.order.reactivated',
|
||||||
|
_('Order reactivated'),
|
||||||
|
_('Order {order.code} has been reactivated.')
|
||||||
|
),
|
||||||
ParametrizedOrderNotificationType(
|
ParametrizedOrderNotificationType(
|
||||||
sender,
|
sender,
|
||||||
'pretix.event.order.expired',
|
'pretix.event.order.expired',
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ class InvoiceAddressState(ImportColumn):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def verbose_name(self):
|
def verbose_name(self):
|
||||||
return _('Invoice address') + ': ' + _('State')
|
return _('Invoice address') + ': ' + pgettext('address', 'State')
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if value:
|
if value:
|
||||||
@@ -398,6 +398,99 @@ class AttendeeEmail(ImportColumn):
|
|||||||
position.attendee_email = value
|
position.attendee_email = value
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeCompany(ImportColumn):
|
||||||
|
identifier = 'attendee_company'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('Company')
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.company = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeStreet(ImportColumn):
|
||||||
|
identifier = 'attendee_street'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('Address')
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.address = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeZip(ImportColumn):
|
||||||
|
identifier = 'attendee_zipcode'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('ZIP code')
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.zipcode = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeCity(ImportColumn):
|
||||||
|
identifier = 'attendee_city'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('City')
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.city = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeCountry(ImportColumn):
|
||||||
|
identifier = 'attendee_country'
|
||||||
|
default_value = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def initial(self):
|
||||||
|
return 'static:' + str(guess_country(self.event))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('Country')
|
||||||
|
|
||||||
|
def static_choices(self):
|
||||||
|
return list(countries)
|
||||||
|
|
||||||
|
def clean(self, value, previous_values):
|
||||||
|
if value and not Country(value).numeric:
|
||||||
|
raise ValidationError(_("Please enter a valid country code."))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.country = value or ''
|
||||||
|
|
||||||
|
|
||||||
|
class AttendeeState(ImportColumn):
|
||||||
|
identifier = 'attendee_state'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return _('Attendee address') + ': ' + _('State')
|
||||||
|
|
||||||
|
def clean(self, value, previous_values):
|
||||||
|
if value:
|
||||||
|
if previous_values.get('attendee_country') not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
|
raise ValidationError(_("States are not supported for this country."))
|
||||||
|
|
||||||
|
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[previous_values.get('attendee_country')]
|
||||||
|
match = [
|
||||||
|
s for s in pycountry.subdivisions.get(country_code=previous_values.get('attendee_country'))
|
||||||
|
if s.type in types and (s.code[3:] == value or s.name == value)
|
||||||
|
]
|
||||||
|
if len(match) == 0:
|
||||||
|
raise ValidationError(_("Please enter a valid state."))
|
||||||
|
return match[0].code[3:]
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
position.state = value or ''
|
||||||
|
|
||||||
|
|
||||||
class Price(ImportColumn):
|
class Price(ImportColumn):
|
||||||
identifier = 'price'
|
identifier = 'price'
|
||||||
verbose_name = gettext_lazy('Price')
|
verbose_name = gettext_lazy('Price')
|
||||||
@@ -596,6 +689,12 @@ def get_all_columns(event):
|
|||||||
default.append(AttendeeNamePart(event, n, l))
|
default.append(AttendeeNamePart(event, n, l))
|
||||||
default += [
|
default += [
|
||||||
AttendeeEmail(event),
|
AttendeeEmail(event),
|
||||||
|
AttendeeCompany(event),
|
||||||
|
AttendeeStreet(event),
|
||||||
|
AttendeeZip(event),
|
||||||
|
AttendeeCity(event),
|
||||||
|
AttendeeCountry(event),
|
||||||
|
AttendeeState(event),
|
||||||
Price(event),
|
Price(event),
|
||||||
Secret(event),
|
Secret(event),
|
||||||
Locale(event),
|
Locale(event),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from django.http import HttpRequest
|
|||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries import Countries
|
from django_countries import Countries
|
||||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
@@ -29,6 +29,7 @@ from pretix.base.models import (
|
|||||||
OrderRefund, Quota,
|
OrderRefund, Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
||||||
|
from pretix.base.services.cart import get_fees
|
||||||
from pretix.base.settings import SettingsSandbox
|
from pretix.base.settings import SettingsSandbox
|
||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
@@ -1106,8 +1107,16 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
return
|
return
|
||||||
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
|
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
|
||||||
|
|
||||||
remainder = cart['total'] - gc.value
|
total = sum(p.total for p in cart['positions'])
|
||||||
if remainder >= Decimal('0.00'):
|
# Recompute fees. Some plugins, e.g. pretix-servicefees, change their fee schedule if a gift card is
|
||||||
|
# applied.
|
||||||
|
fees = get_fees(
|
||||||
|
self.event, request, total, cart['invoice_address'], cs.get('payment'),
|
||||||
|
cart['raw']
|
||||||
|
)
|
||||||
|
total += sum([f.value for f in fees])
|
||||||
|
remainder = total - gc.value
|
||||||
|
if remainder > Decimal('0.00'):
|
||||||
del cs['payment']
|
del cs['payment']
|
||||||
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
|
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
|
||||||
money_filter(remainder, self.event.currency)
|
money_filter(remainder, self.event.currency)
|
||||||
@@ -1210,6 +1219,7 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
)
|
)
|
||||||
refund.info_data = {
|
refund.info_data = {
|
||||||
'gift_card': gc.pk,
|
'gift_card': gc.pk,
|
||||||
|
'gift_card_code': gc.secret,
|
||||||
'transaction_id': trans.pk,
|
'transaction_id': trans.pk,
|
||||||
}
|
}
|
||||||
refund.done()
|
refund.done()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from django.dispatch import receiver
|
|||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from PyPDF2 import PdfFileReader
|
from PyPDF2 import PdfFileReader
|
||||||
from pytz import timezone
|
from pytz import timezone
|
||||||
from reportlab.graphics import renderPDF
|
from reportlab.graphics import renderPDF
|
||||||
@@ -205,6 +205,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"editor_sample": _("Sample city"),
|
"editor_sample": _("Sample city"),
|
||||||
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
|
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
|
||||||
}),
|
}),
|
||||||
|
("attendee_company", {
|
||||||
|
"label": _("Attendee company"),
|
||||||
|
"editor_sample": _("Sample company"),
|
||||||
|
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
|
||||||
|
}),
|
||||||
("addons", {
|
("addons", {
|
||||||
"label": _("List of Add-Ons"),
|
"label": _("List of Add-Ons"),
|
||||||
"editor_sample": _("Addon 1\nAddon 2"),
|
"editor_sample": _("Addon 1\nAddon 2"),
|
||||||
@@ -431,6 +436,8 @@ class Renderer:
|
|||||||
return '(error)'
|
return '(error)'
|
||||||
if o['content'] == 'other':
|
if o['content'] == 'other':
|
||||||
return o['text']
|
return o['text']
|
||||||
|
elif o['content'].startswith('itemmeta:'):
|
||||||
|
return op.item.meta_data.get(o['content'][9:]) or ''
|
||||||
elif o['content'].startswith('meta:'):
|
elif o['content'].startswith('meta:'):
|
||||||
return ev.meta_data.get(o['content'][5:]) or ''
|
return ev.meta_data.get(o['content'][5:]) or ''
|
||||||
elif o['content'] in self.variables:
|
elif o['content'] in self.variables:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from dateutil import parser
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
BASE_CHOICES = (
|
BASE_CHOICES = (
|
||||||
@@ -324,7 +324,7 @@ class ModelRelativeDateTimeField(models.CharField):
|
|||||||
return value.to_string()
|
return value.to_string()
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def from_db_value(self, value, expression, connection, context):
|
def from_db_value(self, value, expression, connection):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
return RelativeDateWrapper.from_string(value)
|
return RelativeDateWrapper.from_string(value)
|
||||||
|
|||||||
224
src/pretix/base/services/cancelevent.py
Normal file
224
src/pretix/base/services/cancelevent.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import logging
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
from pretix.base.decimal import round_decimal
|
||||||
|
from pretix.base.email import get_email_context
|
||||||
|
from pretix.base.i18n import language
|
||||||
|
from pretix.base.models import (
|
||||||
|
Event, InvoiceAddress, Order, OrderFee, OrderPosition, OrderRefund,
|
||||||
|
SubEvent, User, WaitingListEntry,
|
||||||
|
)
|
||||||
|
from pretix.base.services.locking import LockTimeoutException
|
||||||
|
from pretix.base.services.mail import SendMailException, TolerantDict, mail
|
||||||
|
from pretix.base.services.orders import (
|
||||||
|
OrderChangeManager, OrderError, _cancel_order, _try_auto_refund,
|
||||||
|
)
|
||||||
|
from pretix.base.services.tasks import ProfiledEventTask
|
||||||
|
from pretix.celery_app import app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent):
|
||||||
|
with language(wle.locale):
|
||||||
|
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
|
||||||
|
try:
|
||||||
|
mail(
|
||||||
|
wle.email,
|
||||||
|
str(subject).format_map(TolerantDict(email_context)),
|
||||||
|
message,
|
||||||
|
email_context,
|
||||||
|
wle.event,
|
||||||
|
locale=wle.locale
|
||||||
|
)
|
||||||
|
except SendMailException:
|
||||||
|
logger.exception('Waiting list canceled email could not be sent')
|
||||||
|
|
||||||
|
|
||||||
|
def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent,
|
||||||
|
refund_amount: Decimal, user: User, positions: list):
|
||||||
|
with language(order.locale):
|
||||||
|
try:
|
||||||
|
ia = order.invoice_address
|
||||||
|
except InvoiceAddress.DoesNotExist:
|
||||||
|
ia = InvoiceAddress()
|
||||||
|
|
||||||
|
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
|
||||||
|
order=order, position_or_address=ia, event=order.event)
|
||||||
|
real_subject = str(subject).format_map(TolerantDict(email_context))
|
||||||
|
try:
|
||||||
|
order.send_mail(
|
||||||
|
real_subject, message, email_context,
|
||||||
|
'pretix.event.order.email.event_canceled',
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
except SendMailException:
|
||||||
|
logger.exception('Order canceled email could not be sent')
|
||||||
|
|
||||||
|
for p in positions:
|
||||||
|
if subevent and p.subevent_id != subevent.id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||||
|
real_subject = str(subject).format_map(TolerantDict(email_context))
|
||||||
|
email_context = get_email_context(event_or_subevent=subevent or order.event,
|
||||||
|
event=order.event,
|
||||||
|
refund_amount=refund_amount,
|
||||||
|
position_or_address=p,
|
||||||
|
order=order, position=p)
|
||||||
|
try:
|
||||||
|
order.send_mail(
|
||||||
|
real_subject, message, email_context,
|
||||||
|
'pretix.event.order.email.event_canceled',
|
||||||
|
position=p,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
except SendMailException:
|
||||||
|
logger.exception('Order canceled email could not be sent to attendee')
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
|
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
|
||||||
|
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
|
||||||
|
send: bool=False, send_subject: dict=None, send_message: dict=None,
|
||||||
|
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
|
||||||
|
user: int=None, refund_as_giftcard: bool=False):
|
||||||
|
send_subject = LazyI18nString(send_subject)
|
||||||
|
send_message = LazyI18nString(send_message)
|
||||||
|
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
|
||||||
|
send_waitinglist_message = LazyI18nString(send_waitinglist_message)
|
||||||
|
if user:
|
||||||
|
user = User.objects.get(pk=user)
|
||||||
|
|
||||||
|
s = OrderPosition.objects.filter(
|
||||||
|
order=OuterRef('pk')
|
||||||
|
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||||
|
orders_to_cancel = event.orders.annotate(pcnt=Subquery(s, output_field=IntegerField())).filter(
|
||||||
|
status__in=[Order.STATUS_PAID, Order.STATUS_PENDING, Order.STATUS_EXPIRED],
|
||||||
|
pcnt__gt=0
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if subevent:
|
||||||
|
subevent = event.subevents.get(pk=subevent)
|
||||||
|
|
||||||
|
has_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).filter(
|
||||||
|
subevent=subevent
|
||||||
|
)
|
||||||
|
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
|
||||||
|
subevent=subevent
|
||||||
|
)
|
||||||
|
orders_to_change = orders_to_cancel.annotate(
|
||||||
|
has_subevent=Exists(has_subevent),
|
||||||
|
has_other_subevent=Exists(has_other_subevent),
|
||||||
|
).filter(
|
||||||
|
has_subevent=True, has_other_subevent=True
|
||||||
|
)
|
||||||
|
orders_to_cancel = orders_to_cancel.annotate(
|
||||||
|
has_subevent=Exists(has_subevent),
|
||||||
|
has_other_subevent=Exists(has_other_subevent),
|
||||||
|
).filter(
|
||||||
|
has_subevent=True, has_other_subevent=False
|
||||||
|
)
|
||||||
|
|
||||||
|
subevent.log_action(
|
||||||
|
'pretix.subevent.canceled', user=user,
|
||||||
|
)
|
||||||
|
subevent.active = False
|
||||||
|
subevent.save(update_fields=['active'])
|
||||||
|
subevent.log_action(
|
||||||
|
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
orders_to_change = event.orders.none()
|
||||||
|
event.log_action(
|
||||||
|
'pretix.event.canceled', user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in event.items.filter(active=True):
|
||||||
|
i.active = False
|
||||||
|
i.save(update_fields=['active'])
|
||||||
|
i.log_action(
|
||||||
|
'pretix.event.item.changed', user=user, data={'active': False, '_source': 'cancel_event'}
|
||||||
|
)
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for o in orders_to_cancel.only('id', 'total'):
|
||||||
|
try:
|
||||||
|
fee = Decimal('0.00')
|
||||||
|
fee_sum = Decimal('0.00')
|
||||||
|
keep_fee_objects = []
|
||||||
|
if keep_fees:
|
||||||
|
for f in o.fees.all():
|
||||||
|
if f.fee_type in keep_fees:
|
||||||
|
fee += f.value
|
||||||
|
keep_fee_objects.append(f)
|
||||||
|
fee_sum += f.value
|
||||||
|
if keep_fee_percentage:
|
||||||
|
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum)
|
||||||
|
if keep_fee_fixed:
|
||||||
|
fee += Decimal(keep_fee_fixed)
|
||||||
|
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
|
||||||
|
|
||||||
|
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects)
|
||||||
|
refund_amount = o.payment_refund_sum
|
||||||
|
|
||||||
|
try:
|
||||||
|
if auto_refund:
|
||||||
|
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
|
||||||
|
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard)
|
||||||
|
finally:
|
||||||
|
if send:
|
||||||
|
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
|
||||||
|
except LockTimeoutException:
|
||||||
|
logger.exception("Could not cancel order")
|
||||||
|
failed += 1
|
||||||
|
except OrderError:
|
||||||
|
logger.exception("Could not cancel order")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
for o in orders_to_change.values_list('id', flat=True):
|
||||||
|
with transaction.atomic():
|
||||||
|
o = event.orders.select_for_update().get(pk=o)
|
||||||
|
total = Decimal('0.00')
|
||||||
|
positions = []
|
||||||
|
|
||||||
|
ocm = OrderChangeManager(o, user=user, notify=False)
|
||||||
|
for p in o.positions.all():
|
||||||
|
if p.subevent == subevent:
|
||||||
|
total += p.price
|
||||||
|
ocm.cancel(p)
|
||||||
|
positions.append(p)
|
||||||
|
|
||||||
|
fee = Decimal('0.00')
|
||||||
|
if keep_fee_fixed:
|
||||||
|
fee += Decimal(keep_fee_fixed)
|
||||||
|
if keep_fee_percentage:
|
||||||
|
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * total
|
||||||
|
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
|
||||||
|
if fee:
|
||||||
|
f = OrderFee(
|
||||||
|
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
||||||
|
value=fee,
|
||||||
|
order=o,
|
||||||
|
tax_rule=o.event.settings.tax_rate_default,
|
||||||
|
)
|
||||||
|
f._calculate_tax()
|
||||||
|
ocm.add_fee(f)
|
||||||
|
|
||||||
|
ocm.commit()
|
||||||
|
refund_amount = o.payment_refund_sum - o.total
|
||||||
|
|
||||||
|
if auto_refund:
|
||||||
|
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN)
|
||||||
|
|
||||||
|
if send:
|
||||||
|
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions)
|
||||||
|
|
||||||
|
for wle in event.waitinglistentries.filter(subevent=subevent, voucher__isnull=True):
|
||||||
|
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, subevent)
|
||||||
|
|
||||||
|
return failed
|
||||||
@@ -9,7 +9,7 @@ from django.db import DatabaseError, transaction
|
|||||||
from django.db.models import Count, Exists, OuterRef, Q
|
from django.db.models import Count, Exists, OuterRef, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext as _
|
from django.utils.translation import gettext as _, pgettext_lazy
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
@@ -213,7 +213,7 @@ class CartManager:
|
|||||||
has_variations=Count('variations'),
|
has_variations=Count('variations'),
|
||||||
).filter(
|
).filter(
|
||||||
id__in=[i for i in item_ids if i and i not in self._items_cache]
|
id__in=[i for i in item_ids if i and i not in self._items_cache]
|
||||||
)
|
).order_by()
|
||||||
})
|
})
|
||||||
self._variations_cache.update({
|
self._variations_cache.update({
|
||||||
v.pk: v
|
v.pk: v
|
||||||
@@ -221,7 +221,7 @@ class CartManager:
|
|||||||
'quotas'
|
'quotas'
|
||||||
).select_related('item', 'item__event').filter(
|
).select_related('item', 'item__event').filter(
|
||||||
id__in=[i for i in variation_ids if i and i not in self._variations_cache]
|
id__in=[i for i in variation_ids if i and i not in self._variations_cache]
|
||||||
)
|
).order_by()
|
||||||
})
|
})
|
||||||
|
|
||||||
def _check_max_cart_size(self):
|
def _check_max_cart_size(self):
|
||||||
@@ -303,32 +303,6 @@ class CartManager:
|
|||||||
if op.item.require_bundling and not op.addon_to == 'FAKE':
|
if op.item.require_bundling and not op.addon_to == 'FAKE':
|
||||||
raise CartError(error_messages['bundled_only'])
|
raise CartError(error_messages['bundled_only'])
|
||||||
|
|
||||||
if op.item.max_per_order or op.item.min_per_order:
|
|
||||||
new_total = (
|
|
||||||
len([1 for p in self.positions if p.item_id == op.item.pk]) +
|
|
||||||
sum([_op.count for _op in self._operations + current_ops
|
|
||||||
if isinstance(_op, self.AddOperation) and _op.item == op.item]) +
|
|
||||||
op.count -
|
|
||||||
len([1 for _op in self._operations + current_ops
|
|
||||||
if isinstance(_op, self.RemoveOperation) and _op.position.item_id == op.item.pk])
|
|
||||||
)
|
|
||||||
|
|
||||||
if op.item.max_per_order and new_total > op.item.max_per_order:
|
|
||||||
raise CartError(
|
|
||||||
_(error_messages['max_items_per_product']) % {
|
|
||||||
'max': op.item.max_per_order,
|
|
||||||
'product': op.item.name
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if op.item.min_per_order and new_total < op.item.min_per_order:
|
|
||||||
raise CartError(
|
|
||||||
_(error_messages['min_items_per_product']) % {
|
|
||||||
'min': op.item.min_per_order,
|
|
||||||
'product': op.item.name
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_price(self, item: Item, variation: Optional[ItemVariation],
|
def _get_price(self, item: Item, variation: Optional[ItemVariation],
|
||||||
voucher: Optional[Voucher], custom_price: Optional[Decimal],
|
voucher: Optional[Voucher], custom_price: Optional[Decimal],
|
||||||
subevent: Optional[SubEvent], cp_is_net: bool=None, force_custom_price=False,
|
subevent: Optional[SubEvent], cp_is_net: bool=None, force_custom_price=False,
|
||||||
@@ -787,37 +761,48 @@ class CartManager:
|
|||||||
|
|
||||||
return vouchers_ok
|
return vouchers_ok
|
||||||
|
|
||||||
def _check_min_per_product(self):
|
def _check_min_max_per_product(self):
|
||||||
per_product = Counter()
|
items = Counter()
|
||||||
min_per_product = {}
|
|
||||||
for p in self.positions:
|
for p in self.positions:
|
||||||
per_product[p.item_id] += 1
|
items[p.item] += 1
|
||||||
min_per_product[p.item.pk] = p.item.min_per_order
|
|
||||||
|
|
||||||
for op in self._operations:
|
for op in self._operations:
|
||||||
if isinstance(op, self.AddOperation):
|
if isinstance(op, self.AddOperation):
|
||||||
per_product[op.item.pk] += op.count
|
items[op.item] += op.count
|
||||||
min_per_product[op.item.pk] = op.item.min_per_order
|
|
||||||
elif isinstance(op, self.RemoveOperation):
|
elif isinstance(op, self.RemoveOperation):
|
||||||
per_product[op.position.item_id] -= 1
|
items[op.position.item] -= 1
|
||||||
min_per_product[op.position.item.pk] = op.position.item.min_per_order
|
|
||||||
|
|
||||||
err = None
|
err = None
|
||||||
for itemid, num in per_product.items():
|
for item, count in items.items():
|
||||||
min_p = min_per_product[itemid]
|
if count == 0:
|
||||||
if min_p and num < min_p:
|
continue
|
||||||
|
|
||||||
|
if item.max_per_order and count > item.max_per_order:
|
||||||
|
raise CartError(
|
||||||
|
_(error_messages['max_items_per_product']) % {
|
||||||
|
'max': item.max_per_order,
|
||||||
|
'product': item.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.min_per_order and count < item.min_per_order:
|
||||||
self._operations = [o for o in self._operations if not (
|
self._operations = [o for o in self._operations if not (
|
||||||
isinstance(o, self.AddOperation) and o.item.pk == itemid
|
isinstance(o, self.AddOperation) and o.item.pk == item.pk
|
||||||
)]
|
)]
|
||||||
removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)]
|
removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)]
|
||||||
for p in self.positions:
|
for p in self.positions:
|
||||||
if p.item_id == itemid and p.pk not in removals:
|
if p.item_id == item.pk and p.pk not in removals:
|
||||||
self._operations.append(self.RemoveOperation(position=p))
|
self._operations.append(self.RemoveOperation(position=p))
|
||||||
err = _(error_messages['min_items_per_product_removed']) % {
|
err = _(error_messages['min_items_per_product_removed']) % {
|
||||||
'min': min_p,
|
'min': item.min_per_order,
|
||||||
'product': p.item.name
|
'product': item.name
|
||||||
}
|
}
|
||||||
|
if not err:
|
||||||
|
raise CartError(
|
||||||
|
_(error_messages['min_items_per_product']) % {
|
||||||
|
'min': item.min_per_order,
|
||||||
|
'product': item.name
|
||||||
|
}
|
||||||
|
)
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def _perform_operations(self):
|
def _perform_operations(self):
|
||||||
@@ -826,7 +811,7 @@ class CartManager:
|
|||||||
err = None
|
err = None
|
||||||
new_cart_positions = []
|
new_cart_positions = []
|
||||||
|
|
||||||
err = err or self._check_min_per_product()
|
err = err or self._check_min_max_per_product()
|
||||||
|
|
||||||
self._operations.sort(key=lambda a: self.order[type(a)])
|
self._operations.sort(key=lambda a: self.order[type(a)])
|
||||||
seats_seen = set()
|
seats_seen = set()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from django.db import transaction
|
|||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
|
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.timezone import override
|
from django.utils.timezone import override
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import gettext
|
||||||
|
|
||||||
from pretix.base.i18n import LazyLocaleException, language
|
from pretix.base.i18n import LazyLocaleException, language
|
||||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||||
@@ -26,7 +26,7 @@ def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any])
|
|||||||
d = ex.render(form_data)
|
d = ex.render(form_data)
|
||||||
if d is None:
|
if d is None:
|
||||||
raise ExportError(
|
raise ExportError(
|
||||||
ugettext('Your export did not contain any data.')
|
gettext('Your export did not contain any data.')
|
||||||
)
|
)
|
||||||
file.filename, file.type, data = d
|
file.filename, file.type, data = d
|
||||||
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from django.dispatch import receiver
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext, ugettext as _
|
from django.utils.translation import gettext as _, pgettext
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
@@ -37,6 +37,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def build_invoice(invoice: Invoice) -> Invoice:
|
def build_invoice(invoice: Invoice) -> Invoice:
|
||||||
|
invoice.locale = invoice.event.settings.get('invoice_language', invoice.event.settings.locale)
|
||||||
|
if invoice.locale == '__user__':
|
||||||
|
invoice.locale = invoice.order.locale or invoice.event.settings.locale
|
||||||
|
|
||||||
lp = invoice.order.payments.last()
|
lp = invoice.order.payments.last()
|
||||||
|
|
||||||
with language(invoice.locale):
|
with language(invoice.locale):
|
||||||
@@ -85,6 +89,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
).split("\n") if a.strip()
|
).split("\n") if a.strip()
|
||||||
)
|
)
|
||||||
invoice.internal_reference = ia.internal_reference
|
invoice.internal_reference = ia.internal_reference
|
||||||
|
invoice.custom_field = ia.custom_field
|
||||||
invoice.invoice_to_company = ia.company
|
invoice.invoice_to_company = ia.company
|
||||||
invoice.invoice_to_name = ia.name
|
invoice.invoice_to_name = ia.name
|
||||||
invoice.invoice_to_street = ia.street
|
invoice.invoice_to_street = ia.street
|
||||||
@@ -249,17 +254,11 @@ def regenerate_invoice(invoice: Invoice):
|
|||||||
|
|
||||||
|
|
||||||
def generate_invoice(order: Order, trigger_pdf=True):
|
def generate_invoice(order: Order, trigger_pdf=True):
|
||||||
locale = order.event.settings.get('invoice_language', order.event.settings.locale)
|
|
||||||
if locale:
|
|
||||||
if locale == '__user__':
|
|
||||||
locale = order.locale or order.event.settings.locale
|
|
||||||
|
|
||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
order=order,
|
order=order,
|
||||||
event=order.event,
|
event=order.event,
|
||||||
organizer=order.event.organizer,
|
organizer=order.event.organizer,
|
||||||
date=timezone.now().date(),
|
date=timezone.now().date(),
|
||||||
locale=locale
|
|
||||||
)
|
)
|
||||||
invoice = build_invoice(invoice)
|
invoice = build_invoice(invoice)
|
||||||
if trigger_pdf:
|
if trigger_pdf:
|
||||||
@@ -313,7 +312,7 @@ def build_preview_invoice_pdf(event):
|
|||||||
|
|
||||||
with rolledback_transaction(), language(locale):
|
with rolledback_transaction(), language(locale):
|
||||||
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
|
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
|
||||||
expires=timezone.now(), code="PREVIEW", total=119)
|
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
|
||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
order=order, event=event, invoice_no="PREVIEW",
|
order=order, event=event, invoice_no="PREVIEW",
|
||||||
date=timezone.now().date(), locale=locale, organizer=event.organizer
|
date=timezone.now().date(), locale=locale, organizer=event.organizer
|
||||||
@@ -351,7 +350,7 @@ def build_preview_invoice_pdf(event):
|
|||||||
|
|
||||||
if event.tax_rules.exists():
|
if event.tax_rules.exists():
|
||||||
for i, tr in enumerate(event.tax_rules.all()):
|
for i, tr in enumerate(event.tax_rules.all()):
|
||||||
tax = tr.tax(Decimal('100.00'))
|
tax = tr.tax(Decimal('100.00'), base_price_is='gross')
|
||||||
InvoiceLine.objects.create(
|
InvoiceLine.objects.create(
|
||||||
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
||||||
gross_value=tax.gross, tax_value=tax.tax,
|
gross_value=tax.gross, tax_value=tax.tax,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from django.core.mail import (
|
|||||||
)
|
)
|
||||||
from django.core.mail.message import SafeMIMEText
|
from django.core.mail.message import SafeMIMEText
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.translation import pgettext, ugettext as _
|
from django.utils.translation import gettext as _, pgettext
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
renderer = ClassicMailRenderer(None)
|
renderer = ClassicMailRenderer(None)
|
||||||
content_plain = body_plain = render_mail(template, context)
|
content_plain = body_plain = render_mail(template, context)
|
||||||
subject = str(subject).format_map(TolerantDict(context))
|
subject = str(subject).format_map(TolerantDict(context))
|
||||||
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
|
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM) or settings.MAIL_FROM
|
||||||
if event:
|
if event:
|
||||||
sender_name = event.settings.mail_from_name or str(event.name)
|
sender_name = event.settings.mail_from_name or str(event.name)
|
||||||
sender = formataddr((sender_name, sender))
|
sender = formataddr((sender_name, sender))
|
||||||
@@ -276,20 +276,6 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
cm = lambda: scopes_disabled() # noqa
|
cm = lambda: scopes_disabled() # noqa
|
||||||
|
|
||||||
with cm():
|
with cm():
|
||||||
if invoices:
|
|
||||||
invoices = Invoice.objects.filter(pk__in=invoices)
|
|
||||||
for inv in invoices:
|
|
||||||
if inv.file:
|
|
||||||
try:
|
|
||||||
with language(inv.order.locale):
|
|
||||||
email.attach(
|
|
||||||
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
|
|
||||||
inv.file.file.read(),
|
|
||||||
'application/pdf'
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
logger.exception('Could not attach invoice to email')
|
|
||||||
pass
|
|
||||||
if event:
|
if event:
|
||||||
if order:
|
if order:
|
||||||
try:
|
try:
|
||||||
@@ -344,6 +330,21 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
|
|
||||||
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
|
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
|
||||||
|
|
||||||
|
if invoices:
|
||||||
|
invoices = Invoice.objects.filter(pk__in=invoices)
|
||||||
|
for inv in invoices:
|
||||||
|
if inv.file:
|
||||||
|
try:
|
||||||
|
with language(inv.order.locale):
|
||||||
|
email.attach(
|
||||||
|
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
|
||||||
|
inv.file.file.read(),
|
||||||
|
'application/pdf'
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
logger.exception('Could not attach invoice to email')
|
||||||
|
pass
|
||||||
|
|
||||||
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
|
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
from django.utils.timezone import override
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from inlinestyler.utils import inline_css
|
from inlinestyler.utils import inline_css
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ def send_notification(logentry_id: int, action_type: str, user_id: int, method:
|
|||||||
if not notification_type:
|
if not notification_type:
|
||||||
return # Ignore, e.g. plugin not active for this event
|
return # Ignore, e.g. plugin not active for this event
|
||||||
|
|
||||||
with language(user.locale):
|
with language(user.locale), override(logentry.event.timezone if logentry.event else user.timezone):
|
||||||
notification = notification_type.build_notification(logentry)
|
notification = notification_type.build_notification(logentry)
|
||||||
|
|
||||||
if method == "mail":
|
if method == "mail":
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ def parse_csv(file, length=None):
|
|||||||
if '\r' in data and '\n' not in data:
|
if '\r' in data and '\n' not in data:
|
||||||
data = data.replace('\r', '\n')
|
data = data.replace('\r', '\n')
|
||||||
|
|
||||||
dialect = csv.Sniffer().sniff(data.split("\n")[0], delimiters=";,.#:")
|
try:
|
||||||
|
dialect = csv.Sniffer().sniff(data.split("\n")[0], delimiters=";,.#:")
|
||||||
|
except csv.Error:
|
||||||
|
return None
|
||||||
|
|
||||||
if dialect is None:
|
if dialect is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from django.db.transaction import get_connection
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.api.models import OAuthApplication
|
from pretix.api.models import OAuthApplication
|
||||||
@@ -94,6 +94,54 @@ def mark_order_paid(*args, **kwargs):
|
|||||||
raise NotImplementedError("This method is no longer supported since pretix 1.17.")
|
raise NotImplementedError("This method is no longer supported since pretix 1.17.")
|
||||||
|
|
||||||
|
|
||||||
|
def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None):
|
||||||
|
"""
|
||||||
|
Reactivates a canceled order. If ``force`` is not set to ``True``, this will fail if there is not
|
||||||
|
enough quota.
|
||||||
|
"""
|
||||||
|
if order.status != Order.STATUS_CANCELED:
|
||||||
|
raise OrderError('The order was not canceled.')
|
||||||
|
|
||||||
|
with order.event.lock() as now_dt:
|
||||||
|
is_available = force or order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True)
|
||||||
|
if is_available is True:
|
||||||
|
if order.payment_refund_sum >= order.total:
|
||||||
|
order.status = Order.STATUS_PAID
|
||||||
|
else:
|
||||||
|
order.status = Order.STATUS_PENDING
|
||||||
|
order.cancellation_date = None
|
||||||
|
order.set_expires(now(),
|
||||||
|
order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
|
||||||
|
with transaction.atomic():
|
||||||
|
order.save(update_fields=['expires', 'status', 'cancellation_date'])
|
||||||
|
order.log_action(
|
||||||
|
'pretix.event.order.reactivated',
|
||||||
|
user=user,
|
||||||
|
auth=auth,
|
||||||
|
data={
|
||||||
|
'expires': order.expires,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for position in order.positions.all():
|
||||||
|
if position.voucher:
|
||||||
|
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') + 1))
|
||||||
|
|
||||||
|
for gc in position.issued_gift_cards.all():
|
||||||
|
gc = GiftCard.objects.select_for_update().get(pk=gc.pk)
|
||||||
|
gc.transactions.create(value=position.price, order=order)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise OrderError(is_available)
|
||||||
|
|
||||||
|
order_approved.send(order.event, order=order)
|
||||||
|
if order.status == Order.STATUS_PAID:
|
||||||
|
order_paid.send(order.event, order=order)
|
||||||
|
|
||||||
|
num_invoices = order.invoices.filter(is_cancellation=False).count()
|
||||||
|
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
|
||||||
|
generate_invoice(order)
|
||||||
|
|
||||||
|
|
||||||
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None):
|
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None):
|
||||||
"""
|
"""
|
||||||
Extends the deadline of an order. If the order is already expired, the quota will be checked to
|
Extends the deadline of an order. If the order is already expired, the quota will be checked to
|
||||||
@@ -117,9 +165,10 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
|
|||||||
'state_change': was_expired
|
'state_change': was_expired
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if was_expired:
|
if was_expired:
|
||||||
num_invoices = order.invoices.filter(is_cancellation=False).count()
|
num_invoices = order.invoices.filter(is_cancellation=False).count()
|
||||||
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices:
|
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
|
||||||
generate_invoice(order)
|
generate_invoice(order)
|
||||||
|
|
||||||
if order.status == Order.STATUS_PENDING:
|
if order.status == Order.STATUS_PENDING:
|
||||||
@@ -271,15 +320,16 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
|||||||
|
|
||||||
|
|
||||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
|
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
|
||||||
cancellation_fee=None):
|
cancellation_fee=None, keep_fees=None):
|
||||||
"""
|
"""
|
||||||
Mark this order as canceled
|
Mark this order as canceled
|
||||||
:param order: The order to change
|
:param order: The order to change
|
||||||
:param user: The user that performed the change
|
:param user: The user that performed the change
|
||||||
"""
|
"""
|
||||||
|
# If new actions are added to this function, make sure to add the reverse operation to reactivate_order()
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if isinstance(order, int):
|
if isinstance(order, int):
|
||||||
order = Order.objects.get(pk=order)
|
order = Order.objects.select_for_update().get(pk=order)
|
||||||
if isinstance(user, int):
|
if isinstance(user, int):
|
||||||
user = User.objects.get(pk=user)
|
user = User.objects.get(pk=user)
|
||||||
if isinstance(api_token, int):
|
if isinstance(api_token, int):
|
||||||
@@ -294,8 +344,8 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
if not order.cancel_allowed():
|
if not order.cancel_allowed():
|
||||||
raise OrderError(_('You cannot cancel this order.'))
|
raise OrderError(_('You cannot cancel this order.'))
|
||||||
invoices = []
|
invoices = []
|
||||||
i = order.invoices.filter(is_cancellation=False, refered__isnull=True).last()
|
i = order.invoices.filter(is_cancellation=False).last()
|
||||||
if i:
|
if i and not i.refered.exists():
|
||||||
invoices.append(generate_cancellation(i))
|
invoices.append(generate_cancellation(i))
|
||||||
|
|
||||||
for position in order.positions.all():
|
for position in order.positions.all():
|
||||||
@@ -318,31 +368,38 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||||
position.canceled = True
|
position.canceled = True
|
||||||
position.save(update_fields=['canceled'])
|
position.save(update_fields=['canceled'])
|
||||||
|
new_fee = cancellation_fee
|
||||||
for fee in order.fees.all():
|
for fee in order.fees.all():
|
||||||
fee.canceled = True
|
if keep_fees and fee in keep_fees:
|
||||||
fee.save(update_fields=['canceled'])
|
new_fee -= fee.value
|
||||||
|
else:
|
||||||
|
fee.canceled = True
|
||||||
|
fee.save(update_fields=['canceled'])
|
||||||
|
|
||||||
f = OrderFee(
|
if new_fee:
|
||||||
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
f = OrderFee(
|
||||||
value=cancellation_fee,
|
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
||||||
tax_rule=order.event.settings.tax_rate_default,
|
value=new_fee,
|
||||||
order=order,
|
tax_rule=order.event.settings.tax_rate_default,
|
||||||
)
|
order=order,
|
||||||
f._calculate_tax()
|
)
|
||||||
f.save()
|
f._calculate_tax()
|
||||||
|
f.save()
|
||||||
|
|
||||||
if order.payment_refund_sum < cancellation_fee:
|
if order.payment_refund_sum < cancellation_fee:
|
||||||
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
|
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
|
||||||
order.status = Order.STATUS_PAID
|
order.status = Order.STATUS_PAID
|
||||||
order.total = f.value
|
order.total = cancellation_fee
|
||||||
order.save(update_fields=['status', 'total'])
|
order.cancellation_date = now()
|
||||||
|
order.save(update_fields=['status', 'cancellation_date', 'total'])
|
||||||
|
|
||||||
if i:
|
if i:
|
||||||
invoices.append(generate_invoice(order))
|
invoices.append(generate_invoice(order))
|
||||||
else:
|
else:
|
||||||
with order.event.lock():
|
with order.event.lock():
|
||||||
order.status = Order.STATUS_CANCELED
|
order.status = Order.STATUS_CANCELED
|
||||||
order.save(update_fields=['status'])
|
order.cancellation_date = now()
|
||||||
|
order.save(update_fields=['status', 'cancellation_date'])
|
||||||
|
|
||||||
for position in order.positions.all():
|
for position in order.positions.all():
|
||||||
if position.voucher:
|
if position.voucher:
|
||||||
@@ -350,6 +407,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
|
|
||||||
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
|
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
|
||||||
data={'cancellation_fee': cancellation_fee})
|
data={'cancellation_fee': cancellation_fee})
|
||||||
|
order.cancellation_requests.all().delete()
|
||||||
|
|
||||||
if send_mail:
|
if send_mail:
|
||||||
email_template = order.event.settings.mail_text_order_canceled
|
email_template = order.event.settings.mail_text_order_canceled
|
||||||
@@ -553,6 +611,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
bprice = bundle.designated_price or 0
|
bprice = bundle.designated_price or 0
|
||||||
except ItemBundle.DoesNotExist:
|
except ItemBundle.DoesNotExist:
|
||||||
bprice = cp.price
|
bprice = cp.price
|
||||||
|
except ItemBundle.MultipleObjectsReturned:
|
||||||
|
raise OrderError("Invalid product configuration (duplicate bundle)")
|
||||||
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
||||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||||
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
|
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
|
||||||
@@ -631,7 +691,7 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
|
|||||||
total = sum([c.price for c in positions])
|
total = sum([c.price for c in positions])
|
||||||
|
|
||||||
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total,
|
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total,
|
||||||
meta_info=meta_info, positions=positions):
|
meta_info=meta_info, positions=positions, gift_cards=gift_cards):
|
||||||
if resp:
|
if resp:
|
||||||
fees += resp
|
fees += resp
|
||||||
total += sum(f.value for f in fees)
|
total += sum(f.value for f in fees)
|
||||||
@@ -968,7 +1028,6 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
F('event__date_from')
|
F('event__date_from')
|
||||||
)
|
)
|
||||||
).filter(
|
).filter(
|
||||||
status=Order.STATUS_PAID,
|
|
||||||
download_reminder_sent=False,
|
download_reminder_sent=False,
|
||||||
datetime__lte=now() - timedelta(hours=2),
|
datetime__lte=now() - timedelta(hours=2),
|
||||||
first_date__gte=today,
|
first_date__gte=today,
|
||||||
@@ -998,6 +1057,22 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
if not all([r for rr, r in allow_ticket_download.send(event, order=o)]):
|
if not all([r for rr, r in allow_ticket_download.send(event, order=o)]):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not o.ticket_download_available:
|
||||||
|
continue
|
||||||
|
positions = o.positions.select_related('item')
|
||||||
|
|
||||||
|
if o.status != Order.STATUS_PAID:
|
||||||
|
if o.status != Order.STATUS_PENDING or o.require_approval or not \
|
||||||
|
o.event.settings.ticket_download_pending:
|
||||||
|
continue
|
||||||
|
send = False
|
||||||
|
for p in positions:
|
||||||
|
if p.generate_ticket:
|
||||||
|
send = True
|
||||||
|
break
|
||||||
|
if not send:
|
||||||
|
continue
|
||||||
|
|
||||||
with language(o.locale):
|
with language(o.locale):
|
||||||
o.download_reminder_sent = True
|
o.download_reminder_sent = True
|
||||||
o.save(update_fields=['download_reminder_sent'])
|
o.save(update_fields=['download_reminder_sent'])
|
||||||
@@ -1015,6 +1090,9 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
|
|
||||||
if event.settings.mail_send_download_reminder_attendee:
|
if event.settings.mail_send_download_reminder_attendee:
|
||||||
for p in o.positions.all():
|
for p in o.positions.all():
|
||||||
|
if not p.generate_ticket:
|
||||||
|
continue
|
||||||
|
|
||||||
if p.subevent_id:
|
if p.subevent_id:
|
||||||
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
|
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
|
||||||
hour=0, minute=0, second=0, microsecond=0
|
hour=0, minute=0, second=0, microsecond=0
|
||||||
@@ -1075,6 +1153,7 @@ class OrderChangeManager:
|
|||||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat'))
|
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat'))
|
||||||
SplitOperation = namedtuple('SplitOperation', ('position',))
|
SplitOperation = namedtuple('SplitOperation', ('position',))
|
||||||
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value'))
|
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value'))
|
||||||
|
AddFeeOperation = namedtuple('AddFeeOperation', ('fee',))
|
||||||
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee',))
|
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee',))
|
||||||
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
|
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
|
||||||
|
|
||||||
@@ -1145,6 +1224,27 @@ class OrderChangeManager:
|
|||||||
self._quotadiff.subtract(position.quotas)
|
self._quotadiff.subtract(position.quotas)
|
||||||
self._operations.append(self.SubeventOperation(position, subevent))
|
self._operations.append(self.SubeventOperation(position, subevent))
|
||||||
|
|
||||||
|
def change_item_and_subevent(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation],
|
||||||
|
subevent: SubEvent):
|
||||||
|
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
|
||||||
|
raise OrderError(self.error_messages['product_without_variation'])
|
||||||
|
|
||||||
|
price = get_price(item, variation, voucher=position.voucher, subevent=subevent,
|
||||||
|
invoice_address=self._invoice_address)
|
||||||
|
|
||||||
|
if price is None: # NOQA
|
||||||
|
raise OrderError(self.error_messages['product_invalid'])
|
||||||
|
|
||||||
|
new_quotas = (variation.quotas.filter(subevent=subevent)
|
||||||
|
if variation else item.quotas.filter(subevent=subevent))
|
||||||
|
if not new_quotas:
|
||||||
|
raise OrderError(self.error_messages['quota_missing'])
|
||||||
|
|
||||||
|
self._quotadiff.update(new_quotas)
|
||||||
|
self._quotadiff.subtract(position.quotas)
|
||||||
|
self._operations.append(self.ItemOperation(position, item, variation))
|
||||||
|
self._operations.append(self.SubeventOperation(position, subevent))
|
||||||
|
|
||||||
def regenerate_secret(self, position: OrderPosition):
|
def regenerate_secret(self, position: OrderPosition):
|
||||||
self._operations.append(self.RegenerateSecretOperation(position))
|
self._operations.append(self.RegenerateSecretOperation(position))
|
||||||
|
|
||||||
@@ -1188,6 +1288,11 @@ class OrderChangeManager:
|
|||||||
self._operations.append(self.CancelFeeOperation(fee))
|
self._operations.append(self.CancelFeeOperation(fee))
|
||||||
self._invoice_dirty = True
|
self._invoice_dirty = True
|
||||||
|
|
||||||
|
def add_fee(self, fee: OrderFee):
|
||||||
|
self._totaldiff += fee.value
|
||||||
|
self._invoice_dirty = True
|
||||||
|
self._operations.append(self.AddFeeOperation(fee))
|
||||||
|
|
||||||
def change_fee(self, fee: OrderFee, value: Decimal):
|
def change_fee(self, fee: OrderFee, value: Decimal):
|
||||||
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross')
|
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross')
|
||||||
self._totaldiff += value.gross - fee.value
|
self._totaldiff += value.gross - fee.value
|
||||||
@@ -1448,6 +1553,13 @@ class OrderChangeManager:
|
|||||||
invoice_address=self._invoice_address
|
invoice_address=self._invoice_address
|
||||||
).gross
|
).gross
|
||||||
)
|
)
|
||||||
|
elif isinstance(op, self.AddFeeOperation):
|
||||||
|
self.order.log_action('pretix.event.order.changed.addfee', user=self.user, auth=self.auth, data={
|
||||||
|
'fee': op.fee.pk,
|
||||||
|
})
|
||||||
|
op.fee.order = self.order
|
||||||
|
op.fee._calculate_tax()
|
||||||
|
op.fee.save()
|
||||||
elif isinstance(op, self.FeeValueOperation):
|
elif isinstance(op, self.FeeValueOperation):
|
||||||
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
|
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
|
||||||
'fee': op.fee.pk,
|
'fee': op.fee.pk,
|
||||||
@@ -1710,7 +1822,8 @@ class OrderChangeManager:
|
|||||||
i = self.order.invoices.filter(is_cancellation=False).last()
|
i = self.order.invoices.filter(is_cancellation=False).last()
|
||||||
if self.reissue_invoice and i and self._invoice_dirty:
|
if self.reissue_invoice and i and self._invoice_dirty:
|
||||||
self._invoices.append(generate_cancellation(i))
|
self._invoices.append(generate_cancellation(i))
|
||||||
self._invoices.append(generate_invoice(self.order))
|
if invoice_qualified(self.order):
|
||||||
|
self._invoices.append(generate_invoice(self.order))
|
||||||
|
|
||||||
def _check_complete_cancel(self):
|
def _check_complete_cancel(self):
|
||||||
current = self.order.positions.count()
|
current = self.order.positions.count()
|
||||||
@@ -1800,61 +1913,127 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
|
|||||||
raise OrderError(str(error_messages['busy']))
|
raise OrderError(str(error_messages['busy']))
|
||||||
|
|
||||||
|
|
||||||
|
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER,
|
||||||
|
refund_as_giftcard=False):
|
||||||
|
notify_admin = False
|
||||||
|
error = False
|
||||||
|
if isinstance(order, int):
|
||||||
|
order = Order.objects.get(pk=order)
|
||||||
|
refund_amount = order.pending_sum * -1
|
||||||
|
if refund_amount <= Decimal('0.00'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if refund_as_giftcard:
|
||||||
|
proposals = {}
|
||||||
|
can_auto_refund = True
|
||||||
|
can_auto_refund_sum = refund_amount
|
||||||
|
with transaction.atomic():
|
||||||
|
giftcard = order.event.organizer.issued_gift_cards.create(
|
||||||
|
currency=order.event.currency,
|
||||||
|
testmode=order.testmode
|
||||||
|
)
|
||||||
|
giftcard.log_action('pretix.giftcards.created', data={})
|
||||||
|
r = order.refunds.create(
|
||||||
|
order=order,
|
||||||
|
payment=None,
|
||||||
|
source=source,
|
||||||
|
state=OrderRefund.REFUND_STATE_CREATED,
|
||||||
|
execution_date=now(),
|
||||||
|
amount=can_auto_refund_sum,
|
||||||
|
provider='giftcard',
|
||||||
|
info=json.dumps({
|
||||||
|
'gift_card': giftcard.pk
|
||||||
|
})
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
r.payment_provider.execute_refund(r)
|
||||||
|
except PaymentException as e:
|
||||||
|
with transaction.atomic():
|
||||||
|
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||||
|
r.save()
|
||||||
|
order.log_action('pretix.event.order.refund.failed', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
error = True
|
||||||
|
notify_admin = True
|
||||||
|
else:
|
||||||
|
if r.state != OrderRefund.REFUND_STATE_DONE:
|
||||||
|
notify_admin = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
proposals = order.propose_auto_refunds(refund_amount)
|
||||||
|
can_auto_refund_sum = sum(proposals.values())
|
||||||
|
can_auto_refund = (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount
|
||||||
|
if can_auto_refund:
|
||||||
|
for p, value in proposals.items():
|
||||||
|
with transaction.atomic():
|
||||||
|
r = order.refunds.create(
|
||||||
|
payment=p,
|
||||||
|
source=source,
|
||||||
|
state=OrderRefund.REFUND_STATE_CREATED,
|
||||||
|
amount=value,
|
||||||
|
provider=p.provider
|
||||||
|
)
|
||||||
|
order.log_action('pretix.event.order.refund.created', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
r.payment_provider.execute_refund(r)
|
||||||
|
except PaymentException as e:
|
||||||
|
with transaction.atomic():
|
||||||
|
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||||
|
r.save()
|
||||||
|
order.log_action('pretix.event.order.refund.failed', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
error = True
|
||||||
|
notify_admin = True
|
||||||
|
else:
|
||||||
|
if r.state not in (OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_DONE):
|
||||||
|
notify_admin = True
|
||||||
|
|
||||||
|
if refund_amount - can_auto_refund_sum > Decimal('0.00'):
|
||||||
|
if manual_refund:
|
||||||
|
with transaction.atomic():
|
||||||
|
r = order.refunds.create(
|
||||||
|
source=source,
|
||||||
|
state=OrderRefund.REFUND_STATE_CREATED,
|
||||||
|
amount=refund_amount - can_auto_refund_sum,
|
||||||
|
provider='manual'
|
||||||
|
)
|
||||||
|
order.log_action('pretix.event.order.refund.created', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
notify_admin = True
|
||||||
|
|
||||||
|
if notify_admin:
|
||||||
|
order.log_action('pretix.event.order.refund.requested')
|
||||||
|
if error:
|
||||||
|
raise OrderError(
|
||||||
|
_(
|
||||||
|
'There was an error while trying to send the money back to you. Please contact the event organizer '
|
||||||
|
'for further information.')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
||||||
device=None, cancellation_fee=None, try_auto_refund=False):
|
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
||||||
cancellation_fee)
|
cancellation_fee)
|
||||||
if try_auto_refund:
|
if try_auto_refund:
|
||||||
notify_admin = False
|
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard)
|
||||||
error = False
|
|
||||||
order = Order.objects.get(pk=order)
|
|
||||||
refund_amount = order.pending_sum * -1
|
|
||||||
proposals = order.propose_auto_refunds(refund_amount)
|
|
||||||
can_auto_refund = sum(proposals.values()) == refund_amount
|
|
||||||
if can_auto_refund:
|
|
||||||
for p, value in proposals.items():
|
|
||||||
with transaction.atomic():
|
|
||||||
r = order.refunds.create(
|
|
||||||
payment=p,
|
|
||||||
source=OrderRefund.REFUND_SOURCE_BUYER,
|
|
||||||
state=OrderRefund.REFUND_STATE_CREATED,
|
|
||||||
amount=value,
|
|
||||||
provider=p.provider
|
|
||||||
)
|
|
||||||
order.log_action('pretix.event.order.refund.created', {
|
|
||||||
'local_id': r.local_id,
|
|
||||||
'provider': r.provider,
|
|
||||||
})
|
|
||||||
|
|
||||||
try:
|
|
||||||
r.payment_provider.execute_refund(r)
|
|
||||||
except PaymentException as e:
|
|
||||||
with transaction.atomic():
|
|
||||||
r.state = OrderRefund.REFUND_STATE_FAILED
|
|
||||||
r.save()
|
|
||||||
order.log_action('pretix.event.order.refund.failed', {
|
|
||||||
'local_id': r.local_id,
|
|
||||||
'provider': r.provider,
|
|
||||||
'error': str(e)
|
|
||||||
})
|
|
||||||
error = True
|
|
||||||
notify_admin = True
|
|
||||||
else:
|
|
||||||
if r.state != OrderRefund.REFUND_STATE_DONE:
|
|
||||||
notify_admin = True
|
|
||||||
elif refund_amount != Decimal('0.00'):
|
|
||||||
notify_admin = True
|
|
||||||
|
|
||||||
if notify_admin:
|
|
||||||
order.log_action('pretix.event.order.refund.requested')
|
|
||||||
if error:
|
|
||||||
raise OrderError(
|
|
||||||
_('There was an error while trying to send the money back to you. Please contact the event organizer for further information.')
|
|
||||||
)
|
|
||||||
return ret
|
return ret
|
||||||
except LockTimeoutException:
|
except LockTimeoutException:
|
||||||
self.retry()
|
self.retry()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.i18n import LazyLocaleException
|
from pretix.base.i18n import LazyLocaleException
|
||||||
from pretix.base.models import CartPosition, Seat
|
from pretix.base.models import CartPosition, Seat
|
||||||
@@ -25,7 +25,7 @@ def validate_plan_change(event, subevent, plan):
|
|||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
).filter(
|
).filter(
|
||||||
Q(has_v=True) | Q(has_op=True)
|
Q(has_v=True) | Q(has_op=True)
|
||||||
).values_list('seat_guid', flat=True)
|
).values_list('seat_guid', flat=True).order_by()
|
||||||
)
|
)
|
||||||
new_seats = {
|
new_seats = {
|
||||||
ss.guid for ss in plan.iter_all_seats()
|
ss.guid for ss in plan.iter_all_seats()
|
||||||
@@ -40,7 +40,7 @@ def generate_seats(event, subevent, plan, mapping):
|
|||||||
current_seats = {}
|
current_seats = {}
|
||||||
for s in event.seats.select_related('product').annotate(
|
for s in event.seats.select_related('product').annotate(
|
||||||
has_op=Count('orderposition'), has_v=Count('vouchers')
|
has_op=Count('orderposition'), has_v=Count('vouchers')
|
||||||
).filter(subevent=subevent):
|
).filter(subevent=subevent).order_by():
|
||||||
if s.seat_guid in current_seats:
|
if s.seat_guid in current_seats:
|
||||||
s.delete() # Duplicates should not exist
|
s.delete() # Duplicates should not exist
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from dateutil.parser import parse
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||||
from pretix.base.services.tasks import ProfiledEventTask
|
from pretix.base.services.tasks import ProfiledEventTask
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.db.models import (
|
|||||||
Case, Count, DateTimeField, F, Max, OuterRef, Subquery, Sum, Value, When,
|
Case, Count, DateTimeField, F, Max, OuterRef, Subquery, Sum, Value, When,
|
||||||
)
|
)
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
|
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import timedelta
|
|||||||
import requests
|
import requests
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _, ugettext_noop
|
from django.utils.translation import gettext_lazy as _, gettext_noop
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ def send_update_notification_email():
|
|||||||
gs.settings.update_check_email,
|
gs.settings.update_check_email,
|
||||||
_('pretix update available'),
|
_('pretix update available'),
|
||||||
LazyI18nString.from_gettext(
|
LazyI18nString.from_gettext(
|
||||||
ugettext_noop(
|
gettext_noop(
|
||||||
'Hi!\n\nAn update is available for pretix or for one of the plugins you installed in your '
|
'Hi!\n\nAn update is available for pretix or for one of the plugins you installed in your '
|
||||||
'pretix installation. Please click on the following link for more information:\n\n {url} \n\n'
|
'pretix installation. Please click on the following link for more information:\n\n {url} \n\n'
|
||||||
'You can always find information on the latest updates on the pretix.eu blog:\n\n'
|
'You can always find information on the latest updates on the pretix.eu blog:\n\n'
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
|||||||
continue
|
continue
|
||||||
if wle.subevent and not wle.subevent.presale_is_running:
|
if wle.subevent and not wle.subevent.presale_is_running:
|
||||||
continue
|
continue
|
||||||
|
if not wle.item.active or (wle.variation and not wle.variation.active):
|
||||||
|
continue
|
||||||
|
|
||||||
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
|
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
|
||||||
if wle.variation
|
if wle.variation
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ from django.core.files import File
|
|||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.utils.translation import (
|
from django.utils.translation import (
|
||||||
pgettext, pgettext_lazy, ugettext_lazy as _, ugettext_noop,
|
gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
|
||||||
)
|
)
|
||||||
from django_countries import countries
|
from django_countries import countries
|
||||||
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
||||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.api.serializers.fields import ListMultipleChoiceField
|
||||||
from pretix.api.serializers.i18n import I18nField
|
from pretix.api.serializers.i18n import I18nField
|
||||||
from pretix.base.models.tax import TaxRule
|
from pretix.base.models.tax import TaxRule
|
||||||
from pretix.base.reldate import (
|
from pretix.base.reldate import (
|
||||||
@@ -104,6 +105,44 @@ DEFAULTS = {
|
|||||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
|
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'attendee_company_asked': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Ask for company per ticket"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'attendee_company_required': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Require company per ticket"),
|
||||||
|
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_company_asked'}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'attendee_addresses_asked': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Ask for postal addresses per ticket"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'attendee_addresses_required': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Require postal addresses per ticket"),
|
||||||
|
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_addresses_asked'}),
|
||||||
|
)
|
||||||
|
},
|
||||||
'order_email_asked_twice': {
|
'order_email_asked_twice': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
@@ -182,6 +221,20 @@ DEFAULTS = {
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'invoice_address_custom_field': {
|
||||||
|
'default': '',
|
||||||
|
'type': LazyI18nString,
|
||||||
|
'form_class': I18nFormField,
|
||||||
|
'serializer_class': I18nField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Custom address field"),
|
||||||
|
widget=I18nTextInput,
|
||||||
|
help_text=_("If you want to add a custom text field, e.g. for a country-specific registration number, to "
|
||||||
|
"your invoice address form, please fill in the label here. This label will both be used for "
|
||||||
|
"asking the user to input their details as well as for displaying the value on the invoice. "
|
||||||
|
"The field will not be required.")
|
||||||
|
)
|
||||||
|
},
|
||||||
'invoice_address_vatid': {
|
'invoice_address_vatid': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
@@ -337,6 +390,7 @@ DEFAULTS = {
|
|||||||
"you use slow payment methods like bank transfer, we recommend 14 days. If you only use real-time "
|
"you use slow payment methods like bank transfer, we recommend 14 days. If you only use real-time "
|
||||||
"payment methods, we recommend still setting two or three days to allow people to retry failed "
|
"payment methods, we recommend still setting two or three days to allow people to retry failed "
|
||||||
"payments."),
|
"payments."),
|
||||||
|
required=True,
|
||||||
validators=[MinValueValidator(0),
|
validators=[MinValueValidator(0),
|
||||||
MaxValueValidator(1000000)]
|
MaxValueValidator(1000000)]
|
||||||
),
|
),
|
||||||
@@ -350,7 +404,7 @@ DEFAULTS = {
|
|||||||
'type': RelativeDateWrapper,
|
'type': RelativeDateWrapper,
|
||||||
'form_class': RelativeDateField,
|
'form_class': RelativeDateField,
|
||||||
'serializer_class': SerializerRelativeDateField,
|
'serializer_class': SerializerRelativeDateField,
|
||||||
'form_kawrgs': dict(
|
'form_kwargs': dict(
|
||||||
label=_('Last date of payments'),
|
label=_('Last date of payments'),
|
||||||
help_text=_("The last date any payments are accepted. This has precedence over the number of "
|
help_text=_("The last date any payments are accepted. This has precedence over the number of "
|
||||||
"days configured above. If you use the event series feature and an order contains tickets for "
|
"days configured above. If you use the event series feature and an order contains tickets for "
|
||||||
@@ -527,6 +581,7 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.CharField,
|
'serializer_class': serializers.CharField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Domestic tax ID"),
|
label=_("Domestic tax ID"),
|
||||||
|
help_text=_("e.g. tax number in Germany, ABN in Australia, …")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'invoice_address_from_vat_id': {
|
'invoice_address_from_vat_id': {
|
||||||
@@ -623,7 +678,7 @@ DEFAULTS = {
|
|||||||
'locales': {
|
'locales': {
|
||||||
'default': json.dumps([settings.LANGUAGE_CODE]),
|
'default': json.dumps([settings.LANGUAGE_CODE]),
|
||||||
'type': list,
|
'type': list,
|
||||||
'serializer_class': serializers.MultipleChoiceField,
|
'serializer_class': ListMultipleChoiceField,
|
||||||
'serializer_kwargs': dict(
|
'serializer_kwargs': dict(
|
||||||
choices=settings.LANGUAGES,
|
choices=settings.LANGUAGES,
|
||||||
required=True,
|
required=True,
|
||||||
@@ -652,6 +707,17 @@ DEFAULTS = {
|
|||||||
label=_("Default language"),
|
label=_("Default language"),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'show_dates_on_frontpage': {
|
||||||
|
'default': 'True',
|
||||||
|
'type': bool,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Show event times and dates on the ticket shop"),
|
||||||
|
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
|
||||||
|
"does however not affect the display in other locations."),
|
||||||
|
)
|
||||||
|
},
|
||||||
'show_date_to': {
|
'show_date_to': {
|
||||||
'default': 'True',
|
'default': 'True',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
@@ -754,8 +820,8 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'form_class': forms.BooleanField,
|
'form_class': forms.BooleanField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Use feature"),
|
label=_("Allow users to download tickets"),
|
||||||
help_text=_("Use pretix to generate tickets for the user to download and print out."),
|
help_text=_("If this is off, nobody can download a ticket."),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'ticket_download_date': {
|
'ticket_download_date': {
|
||||||
@@ -776,7 +842,11 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'form_class': forms.BooleanField,
|
'form_class': forms.BooleanField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Offer to download tickets separately for add-on products"),
|
label=_("Generate tickets for add-on products"),
|
||||||
|
help_text=_('By default, tickets are only issued for products selected individually, not for add-on '
|
||||||
|
'products. With this option, a separate ticket is issued for every add-on product as well.'),
|
||||||
|
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
|
||||||
|
'data-checkbox-dependency-visual': 'on'}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'ticket_download_nonadm': {
|
'ticket_download_nonadm': {
|
||||||
@@ -785,7 +855,11 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'form_class': forms.BooleanField,
|
'form_class': forms.BooleanField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Generate tickets for non-admission products"),
|
label=_("Generate tickets for all products"),
|
||||||
|
help_text=_('If turned off, tickets are only issued for products that are marked as an "admission ticket"'
|
||||||
|
'in the product settings. You can also turn off tickt issuing in every product separately.'),
|
||||||
|
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
|
||||||
|
'data-checkbox-dependency-visual': 'on'}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'ticket_download_pending': {
|
'ticket_download_pending': {
|
||||||
@@ -794,7 +868,10 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'form_class': forms.BooleanField,
|
'form_class': forms.BooleanField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Offer to download tickets even before an order is paid"),
|
label=_("Generate tickets for pending orders"),
|
||||||
|
help_text=_('If turned off, ticket downloads are only possible after an order has been marked as paid.'),
|
||||||
|
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
|
||||||
|
'data-checkbox-dependency-visual': 'on'}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'event_list_availability': {
|
'event_list_availability': {
|
||||||
@@ -879,6 +956,66 @@ DEFAULTS = {
|
|||||||
label=_("Keep a percentual cancellation fee"),
|
label=_("Keep a percentual cancellation fee"),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'cancel_allow_user_paid_adjust_fees': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Allow customers to voluntarily choose a lower refund"),
|
||||||
|
help_text=_("With this option enabled, your customers can choose to get a smaller refund to support you.")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'cancel_allow_user_paid_adjust_fees_explanation': {
|
||||||
|
'default': LazyI18nString.from_gettext(gettext_noop(
|
||||||
|
'However, if you want us to help keep the lights on here, please consider using the slider below to '
|
||||||
|
'request a smaller refund. Thank you!'
|
||||||
|
)),
|
||||||
|
'type': LazyI18nString,
|
||||||
|
'serializer_class': I18nField,
|
||||||
|
'form_class': I18nFormField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Voluntary lower refund explanation"),
|
||||||
|
widget=I18nTextarea,
|
||||||
|
widget_kwargs={'attrs': {'rows': '2'}},
|
||||||
|
help_text=_("This text will be shown in between the explanation of how the refunds work and the slider "
|
||||||
|
"which your customers can use to choose the amount they would like to receive. You can use it "
|
||||||
|
"e.g. to explain choosing a lower refund will help your organisation.")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'cancel_allow_user_paid_require_approval': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Customers can only request a cancellation that needs to be approved by the event organizer "
|
||||||
|
"before the order is canceled and a refund is issued."),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'cancel_allow_user_paid_refund_as_giftcard': {
|
||||||
|
'default': 'off',
|
||||||
|
'type': str,
|
||||||
|
'serializer_class': serializers.ChoiceField,
|
||||||
|
'serializer_kwargs': dict(
|
||||||
|
choices=[
|
||||||
|
('off', _('All refunds are issued to the original payment method')),
|
||||||
|
('option', _('Customers can choose between a gift card and a refund to their payment method')),
|
||||||
|
('force', _('All refunds are issued as gift cards')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'form_class': forms.ChoiceField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_('Refund method'),
|
||||||
|
choices=[
|
||||||
|
('off', _('All refunds are issued to the original payment method')),
|
||||||
|
('option', _('Customers can choose between a gift card and a refund to their payment method')),
|
||||||
|
('force', _('All refunds are issued as gift cards')),
|
||||||
|
],
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
# When adding a new ordering, remember to also define it in the event model
|
||||||
|
)
|
||||||
|
},
|
||||||
'cancel_allow_user_paid_until': {
|
'cancel_allow_user_paid_until': {
|
||||||
'default': None,
|
'default': None,
|
||||||
'type': RelativeDateWrapper,
|
'type': RelativeDateWrapper,
|
||||||
@@ -978,7 +1115,7 @@ DEFAULTS = {
|
|||||||
},
|
},
|
||||||
'mail_text_resend_link': {
|
'mail_text_resend_link': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
you receive this message because you asked us to send you the link
|
you receive this message because you asked us to send you the link
|
||||||
to your order for {event}.
|
to your order for {event}.
|
||||||
@@ -991,7 +1128,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_resend_all_links': {
|
'mail_text_resend_all_links': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
somebody requested a list of your orders for {event}.
|
somebody requested a list of your orders for {event}.
|
||||||
The list is as follows:
|
The list is as follows:
|
||||||
@@ -1003,7 +1140,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_free_attendee': {
|
'mail_text_order_free_attendee': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
|
||||||
|
|
||||||
you have been registered for {event} successfully.
|
you have been registered for {event} successfully.
|
||||||
|
|
||||||
@@ -1015,7 +1152,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_free': {
|
'mail_text_order_free': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
your order for {event} was successful. As you only ordered free products,
|
your order for {event} was successful. As you only ordered free products,
|
||||||
no payment is required.
|
no payment is required.
|
||||||
@@ -1032,7 +1169,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_placed_require_approval': {
|
'mail_text_order_placed_require_approval': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
we successfully received your order for {event}. Since you ordered
|
we successfully received your order for {event}. Since you ordered
|
||||||
a product that requires approval by the event organizer, we ask you to
|
a product that requires approval by the event organizer, we ask you to
|
||||||
@@ -1046,7 +1183,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_placed': {
|
'mail_text_order_placed': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
we successfully received your order for {event} with a total value
|
we successfully received your order for {event} with a total value
|
||||||
of {total_with_currency}. Please complete your payment before {expire_date}.
|
of {total_with_currency}. Please complete your payment before {expire_date}.
|
||||||
@@ -1065,7 +1202,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_placed_attendee': {
|
'mail_text_order_placed_attendee': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
|
||||||
|
|
||||||
a ticket for {event} has been ordered for you.
|
a ticket for {event} has been ordered for you.
|
||||||
|
|
||||||
@@ -1077,7 +1214,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_changed': {
|
'mail_text_order_changed': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
your order for {event} has been changed.
|
your order for {event} has been changed.
|
||||||
|
|
||||||
@@ -1089,7 +1226,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_paid': {
|
'mail_text_order_paid': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
we successfully received your payment for {event}. Thank you!
|
we successfully received your payment for {event}. Thank you!
|
||||||
|
|
||||||
@@ -1107,7 +1244,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_paid_attendee': {
|
'mail_text_order_paid_attendee': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
|
||||||
|
|
||||||
a ticket for {event} that has been ordered for you is now paid.
|
a ticket for {event} that has been ordered for you is now paid.
|
||||||
|
|
||||||
@@ -1123,7 +1260,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_expire_warning': {
|
'mail_text_order_expire_warning': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
we did not yet receive a full payment for your order for {event}.
|
we did not yet receive a full payment for your order for {event}.
|
||||||
Please keep in mind that we only guarantee your order if we receive
|
Please keep in mind that we only guarantee your order if we receive
|
||||||
@@ -1137,7 +1274,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_waiting_list': {
|
'mail_text_waiting_list': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
you submitted yourself to the waiting list for {event},
|
you submitted yourself to the waiting list for {event},
|
||||||
for the product {product}.
|
for the product {product}.
|
||||||
@@ -1160,7 +1297,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_canceled': {
|
'mail_text_order_canceled': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
your order {code} for {event} has been canceled.
|
your order {code} for {event} has been canceled.
|
||||||
|
|
||||||
@@ -1172,7 +1309,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_approved': {
|
'mail_text_order_approved': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
we approved your order for {event} and will be happy to welcome you
|
we approved your order for {event} and will be happy to welcome you
|
||||||
at our event.
|
at our event.
|
||||||
@@ -1188,7 +1325,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_denied': {
|
'mail_text_order_denied': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
unfortunately, we denied your order request for {event}.
|
unfortunately, we denied your order request for {event}.
|
||||||
|
|
||||||
@@ -1203,7 +1340,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_order_custom_mail': {
|
'mail_text_order_custom_mail': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
You can change your order details and view the status of your order at
|
You can change your order details and view the status of your order at
|
||||||
{url}
|
{url}
|
||||||
@@ -1221,7 +1358,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_download_reminder_attendee': {
|
'mail_text_download_reminder_attendee': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
|
||||||
|
|
||||||
you are registered for {event}.
|
you are registered for {event}.
|
||||||
|
|
||||||
@@ -1233,7 +1370,7 @@ Your {event} team"""))
|
|||||||
},
|
},
|
||||||
'mail_text_download_reminder': {
|
'mail_text_download_reminder': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||||
|
|
||||||
you bought a ticket for {event}.
|
you bought a ticket for {event}.
|
||||||
|
|
||||||
@@ -1283,6 +1420,14 @@ Your {event} team"""))
|
|||||||
'default': '#D36060',
|
'default': '#D36060',
|
||||||
'type': str
|
'type': str
|
||||||
},
|
},
|
||||||
|
'theme_color_background': {
|
||||||
|
'default': '#FFFFFF',
|
||||||
|
'type': str
|
||||||
|
},
|
||||||
|
'theme_round_borders': {
|
||||||
|
'default': 'True',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
'primary_font': {
|
'primary_font': {
|
||||||
'default': 'Open Sans',
|
'default': 'Open Sans',
|
||||||
'type': str
|
'type': str
|
||||||
@@ -1307,6 +1452,22 @@ Your {event} team"""))
|
|||||||
'default': None,
|
'default': None,
|
||||||
'type': File
|
'type': File
|
||||||
},
|
},
|
||||||
|
'logo_image_large': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
|
'logo_show_title': {
|
||||||
|
'default': 'True',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
|
'organizer_logo_image': {
|
||||||
|
'default': None,
|
||||||
|
'type': File
|
||||||
|
},
|
||||||
|
'organizer_logo_image_large': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
'og_image': {
|
'og_image': {
|
||||||
'default': None,
|
'default': None,
|
||||||
'type': File
|
'type': File
|
||||||
@@ -1325,6 +1486,32 @@ Your {event} team"""))
|
|||||||
widget=I18nTextarea
|
widget=I18nTextarea
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'banner_text': {
|
||||||
|
'default': '',
|
||||||
|
'type': LazyI18nString,
|
||||||
|
'serializer_class': I18nField,
|
||||||
|
'form_class': I18nFormField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Banner text (top)"),
|
||||||
|
widget=I18nTextarea,
|
||||||
|
widget_kwargs={'attrs': {'rows': '2'}},
|
||||||
|
help_text=_("This text will be shown above every page of your shop. Please only use this for "
|
||||||
|
"very important messages.")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'banner_text_bottom': {
|
||||||
|
'default': '',
|
||||||
|
'type': LazyI18nString,
|
||||||
|
'serializer_class': I18nField,
|
||||||
|
'form_class': I18nFormField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Banner text (bottom)"),
|
||||||
|
widget=I18nTextarea,
|
||||||
|
widget_kwargs={'attrs': {'rows': '2'}},
|
||||||
|
help_text=_("This text will be shown below every page of your shop. Please only use this for "
|
||||||
|
"very important messages.")
|
||||||
|
)
|
||||||
|
},
|
||||||
'voucher_explanation_text': {
|
'voucher_explanation_text': {
|
||||||
'default': '',
|
'default': '',
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
@@ -1339,7 +1526,7 @@ Your {event} team"""))
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
'checkout_email_helptext': {
|
'checkout_email_helptext': {
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop(
|
'default': LazyI18nString.from_gettext(gettext_noop(
|
||||||
'Make sure to enter a valid email address. We will send you an order '
|
'Make sure to enter a valid email address. We will send you an order '
|
||||||
'confirmation including a link that you need to access your order later.'
|
'confirmation including a link that you need to access your order later.'
|
||||||
)),
|
)),
|
||||||
@@ -1733,7 +1920,7 @@ def validate_settings(event, settings_dict):
|
|||||||
})
|
})
|
||||||
if settings_dict.get('invoice_address_company_required') and not settings_dict.get('invoice_address_required'):
|
if settings_dict.get('invoice_address_company_required') and not settings_dict.get('invoice_address_required'):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'invoice_address_company_requred': _('You have to require invoice addresses to require for company names.')
|
'invoice_address_company_required': _('You have to require invoice addresses to require for company names.')
|
||||||
})
|
})
|
||||||
|
|
||||||
payment_term_last = settings_dict.get('payment_term_last')
|
payment_term_last = settings_dict.get('payment_term_last')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.db.models import Max, Q
|
|||||||
from django.db.models.functions import Greatest
|
from django.db.models.functions import Greatest
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
AnswerSerializer, InvoiceAddressSerializer,
|
AnswerSerializer, InvoiceAddressSerializer,
|
||||||
@@ -194,15 +194,23 @@ class WaitingListShredder(BaseDataShredder):
|
|||||||
le.save(update_fields=['data', 'shredded'])
|
le.save(update_fields=['data', 'shredded'])
|
||||||
|
|
||||||
|
|
||||||
class AttendeeNameShredder(BaseDataShredder):
|
class AttendeeInfoShredder(BaseDataShredder):
|
||||||
verbose_name = _('Attendee names')
|
verbose_name = _('Attendee info')
|
||||||
identifier = 'attendee_names'
|
identifier = 'attendee_info'
|
||||||
description = _('This will remove all attendee names from order positions, as well as logged changes to them.')
|
description = _('This will remove all attendee names and postal addresses from order positions, as well as logged '
|
||||||
|
'changes to them.')
|
||||||
|
|
||||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||||
yield 'attendee-names.json', 'application/json', json.dumps({
|
yield 'attendee-info.json', 'application/json', json.dumps({
|
||||||
'{}-{}'.format(op.order.code, op.positionid): op.attendee_name
|
'{}-{}'.format(op.order.code, op.positionid): {
|
||||||
for op in OrderPosition.all.filter(
|
'name': op.attendee_name,
|
||||||
|
'company': op.company,
|
||||||
|
'street': op.street,
|
||||||
|
'zipcode': op.zipcode,
|
||||||
|
'city': op.city,
|
||||||
|
'country': str(op.country) if op.country else None,
|
||||||
|
'state': op.state
|
||||||
|
} for op in OrderPosition.all.filter(
|
||||||
order__event=self.event
|
order__event=self.event
|
||||||
).filter(
|
).filter(
|
||||||
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
|
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
|
||||||
@@ -214,8 +222,10 @@ class AttendeeNameShredder(BaseDataShredder):
|
|||||||
OrderPosition.all.filter(
|
OrderPosition.all.filter(
|
||||||
order__event=self.event
|
order__event=self.event
|
||||||
).filter(
|
).filter(
|
||||||
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
|
Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False) |
|
||||||
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True})
|
Q(company__isnull=False) | Q(street__isnull=False) | Q(zipcode__isnull=False) | Q(city__isnull=False)
|
||||||
|
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}, company=None, street=None,
|
||||||
|
zipcode=None, city=None)
|
||||||
|
|
||||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
|
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
|
||||||
d = le.parsed_data
|
d = le.parsed_data
|
||||||
@@ -227,6 +237,14 @@ class AttendeeNameShredder(BaseDataShredder):
|
|||||||
d['data'][i]['attendee_name_parts'] = {
|
d['data'][i]['attendee_name_parts'] = {
|
||||||
'_legacy': '█'
|
'_legacy': '█'
|
||||||
}
|
}
|
||||||
|
if 'company' in row:
|
||||||
|
d['data'][i]['company'] = '█'
|
||||||
|
if 'street' in row:
|
||||||
|
d['data'][i]['street'] = '█'
|
||||||
|
if 'zipcode' in row:
|
||||||
|
d['data'][i]['zipcode'] = '█'
|
||||||
|
if 'city' in row:
|
||||||
|
d['data'][i]['city'] = '█'
|
||||||
le.data = json.dumps(d)
|
le.data = json.dumps(d)
|
||||||
le.shredded = True
|
le.shredded = True
|
||||||
le.save(update_fields=['data', 'shredded'])
|
le.save(update_fields=['data', 'shredded'])
|
||||||
@@ -357,7 +375,7 @@ class PaymentInfoShredder(BaseDataShredder):
|
|||||||
def register_payment_provider(sender, **kwargs):
|
def register_payment_provider(sender, **kwargs):
|
||||||
return [
|
return [
|
||||||
EmailAddressShredder,
|
EmailAddressShredder,
|
||||||
AttendeeNameShredder,
|
AttendeeInfoShredder,
|
||||||
InvoiceAddressShredder,
|
InvoiceAddressShredder,
|
||||||
QuestionAnswerShredder,
|
QuestionAnswerShredder,
|
||||||
InvoiceShredder,
|
InvoiceShredder,
|
||||||
|
|||||||
@@ -341,6 +341,16 @@ as the first argument.
|
|||||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
order_reactivated = EventPluginSignal(
|
||||||
|
providing_args=["order"]
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
This signal is sent out every time a canceled order is reactivated. The order object is given
|
||||||
|
as the first argument.
|
||||||
|
|
||||||
|
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
|
"""
|
||||||
|
|
||||||
order_expired = EventPluginSignal(
|
order_expired = EventPluginSignal(
|
||||||
providing_args=["order"]
|
providing_args=["order"]
|
||||||
)
|
)
|
||||||
@@ -472,7 +482,7 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
event_copy_data = EventPluginSignal(
|
event_copy_data = EventPluginSignal(
|
||||||
providing_args=["other", "tax_map", "category_map", "item_map", "question_map", "variation_map"]
|
providing_args=["other", "tax_map", "category_map", "item_map", "question_map", "variation_map", "checkin_list_map"]
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
This signal is sent out when a new event is created as a clone of an existing event, i.e.
|
This signal is sent out when a new event is created as a clone of an existing event, i.e.
|
||||||
@@ -484,9 +494,9 @@ but you might need to modify that data.
|
|||||||
|
|
||||||
The ``sender`` keyword argument will contain the event of the **new** event. The ``other``
|
The ``sender`` keyword argument will contain the event of the **new** event. The ``other``
|
||||||
keyword argument will contain the event to **copy from**. The keyword arguments
|
keyword argument will contain the event to **copy from**. The keyword arguments
|
||||||
``tax_map``, ``category_map``, ``item_map``, ``question_map``, and ``variation_map`` contain
|
``tax_map``, ``category_map``, ``item_map``, ``question_map``, ``variation_map`` and
|
||||||
mappings from object IDs in the original event to objects in the new event of the respective
|
``checkin_list_map`` contain mappings from object IDs in the original event to objects
|
||||||
types.
|
in the new event of the respective types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
item_copy_data = EventPluginSignal(
|
item_copy_data = EventPluginSignal(
|
||||||
@@ -517,7 +527,7 @@ an OrderedDict of (setting name, form field).
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
order_fee_calculation = EventPluginSignal(
|
order_fee_calculation = EventPluginSignal(
|
||||||
providing_args=['positions', 'invoice_address', 'meta_info', 'total']
|
providing_args=['positions', 'invoice_address', 'meta_info', 'total', 'gift_cards']
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
This signals allows you to add fees to an order while it is being created. You are expected to
|
This signals allows you to add fees to an order while it is being created. You are expected to
|
||||||
@@ -528,7 +538,8 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
|||||||
argument will contain the cart positions and ``invoice_address`` the invoice address (useful for
|
argument will contain the cart positions and ``invoice_address`` the invoice address (useful for
|
||||||
tax calculation). The argument ``meta_info`` contains the order's meta dictionary. The ``total``
|
tax calculation). The argument ``meta_info`` contains the order's meta dictionary. The ``total``
|
||||||
keyword argument will contain the total cart sum without any fees. You should not rely on this
|
keyword argument will contain the total cart sum without any fees. You should not rely on this
|
||||||
``total`` value for fee calculations as other fees might interfere.
|
``total`` value for fee calculations as other fees might interfere. The ``gift_cards`` argument lists
|
||||||
|
the gift cards in use.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
order_fee_type_name = EventPluginSignal(
|
order_fee_type_name = EventPluginSignal(
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{% load rich_text %}
|
||||||
|
{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} {{ widget.label|rich_text_snippet }}</label>{% endif %}
|
||||||
@@ -7,24 +7,28 @@ from django import template
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
ALLOWED_TAGS = [
|
ALLOWED_TAGS_SNIPPET = [
|
||||||
'a',
|
'a',
|
||||||
'abbr',
|
'abbr',
|
||||||
'acronym',
|
'acronym',
|
||||||
'b',
|
'b',
|
||||||
'blockquote',
|
|
||||||
'br',
|
'br',
|
||||||
'code',
|
'code',
|
||||||
'em',
|
'em',
|
||||||
'i',
|
'i',
|
||||||
|
'strong',
|
||||||
|
'span',
|
||||||
|
# Update doc/user/markdown.rst if you change this!
|
||||||
|
]
|
||||||
|
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
|
||||||
|
'blockquote',
|
||||||
'li',
|
'li',
|
||||||
'ol',
|
'ol',
|
||||||
'strong',
|
|
||||||
'ul',
|
'ul',
|
||||||
'p',
|
'p',
|
||||||
'table',
|
'table',
|
||||||
@@ -34,7 +38,6 @@ ALLOWED_TAGS = [
|
|||||||
'td',
|
'td',
|
||||||
'th',
|
'th',
|
||||||
'div',
|
'div',
|
||||||
'span',
|
|
||||||
'hr',
|
'hr',
|
||||||
'h1',
|
'h1',
|
||||||
'h2',
|
'h2',
|
||||||
@@ -63,7 +66,7 @@ ALLOWED_PROTOCOLS = ['http', 'https', 'mailto', 'tel']
|
|||||||
|
|
||||||
def safelink_callback(attrs, new=False):
|
def safelink_callback(attrs, new=False):
|
||||||
url = attrs.get((None, 'href'), '/')
|
url = attrs.get((None, 'href'), '/')
|
||||||
if not is_safe_url(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
|
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
|
||||||
signer = signing.Signer(salt='safe-redirect')
|
signer = signing.Signer(salt='safe-redirect')
|
||||||
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||||
attrs[None, 'target'] = '_blank'
|
attrs[None, 'target'] = '_blank'
|
||||||
@@ -95,7 +98,8 @@ def markdown_compile_email(source):
|
|||||||
), parse_email=True)
|
), parse_email=True)
|
||||||
|
|
||||||
|
|
||||||
def markdown_compile(source):
|
def markdown_compile(source, snippet=False):
|
||||||
|
tags = ALLOWED_TAGS_SNIPPET if snippet else ALLOWED_TAGS
|
||||||
return bleach.clean(
|
return bleach.clean(
|
||||||
markdown.markdown(
|
markdown.markdown(
|
||||||
source,
|
source,
|
||||||
@@ -104,7 +108,8 @@ def markdown_compile(source):
|
|||||||
'markdown.extensions.nl2br'
|
'markdown.extensions.nl2br'
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
tags=ALLOWED_TAGS,
|
strip=snippet,
|
||||||
|
tags=tags,
|
||||||
attributes=ALLOWED_ATTRIBUTES,
|
attributes=ALLOWED_ATTRIBUTES,
|
||||||
protocols=ALLOWED_PROTOCOLS,
|
protocols=ALLOWED_PROTOCOLS,
|
||||||
)
|
)
|
||||||
@@ -122,3 +127,17 @@ def rich_text(text: str, **kwargs):
|
|||||||
parse_email=True
|
parse_email=True
|
||||||
)
|
)
|
||||||
return mark_safe(body_md)
|
return mark_safe(body_md)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def rich_text_snippet(text: str, **kwargs):
|
||||||
|
"""
|
||||||
|
Processes markdown and cleans HTML in a text input.
|
||||||
|
"""
|
||||||
|
text = str(text)
|
||||||
|
body_md = bleach.linkify(
|
||||||
|
markdown_compile(text, snippet=True),
|
||||||
|
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
|
||||||
|
parse_email=True
|
||||||
|
)
|
||||||
|
return mark_safe(body_md)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from zipfile import ZipFile
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import Event, Order, OrderPosition
|
from pretix.base.models import Event, Order, OrderPosition
|
||||||
from pretix.base.settings import SettingsSandbox
|
from pretix.base.settings import SettingsSandbox
|
||||||
@@ -142,7 +142,7 @@ class BaseTicketOutput:
|
|||||||
return OrderedDict([
|
return OrderedDict([
|
||||||
('_enabled',
|
('_enabled',
|
||||||
forms.BooleanField(
|
forms.BooleanField(
|
||||||
label=_('Enable output'),
|
label=_('Enable ticket format'),
|
||||||
required=False,
|
required=False,
|
||||||
)),
|
)),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ def timeline_for_event(event, subevent=None):
|
|||||||
|
|
||||||
if not event.has_subevents:
|
if not event.has_subevents:
|
||||||
days = event.settings.get('mail_days_download_reminder', as_type=int)
|
days = event.settings.get('mail_days_download_reminder', as_type=int)
|
||||||
if days is not None:
|
if days is not None and event.settings.ticket_download:
|
||||||
reminder_date = (ev.date_from - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
reminder_date = (ev.date_from - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
tl.append(TimelineEvent(
|
tl.append(TimelineEvent(
|
||||||
event=event, subevent=subevent,
|
event=event, subevent=subevent,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class BanlistValidator:
|
class BanlistValidator:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user