Compare commits

..

376 Commits

Author SHA1 Message Date
Martin Gross
451ed221d9 Settings: Add option to disable walletdetection 2023-07-11 11:05:09 +02:00
Richard Schreiber
b9af34f0fd convert walletdetection to promise with loading indicator 2023-07-05 14:18:55 +02:00
Martin Gross
fc15811b7f isort 2023-06-30 11:32:11 +02:00
Martin Gross
fdb2a20514 Add documentation 2023-06-30 10:47:28 +02:00
Martin Gross
1b1c4358d3 PProv: Implement detection of wallets such as Google Pay and Apple Pay 2023-06-30 10:21:22 +02:00
Moritz Lerch
3717c4b553 Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-06-29 13:29:28 +02:00
Moritz Lerch
609f45d818 Translations: Update German
Currently translated at 100.0% (5362 of 5362 strings)

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

powered by weblate
2023-06-29 13:29:28 +02:00
Richard Schreiber
1d49c98cf2 Widget: add lightbox for product images (Z#23123811) (#3439) 2023-06-29 12:23:00 +02:00
Richard Schreiber
586f42557f Event URLs: Add access-control-allow-origin header for redirects (#3441) 2023-06-29 11:36:50 +02:00
Raphael Michel
e3f219366d Fix crash when removing the phone number (PRETIXEU-8P0) 2023-06-29 09:58:35 +02:00
Yucheng Lin
c571b269ff Translations: Update Chinese (Traditional)
Currently translated at 100.0% (5363 of 5363 strings)

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

powered by weblate
2023-06-28 14:08:41 +02:00
Moritz Lerch
6d57501c5c Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-06-28 14:08:41 +02:00
Moritz Lerch
5f3e039b2e Translations: Update German
Currently translated at 100.0% (5362 of 5362 strings)

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

powered by weblate
2023-06-28 14:08:41 +02:00
Raphael Michel
8fa7aeef78 Markdown: Allow to escape domain name (#3430)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-06-28 14:03:53 +02:00
Raphael Michel
3b5baa7701 Order import: Fix customer column being a required column 2023-06-28 14:00:16 +02:00
Raphael Michel
c6bb3e71bf Order expiration: Allow to configure a delay in days (#3425)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-06-28 13:30:36 +02:00
Richard Schreiber
104607d34e PDF: fix normalization of unicode combination characters 2023-06-28 10:34:17 +02:00
Raphael Michel
714ef0d3b6 Order import: User lowercase email addresses 2023-06-28 09:13:36 +02:00
robbi5
db7c52ca93 Add OS name and version to stored device information (#3434)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-06-28 09:02:07 +02:00
Raphael Michel
fc94fbd9c8 Dockerfile: Remove broken npm installation line 2023-06-27 23:20:06 +02:00
Raphael Michel
61b3207ea2 Bump to 2023.7.0.dev0 2023-06-27 22:53:14 +02:00
Raphael Michel
ccf17db972 Bump to 2023.6.0 2023-06-27 22:48:28 +02:00
Raphael Michel
456bee7efa Order import: Allow to assign a customer 2023-06-27 17:09:09 +02:00
Raphael Michel
ccfdd364a3 Translations: Update German
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel
cf92988eae Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel
6c561b1908 Translations: Update German
Currently translated at 100.0% (5362 of 5362 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel
5634a16a85 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel
6883ae268f Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel
f75f8dead6 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel
0b28df8b83 Translations: Update German
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Yucheng Lin
0ffffc6a51 Translations: Update Chinese (Traditional)
Currently translated at 100.0% (5353 of 5353 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
M C
3f95f06845 Translations: Update Italian
Currently translated at 83.8% (177 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
M C
22bb4a9ac4 Translations: Update Italian
Currently translated at 19.0% (1020 of 5353 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel
ee50ee8e99 Translations: Extend wordlist 2023-06-27 16:53:21 +02:00
Raphael Michel
63a6b17229 Loosen version constraint on importlib_metadata 2023-06-27 15:09:07 +02:00
Raphael Michel
f33153ef01 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-06-27 14:51:54 +02:00
Raphael Michel
09517837ba IdempotencyMiddleware: Require a durable transaction 2023-06-27 13:16:04 +02:00
Raphael Michel
0f9ec8beca API: Expose TaxRule.custom_rules (#3426) 2023-06-27 13:05:54 +02:00
Raphael Michel
6d604889f2 Translations: Update Chinese (Traditional)
Currently translated at 99.9% (5351 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Raphael Michel
f9da500c06 Translations: Update Chinese (Traditional)
Currently translated at 99.9% (5351 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Jonathan Berger
8f3b92a5b4 Translations: Update French
Currently translated at 99.9% (5348 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin
c82aa891e6 Translations: Update Chinese (Traditional)
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin
591ff61d1b Translations: Update Chinese (Traditional)
Currently translated at 100.0% (5353 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
M C
af3ba16631 Translations: Update Italian
Currently translated at 18.7% (1006 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Raphael Michel
dce0bba707 Translations: Update Chinese (Traditional)
Currently translated at 88.8% (4754 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin
0a942a670f Translations: Update Chinese (Traditional)
Currently translated at 88.8% (4754 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin
310b1f50bc Translations: Update Chinese (Traditional)
Currently translated at 87.8% (4703 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin
0cef7029e1 Translations: Update Chinese (Traditional)
Currently translated at 87.2% (4672 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin
fbc2a4cdc2 Translations: Update Chinese (Traditional)
Currently translated at 84.4% (4519 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Ronan LE MEILLAT
2daf6f6d97 Translations: Update French
Currently translated at 99.9% (5348 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
cpoisnel
1fe80fa8c5 Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Maurice Kaag
fa0b31b19f Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Ronan LE MEILLAT
3a77eeaa91 Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Jonathan Berger
a1faa66ecd Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Raphael Michel
1e458d21f9 Data shredder: Add log entries 2023-06-27 09:34:39 +02:00
Raphael Michel
d1a051544f Bump celery to 5.3 (#3433)
also fixes #3070
2023-06-26 12:47:07 +02:00
Raphael Michel
8bd4ddcd0d Add timeout for SMTP connections 2023-06-26 12:36:08 +02:00
Raphael Michel
59a16789ea CartManager: Fix crash PRETIXEU-8NF 2023-06-26 11:12:13 +02:00
Raphael Michel
f4ce3654bb Data shredder: Add missing data-asynctask-long 2023-06-26 09:37:59 +02:00
Raphael Michel
3ad99d8239 Event deletion: Delete failed checkins 2023-06-26 09:37:51 +02:00
Raphael Michel
b415393ccf Data shredder optimizations (#3429)
Co-authored-by: Martin Gross <gross@rami.io>
2023-06-23 16:56:19 +02:00
Raphael Michel
84dbd93d9e Translations: Update Chinese (Traditional)
Currently translated at 84.3% (4515 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Raphael Michel
5a4f990ab9 Translations: Update Chinese (Traditional)
Currently translated at 84.3% (4516 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin
35f3d95a46 Translations: Update Chinese (Traditional)
Currently translated at 84.4% (4518 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT
c729b71320 Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT
8eb7c8db9e Translations: Update French
Currently translated at 99.0% (5300 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin
d5609f6ab0 Translations: Update Chinese (Traditional)
Currently translated at 81.7% (4376 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin
5d8fa31bdf Translations: Update Chinese (Traditional)
Currently translated at 81.7% (4374 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Raphael Michel
9360b1fd90 Translations: Update Chinese (Traditional)
Currently translated at 81.6% (4372 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT
51da6570bf Translations: Update French
Currently translated at 97.3% (5212 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin
fbdbddd555 Translations: Update Chinese (Traditional)
Currently translated at 81.7% (4374 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT
eb3edd83b8 Translations: Update French
Currently translated at 94.7% (5071 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT
25f5fe54a9 Translations: Update French
Currently translated at 93.8% (5023 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT
7bf153bb3b Translations: Update French
Currently translated at 92.7% (4963 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin
48e64071a1 Translations: Update Chinese (Traditional)
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin
95ea4fd4c9 Translations: Update Chinese (Traditional)
Currently translated at 81.6% (4371 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Raphael Michel
206b57adfd Revert "Markdown: Allow to escape domain name"
This reverts commit b7f3f7a7a1.
2023-06-23 15:32:16 +02:00
Raphael Michel
b7f3f7a7a1 Markdown: Allow to escape domain name 2023-06-23 15:32:00 +02:00
Raphael Michel
34e7a0fc31 PDF renderer: Fix crash while embedding iamge (PRETIXEU-8MY) 2023-06-23 11:51:23 +02:00
Raphael Michel
cc7f249cb8 Fix crash if a tax rule on a fee prevents sale (PRETIXEU-8MZ) 2023-06-23 11:49:09 +02:00
Raphael Michel
147061eaa4 Fix issue in middleware after organizer deletion (PRETIXEU-8N3) 2023-06-23 11:25:55 +02:00
Raphael Michel
c16491889b CSS generation: Compress cached result with gzip to save redis memory 2023-06-22 12:35:34 +02:00
Raphael Michel
1eb1d8df5f Check-in export: Fix filter options 2023-06-22 09:04:05 +02:00
Raphael Michel
3f47cf785c Teams: Allow admin user to delete the last team 2023-06-21 16:51:53 +02:00
Raphael Michel
e8859cb2e2 Bank transfer: Fix reference missing for non-SEPA accounts 2023-06-21 15:25:04 +02:00
Raphael Michel
61ab6f729d Add webhooks for waiting list events (#3423)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-06-21 14:17:41 +02:00
Raphael Michel
79c9ba3cf3 Check-in list export: ALlow to filter by status (#3424) 2023-06-21 14:03:37 +02:00
Raphael Michel
1d86f7a0c3 Bank transfer: Do not use <pre> for bank details in emails (#3413) 2023-06-19 12:45:14 +02:00
Yucheng Lin
e259b3994a Translations: Update Chinese (Traditional)
Currently translated at 78.6% (4210 of 5353 strings)

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

powered by weblate
2023-06-19 11:42:11 +02:00
Ronan LE MEILLAT
18e97624fd Translations: Update French
Currently translated at 49.5% (2652 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 64.9% (137 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 64.9% (137 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 49.5% (2653 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 71.5% (151 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 71.5% (151 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 49.5% (2655 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 86.7% (183 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 86.7% (183 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 90.9% (192 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 90.9% (192 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 49.6% (2658 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 93.8% (198 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 93.8% (198 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 49.8% (2666 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 50.4% (2699 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.3% (2800 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.5% (2811 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.5% (2811 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.5% (2811 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.5% (2811 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 54.5% (2920 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 55.3% (2963 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 57.4% (3077 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 57.9% (3102 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 57.9% (3102 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 60.2% (3225 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 61.0% (3269 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 61.2% (3281 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 61.9% (3316 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 62.6% (3353 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 63.6% (3405 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 63.8% (3420 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 66.7% (3572 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 69.1% (3703 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 71.2% (3812 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 71.9% (3851 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 72.5% (3882 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 73.7% (3946 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 73.7% (3947 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 90.3% (4839 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 90.3% (4839 of 5353 strings)

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

powered by weblate
2023-06-19 11:42:11 +02:00
Raphael Michel
1c9a245231 Extend wordlist 2023-06-19 11:31:25 +02:00
Raphael Michel
b51ca58820 Add BaseExporter.available_for_user() 2023-06-16 17:35:36 +02:00
Raphael Michel
7a48cac862 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-06-16 16:35:40 +02:00
Raphael Michel
1bdcc4580e Quick setup: Fix translation of default values 2023-06-16 16:04:25 +02:00
Raphael Michel
dd10bdd433 Shredder: Fix redirect to broken page on error 2023-06-16 15:58:38 +02:00
Raphael Michel
f7a74c2e74 Simple email layout: Remove margin of last paragraph 2023-06-16 15:46:29 +02:00
Raphael Michel
4037e1886d Mail settings: Fix missing texts for preview 2023-06-16 15:42:21 +02:00
Raphael Michel
c4ae363fdb Use hard line breaks in all default email texts 2023-06-16 15:38:46 +02:00
Raphael Michel
3df64a46e7 Rich text: Support intentional newlines in emails 2023-06-16 15:16:20 +02:00
Raphael Michel
69502986ad Email renderers: Allow line breaks in <pre> 2023-06-16 15:05:59 +02:00
Raphael Michel
51ea63335c Email renderers: Unify some CSS details 2023-06-16 15:05:57 +02:00
Raphael Michel
dc76b554f8 Simple email layout: Add missing line 2023-06-16 14:57:05 +02:00
Raphael Michel
f8be8296dd Gift cards: Improved support for cross-organizer acceptance (#3311)
Co-authored-by: Martin Gross <martin@pc-coholic.de>
2023-06-15 14:17:40 +02:00
Yucheng Lin
b3c917925c Translations: Update Chinese (Traditional)
Currently translated at 78.1% (4156 of 5319 strings)

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

powered by weblate
2023-06-15 13:48:35 +02:00
Yucheng Lin
4954373a04 Translations: Update Chinese (Traditional)
Currently translated at 78.0% (4151 of 5319 strings)

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

powered by weblate
2023-06-15 13:48:35 +02:00
Yucheng Lin
5571ec3858 Translations: Update Chinese (Traditional)
Currently translated at 77.9% (4146 of 5319 strings)

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

powered by weblate
2023-06-15 13:48:35 +02:00
hmontheline
9ef3139905 Translations: Update Spanish
Currently translated at 55.4% (2951 of 5319 strings)

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

powered by weblate
2023-06-15 13:48:35 +02:00
Fabian
3139b9fe6f Docs: update requirements and links 2023-06-15 13:21:22 +02:00
Martin Gross
437d33ba79 Expose SubEvent-PK in SubEvent Overview List (#3410) 2023-06-15 10:57:53 +02:00
Raphael Michel
0a9890b1b0 Transaction list export: Add count * price column 2023-06-14 11:52:36 +02:00
Raphael Michel
1420ad43db Grammar fix in backend warning message 2023-06-13 22:06:18 +02:00
Raphael Michel
30da7a6429 Order expert search: Allow to filter by check-in/check-out 2023-06-13 21:56:47 +02:00
Raphael Michel
a2f3dcce02 Do not allow to generate invoice for expired or canceled order 2023-06-13 15:56:18 +02:00
Raphael Michel
41f5ca3f9d Translations: Update Chinese (Traditional)
Currently translated at 77.9% (4145 of 5319 strings)

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

powered by weblate
2023-06-13 15:07:37 +02:00
Yucheng Lin
817f1e0371 Translations: Update Chinese (Traditional)
Currently translated at 77.9% (4145 of 5319 strings)

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

powered by weblate
2023-06-13 15:07:37 +02:00
Martin Gross
35fc001768 Add binary_file to SettingsSandbox get() (#3407) 2023-06-13 14:58:36 +02:00
Raphael Michel
002416e435 Add check-in simulator (#3380) 2023-06-13 14:57:24 +02:00
dependabot[bot]
4917249bab Update requests requirement from ==2.30.* to ==2.31.* (#3399)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 10:29:29 +02:00
Martin Gross
afd2468375 Add ePayBL documentation (#3397) 2023-06-12 10:29:06 +02:00
Raphael Michel
54d06dd7f8 Customer accounts: Validate duplicate identifier 2023-06-12 10:23:22 +02:00
Raphael Michel
5e59844cf5 Fix incorrect directory check 2023-06-12 10:13:49 +02:00
Raphael Michel
0d2a981674 Add dependency on pretix-plugin-build to avoid trouble 2023-06-12 09:38:17 +02:00
Raphael Michel
943aeaa31f Do not run custom build commands on other packages 2023-06-12 09:34:56 +02:00
Raphael Michel
cfe0f67f0d API: Allow to run exporter without events 2023-06-09 16:01:47 +02:00
Raphael Michel
635bb94cc4 API: Add date range filters for events and subevents 2023-06-09 15:20:53 +02:00
Raphael Michel
cf732ce173 Event dashboard: Make comment text box larger 2023-06-09 13:33:47 +02:00
Richard Schreiber
74e9a4ad2d API: add log_action/webhook for confirmed payments (#3395) 2023-06-09 09:29:32 +02:00
Raphael Michel
570357e9be Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5319 of 5319 strings)

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

powered by weblate
2023-06-07 18:04:43 +02:00
Raphael Michel
473375d4ae Translations: Update German
Currently translated at 100.0% (5319 of 5319 strings)

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

powered by weblate
2023-06-07 18:04:43 +02:00
Raphael Michel
a78b698520 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2023-06-07 17:41:44 +02:00
Thomas Vranken
332c968294 Translations: Update Dutch
Currently translated at 85.4% (4539 of 5314 strings)

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

powered by weblate
2023-06-07 17:05:02 +02:00
Raphael Michel
ad12c344c5 Translations: Add Lithuanian 2023-06-07 17:05:02 +02:00
dependabot[bot]
91c0db1ac0 Update pyjwt requirement from ==2.6.* to ==2.7.* (#3394)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-07 17:04:43 +02:00
Raphael Michel
4d231b70aa Accounting report: Fix hardcoded currency 2023-06-07 17:03:45 +02:00
Raphael Michel
ab2f6f6bed Accounting report: Allow to split by subevent, introduce sum by event 2023-06-07 16:45:28 +02:00
Richard Schreiber
28458f7b85 Cart: fix single-select checkbox button initial checked-state 2023-06-07 14:30:16 +02:00
Raphael Michel
50ff968c17 Fix #3391 -- Don't crash on GeoIP lookup failure 2023-06-06 17:12:38 +02:00
Richard Schreiber
0b4064f14f Fix: use format_lazy for formatted translation in settings (#3390) 2023-06-06 14:56:30 +02:00
Richard Schreiber
1897bd4b26 Cart: make single-select checkbox look like a button 2023-06-06 08:53:35 +02:00
dependabot[bot]
fd6843822b Update pytest-xdist requirement from ==3.2.* to ==3.3.* (#3388)
Updates the requirements on [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) to permit the latest version.
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.2.0...v3.3.1)

---
updated-dependencies:
- dependency-name: pytest-xdist
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 22:33:09 +02:00
Raphael Michel
ee1644e037 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-06-05 18:34:11 +02:00
Raphael Michel
a6c1486650 Translations: Update German
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-06-05 18:34:11 +02:00
Raphael Michel
f4b437e92b Remove MariaDB support (#3381) 2023-06-05 18:25:20 +02:00
Raphael Michel
446c55dc89 Silence deprecation warning caused by pycountry 2023-06-05 18:24:57 +02:00
Raphael Michel
0990eeeea0 Fix deprecation warning 2023-06-05 18:24:51 +02:00
Raphael Michel
591fe23a99 Invoices: Fix timezone when calculating date of cancellation 2023-06-05 15:49:39 +02:00
Raphael Michel
ad70765287 Fix event creation after Django 4.1 upgrade 2023-06-05 13:00:32 +02:00
Richard Schreiber
c59d29493c Checkout: Hide empty add-on forms and show seat above add-on form 2023-06-05 10:08:47 +02:00
Raphael Michel
bd32b33ba9 Bump Django to 4.1.* (#2989) 2023-06-05 09:56:31 +02:00
Raphael Michel
3a8556bb78 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-06-05 09:35:44 +02:00
Raphael Michel
c972d24ce7 Translations: Update German
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-06-05 09:35:44 +02:00
Yucheng Lin
647e68ef01 Translations: Update Chinese (Traditional)
Currently translated at 63.7% (3388 of 5314 strings)

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

powered by weblate
2023-06-05 09:35:44 +02:00
Yucheng Lin
f439a591df Translations: Update Chinese (Traditional)
Currently translated at 63.6% (3385 of 5314 strings)

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

powered by weblate
2023-06-05 09:35:44 +02:00
Richard Schreiber
8f17b338d1 Replace deprecated pypdf.PdfMerger with pypdf.PdfWriter (#3383) 2023-06-05 09:32:03 +02:00
Raphael Michel
35350a13d6 Fix #3360 -- Allow to revoke devices before initialized 2023-06-04 18:06:00 +02:00
Raphael Michel
0d93f7f52f Fix crash in name rendering (PRETIXEU-8GS) 2023-06-03 21:49:14 +02:00
dependabot[bot]
170dcf93e7 Update pypdf requirement from ==3.8.* to ==3.9.* (#3377)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 21:26:17 +02:00
dependabot[bot]
9319202213 Bump @babel/core from 7.21.5 to 7.22.1 in /src/pretix/static/npm_dir (#3373)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 21:25:26 +02:00
dependabot[bot]
bfd0eee2c1 Update mt-940 requirement from ==4.23.* to ==4.30.* (#3345)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2023-06-02 21:25:15 +02:00
dependabot[bot]
8570f53ed0 Update django-otp requirement from ==1.1.* to ==1.2.* (#3338)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 20:08:56 +02:00
Raphael Michel
f56f6dd628 Voucher: Add link to order in voucher history 2023-06-02 20:07:12 +02:00
Richard Schreiber
413fabd821 Product list: add border to disabled spinner buttons (#3359) 2023-06-02 20:04:42 +02:00
Julian Rother
9813e59210 API: Fix crash when creating addons with order change endpoint (#3363) 2023-06-02 20:00:40 +02:00
Richard Schreiber
d91d942eac Invoicing: Add order-code to organizer CC mail (Z#23123051) (#3370) 2023-06-02 19:59:31 +02:00
dependabot[bot]
22104f79bd Bump @rollup/plugin-node-resolve from 15.0.2 to 15.1.0 in /src/pretix/static/npm_dir (#3374)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 19:59:12 +02:00
dependabot[bot]
f289ad9e4f Bump @babel/preset-env from 7.21.5 to 7.22.4 in /src/pretix/static/npm_dir (#3375)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 19:59:00 +02:00
Raphael Michel
f81a734716 Translations: Update Chinese (Traditional)
Currently translated at 63.6% (3384 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Raphael Michel
7a27a42e79 Translations: Update Chinese (Traditional)
Currently translated at 63.6% (3384 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Yucheng Lin
65a2bab9bb Translations: Update Chinese (Traditional)
Currently translated at 63.6% (3384 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Yucheng Lin
a26f46b619 Translations: Update Chinese (Traditional)
Currently translated at 63.2% (3359 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Hans Fraiponts
5c37c85415 Translations: Update Dutch
Currently translated at 85.2% (4531 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Yucheng Lin
8ddba36690 Translations: Update Chinese (Traditional)
Currently translated at 61.6% (3275 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Thomas Vranken
f9bf05e09b Translations: Update Dutch
Currently translated at 85.2% (4530 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Raphael Michel
8471422bba Fix grammer error in settings help text 2023-06-02 19:08:21 +02:00
Raphael Michel
ee9acebe03 Devices: Fix crash in form validation 2023-06-02 17:19:25 +02:00
Raphael Michel
35d2a73f75 Voucher creation: Fix crash in validation (PRETIXEU-8GF) 2023-06-02 17:19:25 +02:00
Richard Schreiber
eb3eca45b5 Checkout/Addon: fix spinner button class name 2023-06-01 16:12:54 +02:00
Martin Gross
f7816924b0 Add Chinese (Traditional) (zh_Hant) to list of available languages. 2023-05-31 13:06:31 +02:00
Raphael Michel
12c3fef390 Docs: Add missing navigation node 2023-05-31 12:58:54 +02:00
Raphael Michel
8e39aaa292 Bump version to 4.21.0.dev0 2023-05-31 12:45:24 +02:00
Raphael Michel
ee186b283d Bump version to 4.20.0 2023-05-31 12:44:22 +02:00
Raphael Michel
87130c3f2c Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-05-31 12:43:25 +02:00
Raphael Michel
23e2fda762 Translations: Update German
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-05-31 12:43:25 +02:00
Raphael Michel
dc4e82905f Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2023-05-31 11:02:56 +02:00
Raphael Michel
6845771148 Fix failing docker build 2023-05-31 01:18:46 +02:00
pretix translation bot
c26bec93c8 Update translations (#3361)
Co-authored-by: Moritz Lerch <dev@moritz-lerch.de>
Co-authored-by: Maciej Szymczak <maciej+github@szymczak.at>
Co-authored-by: Yucheng Lin <yuchenglinedu@gmail.com>
Co-authored-by: Martin Gross <gross@rami.io>
Co-authored-by: Supaplextw <bejokeup@gmail.com>
2023-05-30 23:14:34 +02:00
Richard Schreiber
46238eb157 Export: fix timezone in event-data export
Co-authored-by: Raphael Michel <michel@rami.io>
2023-05-30 09:22:53 +02:00
Phin Wolkwitz
b3298c91c3 Event settings: Extend product metadata (Z#23116647) (#3241)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Raphael Michel <michel@rami.io>
2023-05-26 14:09:41 +02:00
Yucheng Lin
7801d06d17 Translations: Update Chinese (Traditional)
Currently translated at 40.9% (2176 of 5309 strings)

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

powered by weblate
2023-05-26 11:38:21 +02:00
Raphael Michel
9cc1d16676 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5309 of 5309 strings)

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

powered by weblate
2023-05-26 11:38:21 +02:00
Raphael Michel
8dd3ec89e0 Translations: Update German
Currently translated at 100.0% (5309 of 5309 strings)

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

powered by weblate
2023-05-26 11:38:21 +02:00
Raphael Michel
7a419f9bb5 Hide voucher redemption if the sale period is over 2023-05-26 11:30:09 +02:00
Raphael Michel
c594b6c1e5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-05-26 11:20:33 +02:00
Raphael Michel
763e811c7b Bank transfer: Update text for invoice sending 2023-05-26 11:20:03 +02:00
Raphael Michel
380dc46deb Translations: Update Chinese (Traditional)
Currently translated at 40.9% (2175 of 5305 strings)

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

powered by weblate
2023-05-26 11:19:53 +02:00
Yucheng Lin
82f6084059 Translations: Update Chinese (Traditional)
Currently translated at 41.0% (2177 of 5305 strings)

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

powered by weblate
2023-05-26 11:19:53 +02:00
Raphael Michel
9af889ad02 Questions: Warn about deleting answers 2023-05-26 11:16:50 +02:00
Raphael Michel
9869516b9c Check-in list CSV: Use check-in list name in filename 2023-05-26 11:07:02 +02:00
Raphael Michel
84180f5af4 Fix address validation for attendee data 2023-05-25 13:34:55 +02:00
Raphael Michel
cf781fc79e Voucher list: Optimize SQL query 2023-05-25 10:45:00 +02:00
Yucheng Lin
faa14a610c Translations: Update Chinese (Traditional)
Currently translated at 40.1% (2130 of 5305 strings)

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

powered by weblate
2023-05-25 10:27:32 +02:00
Raphael Michel
997deb72e1 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5305 of 5305 strings)

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

powered by weblate
2023-05-25 10:27:32 +02:00
Raphael Michel
d84998143e Translations: Update German
Currently translated at 100.0% (5305 of 5305 strings)

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

powered by weblate
2023-05-25 10:27:32 +02:00
Raphael Michel
deef067dae Translations: Update Chinese (Traditional)
Currently translated at 39.8% (2112 of 5305 strings)

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

powered by weblate
2023-05-25 10:27:32 +02:00
Yucheng Lin
8356c0f5bf Translations: Update Chinese (Traditional)
Currently translated at 39.8% (2113 of 5305 strings)

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

powered by weblate
2023-05-25 10:27:32 +02:00
Michele Pagnozzi
042e60ee1c Translations: Update Italian
Currently translated at 18.8% (1000 of 5305 strings)

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

powered by weblate
2023-05-25 10:27:32 +02:00
Raphael Michel
a3202ffc71 Voucher bulk creation: Fix vouchers being created in wrong order 2023-05-25 10:25:05 +02:00
Raphael Michel
c8ef681cc3 Event calendar: Respect voucher for availability (#3351) 2023-05-24 17:52:10 +02:00
Raphael Michel
63e4841460 Remove debug statement 2023-05-24 11:33:23 +02:00
Raphael Michel
af503d06fe Remove debug statement 2023-05-24 11:32:53 +02:00
Raphael Michel
ec24776e66 Invoice exporter: Ignore failed/canceled payments when filtering by provider 2023-05-24 10:33:00 +02:00
Raphael Michel
9a1163c65a Docs: Add note on SMTPs with rate limits 2023-05-23 14:43:34 +02:00
Raphael Michel
1237b8ba47 Invoice: Improve handling of special characters in file names (#3347)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-05-23 12:17:06 +02:00
Raphael Michel
364d86085c Invoices: Support font choice and Arabic text (#3343)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-05-23 11:35:56 +02:00
Yucheng Lin
f7d52abb0e Translations: Update Chinese (Traditional)
Currently translated at 32.3% (1716 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
158b69ddb2 Translations: Update Chinese (Traditional)
Currently translated at 32.0% (1699 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
1e1af7572a Translations: Update Chinese (Traditional)
Currently translated at 30.2% (1605 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
fe05e11f6d Translations: Update Chinese (Traditional)
Currently translated at 30.2% (1605 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
94c1bd2e7e Translations: Update Chinese (Traditional)
Currently translated at 30.1% (1597 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
fa43fb702d Translations: Update Chinese (Traditional)
Currently translated at 30.0% (1594 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
70b466971c Translations: Update Chinese (Traditional)
Currently translated at 30.0% (1594 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
d86eef66fa Translations: Update Chinese (Traditional)
Currently translated at 30.0% (1594 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
f2ce4b8feb Translations: Update Chinese (Traditional)
Currently translated at 29.9% (1591 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
c9077a0e15 Translations: Update Chinese (Traditional)
Currently translated at 29.7% (1580 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
337406b612 Translations: Update Chinese (Traditional)
Currently translated at 29.7% (1580 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
cdcd1f5cc9 Translations: Update Chinese (Traditional)
Currently translated at 29.7% (1577 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
12eca31426 Translations: Update Chinese (Traditional)
Currently translated at 29.7% (1576 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
bda9813253 Translations: Update Chinese (Traditional)
Currently translated at 29.6% (1574 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
77e96564ec Translations: Update Chinese (Traditional)
Currently translated at 29.6% (1573 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
7fdbe1edd6 Translations: Update Chinese (Traditional)
Currently translated at 29.6% (1571 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
dc91d049ea Translations: Update Chinese (Traditional)
Currently translated at 29.6% (1571 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
7a11be63ec Translations: Update Chinese (Traditional)
Currently translated at 29.5% (1570 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
5eef82f616 Translations: Update Chinese (Traditional)
Currently translated at 29.5% (1570 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
c6b00e9ad6 Translations: Update Chinese (Traditional)
Currently translated at 29.4% (1564 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
6da9111519 Translations: Update Chinese (Traditional)
Currently translated at 29.4% (1561 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
8448e0e35c Translations: Update Chinese (Traditional)
Currently translated at 29.4% (1560 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
a3aa69d203 Translations: Update Chinese (Traditional)
Currently translated at 29.3% (1559 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
2a262c78d6 Translations: Update Chinese (Traditional)
Currently translated at 29.3% (1558 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
20202d3f50 Translations: Update Chinese (Traditional)
Currently translated at 29.3% (1556 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
cdd26dabaa Translations: Update Chinese (Traditional)
Currently translated at 29.3% (1555 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
5bbde9b53d Translations: Update Chinese (Traditional)
Currently translated at 29.3% (1555 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
b7e80a5a8d Translations: Update Chinese (Traditional)
Currently translated at 29.2% (1550 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
cc9fe68aa4 Translations: Update Chinese (Traditional)
Currently translated at 29.1% (1544 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
73ea04b4c0 Translations: Update Chinese (Traditional)
Currently translated at 29.0% (1542 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
edd9e1c9c1 Translations: Update Chinese (Traditional)
Currently translated at 29.0% (1539 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
3aec3c739f Translations: Update Chinese (Traditional)
Currently translated at 28.9% (1538 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
f336a0d259 Translations: Update Chinese (Traditional)
Currently translated at 28.9% (1537 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
9fb8657e00 Translations: Update Chinese (Traditional)
Currently translated at 28.9% (1537 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
9fdc6a5f16 Translations: Update Chinese (Traditional)
Currently translated at 28.8% (1533 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
6bdc7f8b41 Translations: Update Chinese (Traditional)
Currently translated at 28.7% (1523 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Supaplextw
e7cd5a3215 Translations: Update Chinese (Traditional)
Currently translated at 28.7% (1523 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Yucheng Lin
5400a3cdea Translations: Update Chinese (Traditional)
Currently translated at 22.6% (1199 of 5305 strings)

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

powered by weblate
2023-05-22 12:05:15 +02:00
Raphael Michel
c75c080c5c Vouchers: Allow to set all addons or bundles as included (#3322)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-05-22 11:59:27 +02:00
dependabot[bot]
5eff9a86f4 Update pycryptodome requirement from ==3.17.* to ==3.18.* (#3339)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-22 11:57:31 +02:00
Raphael Michel
0f8ac3ffb3 Revert "Invoices: Support font choice and Arabic text"
This reverts commit d6f0615712.
2023-05-22 10:53:06 +02:00
Raphael Michel
d6f0615712 Invoices: Support font choice and Arabic text 2023-05-22 10:52:46 +02:00
Raphael Michel
e0524f2a03 New plugin signal order_valid_if_pending (#3337) 2023-05-19 16:09:20 +02:00
Raphael Michel
db013f5e8c Check-in lists: Fix exception in rule validation 2023-05-19 16:08:25 +02:00
Raphael Michel
1c3623b223 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5305 of 5305 strings)

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

powered by weblate
2023-05-19 14:51:43 +02:00
Raphael Michel
c9007de853 Translations: Update German
Currently translated at 100.0% (5305 of 5305 strings)

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

powered by weblate
2023-05-19 14:51:43 +02:00
Raphael Michel
a7052abb43 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-05-19 13:33:58 +02:00
Yucheng Lin
07b8555fa6 Translations: Update Chinese (Traditional)
Currently translated at 20.9% (1107 of 5290 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
Yucheng Lin
fb26229834 Translations: Update Chinese (Traditional)
Currently translated at 20.7% (1100 of 5290 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
Yucheng Lin
6a508b87f7 Translations: Update Chinese (Traditional)
Currently translated at 20.3% (1078 of 5290 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
Raphael Michel
c9c379346e Translations: Update Chinese (Traditional)
Currently translated at 20.2% (1069 of 5290 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
Yucheng Lin
19fce8b086 Translations: Update Chinese (Traditional)
Currently translated at 20.2% (1069 of 5290 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
M C
20ad0becb3 Translations: Update Italian
Currently translated at 18.7% (992 of 5290 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
Yucheng Lin
e489134bdb Translations: Update Chinese (Traditional)
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
Yucheng Lin
9dc7201f50 Translations: Update Chinese (Traditional)
Currently translated at 15.0% (796 of 5290 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
M C
376bb48686 Translations: Update Italian
Currently translated at 82.9% (175 of 211 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
M C
8f85c015fb Translations: Update Italian
Currently translated at 18.5% (982 of 5290 strings)

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

powered by weblate
2023-05-19 13:33:19 +02:00
Raphael Michel
2decf026e9 Fix missing localization of salutation 2023-05-19 10:05:38 +02:00
Raphael Michel
02b42bd7ab Check-in: Fix checking in products with add-ons through their medium 2023-05-19 09:28:19 +02:00
Raphael Michel
78d8e49990 Reports: Add new "accounting report" (#3314) 2023-05-19 09:23:34 +02:00
dependabot[bot]
0de8239348 Bump django-formtools from 2.4 to 2.4.1 (#3329)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-19 09:23:19 +02:00
dependabot[bot]
e644faf6b3 Update reportlab requirement from ==3.6.* to ==4.0.* (#3300)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 11:53:41 +02:00
Raphael Michel
8d6d0c5893 Show name including saluation in some places (Z#23121817) (#3320)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-05-17 11:53:28 +02:00
dependabot[bot]
37ba5a983b Update requests requirement from ==2.29.* to ==2.30.* (#3303)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 10:45:58 +02:00
Yucheng Lin
6fd8f9809c Translations: Update Chinese (Traditional)
Currently translated at 12.9% (686 of 5290 strings)

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

powered by weblate
2023-05-17 10:45:46 +02:00
Moritz Lerch
58dd6d7600 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5290 of 5290 strings)

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

powered by weblate
2023-05-17 10:45:46 +02:00
Moritz Lerch
6d7e585d97 Translations: Update German
Currently translated at 100.0% (5290 of 5290 strings)

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

powered by weblate
2023-05-17 10:45:46 +02:00
Raphael Michel
104c11d5dc Order search: Fix crash PRETIXEU-8F3 2023-05-16 18:07:33 +02:00
Raphael Michel
90ee435f55 Widget: Fix waiting list integration of seated events (#3323) 2023-05-16 18:07:00 +02:00
Raphael Michel
1d1f68945f Self-service order change: Respect Item.max/min_per_order (Z#23122195) (#3319)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-05-16 18:06:52 +02:00
Raphael Michel
6e4e161973 Add tests 2023-05-16 13:23:57 +02:00
Julian Rother
14fcacfb4d Fix Order._can_be_paid checks 2023-05-16 13:23:57 +02:00
Raphael Michel
676b37f9c2 Voucher redemption: Fix missing max attribute (Z#23122239) 2023-05-16 10:37:55 +02:00
Yucheng Lin
b81accf551 Translations: Update Chinese (Traditional)
Currently translated at 12.5% (666 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Supaplextw
759d13f7b6 Translations: Update Chinese (Traditional)
Currently translated at 12.5% (665 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Yucheng Lin
f73cb4cda3 Translations: Update Chinese (Traditional)
Currently translated at 12.5% (665 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Supaplextw
8376a2da23 Translations: Update Chinese (Traditional)
Currently translated at 57.8% (122 of 211 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Yucheng Lin
eeea64bd53 Translations: Update Chinese (Traditional)
Currently translated at 12.2% (646 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Supaplextw
7caa957f07 Translations: Update Chinese (Traditional)
Currently translated at 12.2% (646 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Supaplextw
5657ed8173 Translations: Update Chinese (Traditional)
Currently translated at 45.9% (97 of 211 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Supaplextw
148addaaea Translations: Update Chinese (Traditional)
Currently translated at 9.6% (509 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Raphael Michel
22d2b23b37 Translations: Update Chinese (Traditional)
Currently translated at 9.6% (509 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Yucheng Lin
959e940be7 Translations: Update Chinese (Traditional)
Currently translated at 9.6% (508 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Yucheng Lin
0ddae0ed99 Translations: Update Chinese (Traditional)
Currently translated at 8.8% (468 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Yucheng Lin
abf8c65d8b Translations: Update Chinese (Traditional)
Currently translated at 8.7% (465 of 5290 strings)

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

powered by weblate
2023-05-16 10:30:30 +02:00
Raphael Michel
069c599cff Order list: Remove sums that cause lots of confusion (#3315) 2023-05-16 10:24:54 +02:00
Richard Schreiber
e7d6bfd8b1 Fix spin-buttons when no max-attribute present (Z#23122239) (#3317) 2023-05-16 10:23:42 +02:00
Raphael Michel
4678cef32a Fix pyproject.toml wheel build issues (#3313) 2023-05-13 12:40:16 +02:00
Raphael Michel
5de3b76718 Exporters: Support "featured" flag on organizer level 2023-05-13 12:29:47 +02:00
Raphael Michel
670e22a611 ReportlabExportMixin: Dynamically adjust to leftMargin/rightMargin 2023-05-12 16:14:52 +02:00
Raphael Michel
c0419518c3 GiftCard: Add more information to transactions (#3308) 2023-05-12 09:38:35 +02:00
Raphael Michel
916ee0697f Translations: Update German
Currently translated at 100.0% (5290 of 5290 strings)

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

powered by weblate
2023-05-11 18:31:24 +02:00
Raphael Michel
813a2332eb Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5290 of 5290 strings)

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

powered by weblate
2023-05-11 18:31:24 +02:00
Raphael Michel
b4558201f5 Extend spelling wordlist 2023-05-11 18:29:03 +02:00
Raphael Michel
059cc2ab09 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-05-11 17:21:16 +02:00
Raphael Michel
e194063827 Fix isort issue 2023-05-11 14:29:52 +02:00
Raphael Michel
6ae5eecf27 Run event_view on org-level plugin views 2023-05-11 14:29:52 +02:00
Yucheng Lin
89fe3d5bd2 Translations: Update Chinese (Traditional)
Currently translated at 6.2% (329 of 5276 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Supaplextw
0de58cd213 Translations: Update Chinese (Traditional)
Currently translated at 32.2% (68 of 211 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Supaplextw
ddb9b3f445 Translations: Update Chinese (Traditional)
Currently translated at 6.0% (317 of 5276 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Richard Schreiber
96414d90d4 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5274 of 5274 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Richard Schreiber
502cb60dc5 Translations: Update German
Currently translated at 100.0% (5274 of 5274 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Yucheng Lin
d680652aa8 Translations: Update Chinese (Traditional)
Currently translated at 6.0% (317 of 5276 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Raphael Michel
0060f98233 Translations: Update Chinese (Traditional)
Currently translated at 6.0% (317 of 5276 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Supaplextw
aa3af8790c Translations: Update Chinese (Traditional)
Currently translated at 29.3% (62 of 211 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Supaplextw
814c475475 Translations: Update Chinese (Traditional)
Currently translated at 5.6% (300 of 5276 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Yucheng Lin
61526f5465 Translations: Update Chinese (Traditional)
Currently translated at 5.6% (300 of 5276 strings)

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

powered by weblate
2023-05-11 13:41:43 +02:00
Raphael Michel
19e762c9b9 Allow to highlight order code on invoice layouts (#3309) 2023-05-11 13:29:59 +02:00
Raphael Michel
1777a954a9 Add exporter for transaction data 2023-05-11 10:35:35 +02:00
Richard Schreiber
b8c7ace30e Widget: fix quantity spinner buttons after reload (#3305) 2023-05-10 17:41:58 +02:00
Raphael Michel
e153fa7227 Bank transfer: Allow to restrict to business customers 2023-05-09 18:19:25 +02:00
Richard Schreiber
232366a639 Cart: disable/enable add-to-cart button even with seating active (#3297) 2023-05-09 18:15:47 +02:00
pretix translation bot
9afaa677c4 Update translations (#3302)
Co-authored-by: M C <micasadmail@gmail.com>
Co-authored-by: Yucheng Lin <yuchenglinedu@gmail.com>
Co-authored-by: Raphael Michel <michel@rami.io>
2023-05-09 18:13:30 +02:00
Raphael Michel
bb67ecc8e6 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-05-09 17:54:53 +02:00
Raphael Michel
ce8bee5c11 Order confirmation: Fine-tune text 2023-05-09 17:54:21 +02:00
Supaplextw
abfca211b8 Translations: Update Chinese (Traditional)
Currently translated at 26.3% (55 of 209 strings)

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

powered by weblate
2023-05-09 17:54:18 +02:00
Supaplextw
60a4428ebb Translations: Update Chinese (Traditional)
Currently translated at 0.8% (44 of 5268 strings)

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

powered by weblate
2023-05-09 17:54:18 +02:00
Yucheng Lin
4058772da8 Translations: Update Chinese (Traditional)
Currently translated at 0.8% (44 of 5268 strings)

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

powered by weblate
2023-05-09 17:54:18 +02:00
Allen Wang
55b4059abe Translations: Update Chinese (Traditional)
Currently translated at 0.8% (44 of 5268 strings)

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

powered by weblate
2023-05-09 17:54:18 +02:00
Supaplextw
497cb8de06 Translations: Add Chinese (Traditional) 2023-05-09 17:54:18 +02:00
Allen Wang
0897e375c8 Translations: Add Chinese (Min Nan) 2023-05-09 17:54:18 +02:00
Allen Wang
0c81b57225 Translations: Add Chinese (Traditional) 2023-05-09 17:54:18 +02:00
Raphael Michel
6fac1aeb62 Add new gift card to orderposition relationship (#3291) 2023-05-09 09:54:46 +02:00
dependabot[bot]
996621c028 Update protobuf requirement from ==4.22.* to ==4.23.* (#3299)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-09 09:54:16 +02:00
Raphael Michel
119a2621b5 Sendmail: Optimize query 2023-05-08 18:07:34 +02:00
Richard Schreiber
85dd7a078e Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5268 of 5268 strings)

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

powered by weblate
2023-05-08 17:19:33 +02:00
Richard Schreiber
14114a6c1f Translations: Update German
Currently translated at 100.0% (5268 of 5268 strings)

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

powered by weblate
2023-05-08 17:19:33 +02:00
Richard Schreiber
f79ac05dcb Open ID: validate requested claims only if config provides them (#3296) 2023-05-08 14:22:19 +02:00
Richard Schreiber
5bacbfa9f1 Fix custom spinner-buttons missing change-event 2023-05-08 13:21:42 +02:00
Raphael Michel
c051d04462 OIDC: Fix error in URL splitting 2023-05-08 12:51:14 +02:00
Richard Schreiber
1d0eb81659 Widget & Cart: Add custom number spinners for item quantity 2023-05-08 11:38:44 +02:00
dependabot[bot]
f97effd0b7 Update sphinx requirement from ==6.2.* to ==7.0.* (#3287)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-08 10:28:12 +02:00
dependabot[bot]
4f71244e64 Update dnspython requirement from ==2.2.* to ==2.3.* (#3288)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-08 10:27:45 +02:00
Raphael Michel
d800447cd6 Fix for #3130 -- OIDC with Azure AD issues (#3222) 2023-05-08 10:27:15 +02:00
Tobias Kunze
b29686d9f2 Fix shell_scoped without shell_plus (#3292) 2023-05-04 21:09:32 +02:00
Raphael Michel
369f4b6f8d Docs: Add troubleshooting info to the mysql2postgres guide (#3289) 2023-05-04 12:50:45 +02:00
Martin Gross
11594346eb requires_approval: Do not decorate box with warning with alert-primary (Z#23121313) 2023-05-03 13:18:27 +02:00
Raphael Michel
4dc5c770e3 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5268 of 5268 strings)

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

powered by weblate
2023-05-03 10:14:01 +02:00
Raphael Michel
f4de616e73 Translations: Update German
Currently translated at 100.0% (5268 of 5268 strings)

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

powered by weblate
2023-05-03 10:14:01 +02:00
Raphael Michel
3b615fea6d Fix inconsistent naming of log messages 2023-05-03 10:03:51 +02:00
dependabot[bot]
dca0329cd1 Update django-phonenumber-field requirement from ==7.0.* to ==7.1.* (#3285)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-03 09:38:10 +02:00
Raphael Michel
0100383686 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-05-03 09:37:27 +02:00
dependabot[bot]
835788e477 Bump django-filter from 23.1 to 23.2 (#3284)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-03 09:37:22 +02:00
dependabot[bot]
298c8989f1 Bump @babel/preset-env from 7.21.4 to 7.21.5 in /src/pretix/static/npm_dir (#3282)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-03 09:36:53 +02:00
Raphael Michel
135dec81ff Waiting list: Fix description 2023-05-02 18:04:35 +02:00
Raphael Michel
2a8b6ae66a Update jQuery to 3.6.4 (#3270) 2023-05-02 11:16:06 +02:00
dependabot[bot]
e86eb345b3 Update requests requirement from ==2.28.* to ==2.29.* (#3273)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-02 10:59:10 +02:00
dependabot[bot]
050ff43a55 Update django-scopes requirement from ==1.2.* to ==2.0.* (#3272)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-02 10:58:58 +02:00
dependabot[bot]
00a77f8652 Bump @babel/core from 7.21.4 to 7.21.5 in /src/pretix/static/npm_dir (#3283)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-02 10:58:48 +02:00
Mie Frydensbjerg
6a2dc32c1d Translations: Update Danish
Currently translated at 33.1% (1742 of 5262 strings)

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

powered by weblate
2023-05-02 10:58:22 +02:00
Julian Geraerds
740dcceda7 Translations: Update Dutch
Currently translated at 86.0% (4530 of 5262 strings)

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

powered by weblate
2023-05-02 10:58:22 +02:00
Raphael Michel
3810dcd5b8 Waiting list: Optionally allow multiple entries per email (#3277)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-05-02 10:27:56 +02:00
Raphael Michel
fa4cdbfe4a Fix #3281 -- Docker build broken 2023-05-02 10:13:38 +02:00
Richard Schreiber
0e008812c3 Control/Widget: improve empty label for dates dropdown 2023-05-02 10:10:13 +02:00
Raphael Michel
418bfa8b6b Do not offer manual expiry for orders in approval process 2023-04-28 18:30:46 +02:00
Martin Gross
d080e35999 PPv2: Display control-warning also for BUYER_COMPLAINT 2023-04-28 14:37:46 +02:00
Martin Gross
b641d343d6 PPv2: Make PENDING_REVIEW payments more visible in control view 2023-04-28 13:49:12 +02:00
Martin Gross
377765e2e1 Boxoffice: Fix crash for manually confirmed ZVT-payments (Fixes
PRETIXEU-8DX)
2023-04-27 12:57:21 +02:00
Richard Schreiber
b8b5835eff Fix asynctask’s ajax success callback signature 2023-04-27 09:04:00 +02:00
Raphael Michel
4383187e36 Update .gitlab-ci.yml release script 2023-04-26 15:54:15 +02:00
Richard Schreiber
38e826724f Cart: Add sneak-peek preview (#3259) 2023-04-26 14:43:23 +02:00
Raphael Michel
6b983d5f55 Bump to 4.12.0.dev0 2023-04-26 14:38:57 +02:00
430 changed files with 259687 additions and 136758 deletions

49
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Build
on:
push:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
permissions:
contents: read # to fetch code (actions/checkout)
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ubuntu-22.04
name: Packaging
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext unzip
- name: Install Python dependencies
run: pip3 install -U setuptools build pip check-manifest
- name: Run check-manifest
run: check-manifest
- name: Run build
run: python -m build
- name: Check files
run: unzip -l dist/pretix*whl | grep node_modules || exit 1

View File

@@ -35,7 +35,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
run: pip3 install -e ".[dev]" psycopg2-binary
- name: Run isort
run: isort -c .
working-directory: ./src
@@ -55,7 +55,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
run: pip3 install -e ".[dev]" psycopg2-binary
- name: Run flake8
run: flake8 .
working-directory: ./src

View File

@@ -25,24 +25,14 @@ jobs:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
database: [sqlite, postgres, mysql]
database: [sqlite, postgres]
exclude:
- database: mysql
python-version: "3.9"
- database: mysql
python-version: "3.11"
- database: sqlite
python-version: "3.9"
- database: sqlite
python-version: "3.10"
steps:
- uses: actions/checkout@v2
- uses: getong/mariadb-action@v1.1
with:
mariadb version: '10.10'
mysql database: 'pretix'
mysql root password: ''
if: matrix.database == 'mysql'
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '11'
@@ -61,9 +51,9 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext mariadb-client
run: sudo apt update && sudo apt install gettext
- name: Install Python dependencies
run: pip3 install --ignore-requires-python -e ".[dev]" mysqlclient psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
- name: Run checks
run: python manage.py check
working-directory: ./src

View File

@@ -22,14 +22,15 @@ pypi:
- source env/bin/activate
- pip install -U pip wheel setuptools check-manifest twine
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- cd src
- python setup.py sdist
- pip install dist/pretix-*.tar.gz
- python -m pretix migrate
- python -m pretix check
- check-manifest
- cd src
- make npminstall
- python setup.py sdist bdist_wheel
- cd ..
- check-manifest
- python -m build
- twine check dist/*
- twine upload dist/*
tags:

View File

@@ -3,7 +3,6 @@ FROM python:3.11-bullseye
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libmariadb-dev \
gettext \
git \
libffi-dev \
@@ -34,25 +33,12 @@ RUN apt-get update && \
mkdir /static && \
mkdir /etc/supervisord && \
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \
apt-get install -y nodejs && \
curl -qL https://www.npmjs.com/install.sh | sh
apt-get install -y nodejs
ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings
# To copy only the requirements files needed to install from PIP
COPY src/setup.py /pretix/src/setup.py
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix/src && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached,mysql]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
COPY deployment/docker/supervisord /etc/supervisord
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
@@ -60,9 +46,19 @@ COPY deployment/docker/supervisord.web.conf /etc/supervisord.web.conf
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
COPY deployment/docker/nginx-max-body-size.conf /etc/nginx/conf.d/nginx-max-body-size.conf
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY pyproject.toml /pretix/pyproject.toml
COPY _build /pretix/_build
COPY src /pretix/src
RUN cd /pretix/src && python setup.py install
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \

View File

@@ -1,5 +1,7 @@
include LICENSE
include README.rst
include src/Makefile
include _build/backend.py
global-include *.proto
recursive-include src/pretix/static *
recursive-include src/pretix/static.dist *
@@ -31,3 +33,16 @@ recursive-include src/pretix/plugins/returnurl/templates *
recursive-include src/pretix/plugins/returnurl/static *
recursive-include src/pretix/plugins/webcheckin/templates *
recursive-include src/pretix/plugins/webcheckin/static *
recursive-include src *.cfg
recursive-include src *.csv
recursive-include src *.gitkeep
recursive-include src *.jpg
recursive-include src *.json
recursive-include src *.py
recursive-include src *.svg
recursive-include src *.txt
recursive-include src Makefile
recursive-exclude doc *
recursive-exclude deployment *
recursive-exclude res *

12
_build/backend.py Normal file
View File

@@ -0,0 +1,12 @@
import tomli
from setuptools import build_meta as _orig
from setuptools.build_meta import *
def get_requires_for_build_wheel(config_settings=None):
with open("pyproject.toml", "rb") as f:
p = tomli.load(f)
return [
*_orig.get_requires_for_build_wheel(config_settings),
*p['project']['dependencies']
]

View File

@@ -154,23 +154,15 @@ Example::
port=3306
``backend``
One of ``mysql`` (deprecated), ``sqlite3`` and ``postgresql``.
One of ``sqlite3`` and ``postgresql``.
Default: ``sqlite3``.
If you use MySQL, be sure to create your database using
``CREATE DATABASE <dbname> CHARACTER SET utf8;``. Otherwise, Unicode
support will not properly work.
``name``
The database's name. Default: ``db.sqlite3``.
``user``, ``password``, ``host``, ``port``
Connection details for the database connection. Empty by default.
``galera``
(Deprecated) Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
.. _`config-replica`:
Database replica settings

View File

@@ -26,7 +26,7 @@ installation guides):
* `Docker`_
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 9.6+ database server
* A `PostgreSQL`_ 11+ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -321,11 +321,11 @@ workers, e.g. ``docker run … taskworker -Q notifications --concurrency 32``.
.. _Docker: https://docs.docker.com/engine/installation/linux/debian/
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-22-04
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _redis website: https://redis.io/topics/security

View File

@@ -16,14 +16,11 @@ To use pretix, you will need the following things:
* A periodic task runner, e.g. ``cron``
* **A database**. This needs to be a SQL-based that is supported by Django. We highly recommend to either
go for **PostgreSQL** or **MySQL/MariaDB**. If you do not provide one, pretix will run on SQLite, which is useful
go for **PostgreSQL**. If you do not provide one, pretix will run on SQLite, which is useful
for evaluation and development purposes.
.. warning:: Do not ever use SQLite in production. It will break.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
faster. Also, you need a proxying web server in front to provide SSL encryption.

View File

@@ -21,6 +21,7 @@ Requirements
Please set up the following systems beforehand, we'll not explain them here in detail (but see these links for external
installation guides):
* A python 3.9+ installation
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 11+ database server
@@ -323,11 +324,11 @@ Then, proceed like after any plugin installation::
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-22-04
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/

View File

@@ -3,11 +3,11 @@
Migrating from MySQL/MariaDB to PostgreSQL
==========================================
Our recommended database for all production installations is PostgreSQL. Support for MySQL/MariaDB will be removed in
pretix 5.0.
Our recommended database for all production installations is PostgreSQL. Support for MySQL/MariaDB has been removed
in newer pretix releases.
In order to follow this guide, your pretix installation needs to be a version that fully supports MySQL/MariaDB. If you
already upgraded to pretix 5.0, downgrade back to the last 4.x release using ``pip``.
already upgraded to pretix 5.0 or later, downgrade back to the last 4.x release using ``pip``.
.. note:: We have tested this guide carefully, but we can't assume any liability for its correctness. The data loss
risk should be low as long as pretix is not running while you do the migration. If you are a pretix Enterprise
@@ -51,7 +51,7 @@ For our standard docker installation, create the database and user like this::
# sudo -u postgres createuser -P pretix
# sudo -u postgres createdb -O pretix pretix
Make sure that your database listens on the network. If PostgreSQL on the same same host as docker, but not inside a docker container, we recommend that you just listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
Make sure that your database listens on the network. If PostgreSQL on the same same host as docker, but not inside a docker container, we recommend that you listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
listen_addresses = 'localhost,172.17.0.1'
@@ -153,4 +153,89 @@ And you're done! After you've verified everything has been copied correctly, you
.. note:: Don't forget to update your backup process to back up your PostgreSQL database instead of your MySQL database now.
Troubleshooting
---------------
Peer authentication failed
""""""""""""""""""""""""""
Sometimes you might see an error message like this::
django.db.utils.OperationalError: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: Peer authentication failed for user "pretix"
It is important to understand that PostgreSQL by default offers two types of authentication:
- **Peer authentication**, which works automatically based on the Linux user you are working as. This requires that
the connection is made through a local socket (empty ``host=`` in ``pretix.cfg``) and the name of the PostgreSQL user
and the Linux user are identical.
- Typically, you might run into this error if you accidentally execute ``python -m pretix`` commands as root instead
of the ``pretix`` user.
- **Password authentication**, which requires a username and password and works over network connections. To force
password authentication instead of peer authentication, set ``host=127.0.0.1`` in ``pretix.cfg``.
- You can alter the password on a PostgreSQL shell using the command ``ALTER USER pretix WITH PASSWORD '***';``.
When creating a user with the ``createuser`` command, pass option ``-P`` to set a new password.
- Even with password authentication, PostgreSQL by default only allows local connections. To allow remote connections,
you need to adjust both the ``listen_address`` configuration parameter as well as the ``pg_hba.conf`` file (see above
for an example with the docker networking setup).
Database error: relation does not exist
"""""""""""""""""""""""""""""""""""""""
If you see an error like this::
2023-04-17T19:20:47.744023Z ERROR Database error 42P01: relation "public.pretix_foobar" does not exist
QUERY: ALTER TABLE public.pretix_foobar DROP CONSTRAINT IF EXISTS pretix_foobar_order_id_57e2cb41_fk_pretixbas CASCADE;
2023-04-17T19:20:47.744023Z FATAL Failed to create the schema, see above.
The reason is most likely that in the past, you installed a pretix plugin that you no longer have installed. However,
the database still contains tables of that plugin. If you want to keep the data, reinstall the plugin and re-run the
``migrate`` step from above. If you want to get rid of the data, manually drop the table mentioned in the error message
from your MySQL database::
# mysql -u root pretix
mysql> DROP TABLE pretix_foobar;
Then, retry. You might see a new error message with a new table, which you can handle the same way.
Cleaning out a failed attempt
"""""""""""""""""""""""""""""
You might want to clean your PostgreSQL database before you try again after an error. You can do so like this::
# sudo -u postgres psql pretix
pretix=# DROP SCHEMA public CASCADE;
pretix=# CREATE SCHEMA public;
pretix=# ALTER SCHEMA public OWNER TO pretix;
``pgloader`` crashes with heap exhaustion error
"""""""""""""""""""""""""""""""""""""""""""""""
On some larger databases, we've seen ``pgloader`` crash with error messages similar to this::
Heap exhausted during garbage collection: 16 bytes available, 48 requested.
Or this::
2021-01-04T21:31:17.367000Z ERROR A SB-KERNEL::HEAP-EXHAUSTED-ERROR condition without bindings for heap statistics. (If
you did not expect to see this message, please report it.
2021-01-04T21:31:17.382000Z ERROR The value
NIL
is not of type
NUMBER
when binding SB-KERNEL::X
The ``pgloader`` version distributed for Debian and Ubuntu is compiled with the ``SBCL`` compiler. If compiled with
``CCL``, these bugs go away. Unfortunately, it is pretty hard to compile ``pgloader`` manually with ``CCL``. If you
run into this, we therefore recommend using the docker container provided by the ``pgloader`` maintainers::
sudo docker run --rm -v /tmp:/tmp --network host -it dimitri/pgloader:ccl.latest pgloader /tmp/pretix.load
As peer authentication is not available from inside the container, this requires you to use password-based authentication
in PostgreSQL (see above).
.. _PostgreSQL repositories: https://wiki.postgresql.org/wiki/Apt

View File

@@ -32,6 +32,8 @@ as well as the type of underlying hardware. Example:
"token": "kpp4jn8g2ynzonp6",
"hardware_brand": "Samsung",
"hardware_model": "Galaxy S",
"os_name": "Android",
"os_version": "2.3.6",
"software_brand": "pretixdroid",
"software_version": "4.0.0"
}
@@ -98,6 +100,8 @@ following endpoint:
{
"hardware_brand": "Samsung",
"hardware_model": "Galaxy S",
"os_name": "Android",
"os_version": "2.3.6",
"software_brand": "pretixdroid",
"software_version": "4.1.0",
"info": {"arbitrary": "data"}

View File

@@ -24,6 +24,8 @@ all_events boolean Whether this de
limit_events list List of event slugs this device has access to
hardware_brand string Device hardware manufacturer (read-only)
hardware_model string Device hardware model (read-only)
os_name string Device operating system name (read-only)
os_version string Device operating system version (read-only)
software_brand string Device software product (read-only)
software_version string Device software version (read-only)
created datetime Creation time
@@ -76,6 +78,8 @@ Device endpoints
"security_profile": "full",
"hardware_brand": "Zebra",
"hardware_model": "TC25",
"os_name": "Android",
"os_version": "8.1.0",
"software_brand": "pretixSCAN",
"software_version": "1.5.1"
}
@@ -123,6 +127,8 @@ Device endpoints
"security_profile": "full",
"hardware_brand": "Zebra",
"hardware_model": "TC25",
"os_name": "Android",
"os_version": "8.1.0",
"software_brand": "pretixSCAN",
"software_version": "1.5.1"
}
@@ -173,6 +179,8 @@ Device endpoints
"initialized": null
"hardware_brand": null,
"hardware_model": null,
"os_name": null,
"os_version": null,
"software_brand": null,
"software_version": null
}

View File

@@ -70,6 +70,11 @@ Endpoints
The ``public_url`` field has been added.
.. versionchanged:: 5.0
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
added.
.. http:get:: /api/v1/organizers/(organizer)/events/
Returns a list of all events within a given organizer the authenticated user/token has access to.
@@ -141,6 +146,10 @@ Endpoints
:query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.

View File

@@ -111,7 +111,7 @@ Listing available exporters
"input_parameters": [
{
"name": "events",
"required": true
"required": false
},
{
"name": "_format",

View File

@@ -20,6 +20,12 @@ currency string Currency of the
testmode boolean Whether this is a test gift card
expires datetime Expiry date (or ``null``)
conditions string Special terms and conditions for this card (or ``null``)
owner_ticket integer Internal ID of an order position that is the "owner" of
this gift card and can view all transactions. When setting
this field, you can also give the ``secret`` of an order
position.
issuer string Organizer slug of the organizer who created this gift
card and is responsible for it.
===================================== ========================== =======================================================
The gift card transaction resource contains the following public fields:
@@ -35,8 +41,17 @@ value money (string) Transaction amo
event string Event slug, if the gift card was used in the web shop (or ``null``)
order string Order code, if the gift card was used in the web shop (or ``null``)
text string Custom text of the transaction (or ``null``)
info object Additional data about the transaction (or ``null``)
acceptor string Organizer slug of the organizer who created this transaction
(can be ``null`` for all transactions performed before
this field was added.)
===================================== ========================== =======================================================
.. versionchanged:: 4.20
The ``owner_ticket`` and ``issuer`` attributes of the gift card and the ``info`` and ``acceptor`` attributes of the
gift card transaction resource have been added.
Endpoints
---------
@@ -72,6 +87,8 @@ Endpoints
"testmode": false,
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "13.37"
}
]
@@ -81,6 +98,10 @@ Endpoints
: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.
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that the ``owner_ticket``
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -113,6 +134,8 @@ Endpoints
"testmode": false,
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "13.37"
}
@@ -157,10 +180,16 @@ Endpoints
"currency": "EUR",
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "13.37"
}
:param organizer: The ``slug`` field of the organizer to create a gift card for
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that the ``owner_ticket``
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:statuscode 201: no error
:statuscode 400: The gift card could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
@@ -205,6 +234,8 @@ Endpoints
"currency": "EUR",
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "14.00"
}
@@ -250,6 +281,8 @@ Endpoints
"testmode": false,
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "15.37"
}
@@ -293,7 +326,11 @@ Endpoints
"value": "50.00",
"event": "democon",
"order": "FXQYW",
"text": null
"text": null,
"acceptor": "bigevents",
"info": {
"created_by": "plugin1"
}
}
]
}

View File

@@ -18,6 +18,7 @@ at :ref:`plugin-docs`.
item_variations
item_bundles
item_add-ons
item_meta_properties
questions
question_options
quotas

View File

@@ -0,0 +1,211 @@
Item Meta Properties
====================
Resource description
--------------------
An Item Meta Property is used to include (event internally relevant) meta information with every item (product). This
could be internal categories like booking positions.
The Item Meta Properties resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Unique ID for this property
name string Name of the property
default string Value of the default option
required boolean If ``true``, this property will have to be assigned a
value in all items of the related event
allowed_values list List of all permitted values for this property,
or ``null`` for no limitation
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/
Returns a list of all Item Meta Properties within a given event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/ 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,
"name": "Color",
"default": "red",
"required": true,
"allowed_values": ["red", "green", "blue"]
}
]
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
Returns information on one property, identified by its id.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"id": 1,
"name": "Color",
"default": "red",
"required": true,
"allowed_values": ["red", "green", "blue"]
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the item meta property to retrieve
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/
Creates a new item meta property
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "ref-code",
"default": "abcde",
"required": true,
"allowed_values": null
}
**Example response**:
.. sourcecode:: http
{
"id": 2,
"name": "ref-code",
"default": "abcde",
"required": true,
"allowed_values": null
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:statuscode 201: no error
:statuscode 400: The item meta property could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
Update an item meta property. 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.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/2/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"required": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 2,
"name": "ref-code",
"default": "abcde",
"required": false,
"allowed_values": []
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the item meta property to modify
:statuscode 200: no error
:statuscode 400: The property could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
Delete an item meta property.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/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
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the item meta property to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.

View File

@@ -19,6 +19,7 @@ Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the medium
type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``.
organizer string Organizer slug of the organizer who "owns" this medium.
identifier string Unique identifier of the medium. The format depends on the ``type``.
active boolean Whether this medium may be used.
created datetime Date of creation
@@ -67,6 +68,7 @@ Endpoints
"results": [
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
@@ -91,11 +93,11 @@ Endpoints
:query string updated_since: Only show media updated since a given date.
:query integer linked_orderposition: Only show media linked to the given ticket.
:query integer linked_giftcard: Only show media linked to the given gift card.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -123,6 +125,7 @@ Endpoints
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
@@ -138,11 +141,11 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the medium to fetch
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
@@ -152,6 +155,9 @@ Endpoints
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
medium behind the scenes.
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
**Example request**:
.. sourcecode:: http
@@ -176,6 +182,7 @@ Endpoints
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
@@ -235,6 +242,7 @@ Endpoints
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
@@ -291,6 +299,7 @@ Endpoints
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",

View File

@@ -63,6 +63,11 @@ last_modified datetime Last modificati
The ``search`` query parameter has been added to filter sub-events by their name or location in any language.
.. versionchanged:: 5.0
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
added.
Endpoints
---------
@@ -130,6 +135,10 @@ Endpoints
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:query search: Only return events matching a given search query.
:param organizer: The ``slug`` field of a valid organizer
@@ -458,6 +467,10 @@ Endpoints
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel.
:param organizer: The ``slug`` field of a valid organizer

View File

@@ -20,11 +20,16 @@ internal_name string An optional nam
rate decimal (string) Tax rate in percent
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
the specified product price
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will
be ignored if custom rules are set.
home_country string Merchant country (required for reverse charge), can be
``null`` or empty string
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
rules keep the gross price constant (default is ``false``)
custom_rules object Dynamic rules specification. Each list element
corresponds to one rule that will be processed in order.
The current version of the schema in use can be found
`here`_.
===================================== ========================== =======================================================
@@ -32,6 +37,10 @@ keep_gross_if_rate_changes boolean If ``true``, ch
The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added.
.. versionchanged:: 2023.6
The ``custom_rules`` attribute has been added.
Endpoints
---------
@@ -68,6 +77,7 @@ Endpoints
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE"
}
]
@@ -108,6 +118,7 @@ Endpoints
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE"
}
@@ -156,6 +167,7 @@ Endpoints
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE"
}
@@ -203,6 +215,7 @@ Endpoints
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE"
}
@@ -242,3 +255,5 @@ Endpoints
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json

View File

@@ -47,6 +47,8 @@ tag string A string that i
comment string An internal comment on the voucher
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
===================================== ========================== =======================================================
@@ -95,6 +97,9 @@ Endpoints
"comment": "",
"seat": null,
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
]
}
@@ -161,7 +166,10 @@ Endpoints
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -198,7 +206,10 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
**Example response**:
@@ -225,7 +236,10 @@ Endpoints
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
:param organizer: The ``slug`` field of the organizer to create a voucher for
@@ -264,7 +278,10 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
},
{
"code": "ASDKLJCYXCASDASD",
@@ -279,7 +296,10 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
},
**Example response**:
@@ -353,7 +373,10 @@ Endpoints
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -50,6 +50,10 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.payment.confirmed``
* ``pretix.event.order.approved``
* ``pretix.event.order.denied``
* ``pretix.event.orders.waitinglist.added``
* ``pretix.event.orders.waitinglist.changed``
* ``pretix.event.orders.waitinglist.deleted``
* ``pretix.event.orders.waitinglist.voucher_assigned``
* ``pretix.event.checkin``
* ``pretix.event.checkin.reverted``
* ``pretix.event.added``

View File

@@ -18,13 +18,13 @@ If you want to add a custom view to the control area of an event, just register
.. code-block:: python
from django.conf.urls import url
from django.urls import re_path
from . import views
urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.admin_view, name='backend'),
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.admin_view, name='backend'),
]
It is required that your URL parameters are called ``organizer`` and ``event``. If you want to

View File

@@ -13,7 +13,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators
register_ticket_secret_generators, gift_card_transaction_display
Order events
""""""""""""
@@ -21,7 +21,7 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
: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
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins
"""""""""

View File

@@ -70,6 +70,8 @@ The provider class
.. autoattribute:: settings_form_fields
.. autoattribute:: walletqueries
.. automethod:: settings_form_clean
.. automethod:: settings_content_render

View File

@@ -35,13 +35,13 @@ automatically and should be provided by any plugin that provides any view.
A very basic example that provides one view in the admin panel and one view in the frontend
could look like this::
from django.conf.urls import url
from django.urls import re_path
from . import views
urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.AdminView.as_view(), name='backend'),
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.AdminView.as_view(), name='backend'),
]
event_patterns = [

143
doc/plugins/epaybl.rst Normal file
View File

@@ -0,0 +1,143 @@
ePayBL
======
.. note::
Since ePayBL is only available to german federal, provincial and communal entities, the following page is also
only provided in german. Should you require assistance with ePayBL and do not speak this language, please feel free
reach out to support@pretix.eu.
Einführung
----------
.. note::
Sollten Sie lediglich schnell entscheiden wollen, welcher Kontierungsmodus in den Einstellungen des pretix
ePayBL-plugins gewählt werden soll, so springen Sie direkt zur Sektion :ref:`Kontierungsmodus`.
`ePayBL`_ - das ePayment-System von Bund und Länder - ist das am weitesten verbreitete Zahlungssystem für Bundes-, Länder-
sowie kommunale Aufgabenträger. Während es nur wie eines von vielen anderen Zahlungssystemen scheint, so bietet es
seinen Nutzern besondere Vorteile, wie die automatische Erfassung von Zahlungsbelegen, dem Übertragen von Buchungen in
Haushaltskassen/-systeme sowie die automatische Erfassung von Kontierungen und Steuermerkmalen.
Rein technisch gesehen ist ePayBL hierbei nicht ein eigenständiger Zahlungsdienstleister sondern nur ein eine Komponente
im komplexen System, dass die Zahlungsabwicklung für Kommunen und Behörden ist.
Im folgenden der schematische Aufbau einer Umgebung, in welcher ePayBL zum Einsatz kommt:
.. figure:: img/epaybl_flowchart.png
:class: screenshot
Quelle: Integrationshandbuch ePayBL-Konnektor, DResearch Digital Media Systems GmbH
In diesem Schaubild stellt pretix, bzw. die von Ihnen als Veranstalter angelegten Ticketshops, das Fachverfahren dar.
ePayBL stellt das Bindeglied zwischen den Fachverfahren, Haushaltssystemen und dem eigentlichen Zahlungsdienstleister,
dem sog. ZV-Provider dar. Dieser ZV-Provider ist die Stelle, welche die eigentlichen Kundengelder einzieht und an den
Händler auszahlt. Das Gros der Zahlungsdienstleister unterstützt pretix hierbei auch direkt; sprich: Sollten Sie die
Anbindung an Ihre Haushaltssysteme nicht benötigen, kann eine direkte Anbindung in der Regel ebenso - und dies bei meist
vermindertem Aufwand - vorgenommen werden.
In der Vergangenheit zeigte sich jedoch schnell, dass nicht jeder IT-Dienstleister immer sofort die neueste Version von
ePayBL seinen Nutzern angeboten hat. Die Gründe hierfür sind mannigfaltig: Von fest vorgegebenen Update-Zyklen bis hin
zu Systeme mit speziellen Anpassungen, kann leider nicht davon ausgegangen werden, dass alle ePayBL-Systeme exakt gleich
ansprechbar sind - auch wenn es sich dabei eigentlich um einen standardisierten Dienst handelt.
Aus diesem Grund gibt es mit dem ePayBL-Konnektor eine weitere Abstraktionsschicht welche optional zwischen den
Fachverfahren und dem ePayBL-Server sitzt. Dieser Konnektor wird so gepflegt, dass er zum einen eine dauerhaft
gleichartige Schnittstelle den Fachverfahren bietet aber gleichzeitig auch mit jeder Version des ePayBL-Servers
kommunizieren kann - egal wie neu oder alt, wie regulär oder angepasst diese ist.
Im Grunde müsste daher eigentlich immer gesagt werden, dass pretix eine Anbindung an den ePayBL-Konnektor bietet; nicht
an "ePayBL" oder den "ePayBL-Server". Diese Unterscheidung kann bei der Ersteinrichtung und Anforderung von Zugangsdaten
von Relevanz sein. Da in der Praxis jedoch beide Begriffe gleichbedeutend genutzt werden, wird im Folgenden auch nur von
einer ePayBL-Anbindung die Rede sein - auch wenn explizit der Konnektor gemeint ist.
.. _`Kontierungsmodus`:
Kontierungsmodus
----------------
ePayBL ist ein Produkt, welches für die Abwicklung von Online-Zahlungsvorgängen in der Verwaltung geschaffen wurde. Ein
Umfeld, in dem klar definiert ist, was ein Kunde gerade bezahlt und wohin das Geld genau fließt. Diese Annahmen lassen
sich in einem Ticketshop wie pretix jedoch nur teilweise genauso abbilden.
Die ePayBL-Integration für pretix bietet daher zwei unterschiedliche Modi an, wie Buchungen erfasst und an ePayBL und
damit auch an die dahinterliegenden Haushaltssysteme gemeldet werden können.
Kontierung pro Position/Artikel
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dieser Modus versucht den klassischen, behördentypischen ePayBL-Zahlungsvorgang abzubilden: Jede einzelne Position, die
ein Kunde in den Warenkorb legt, wird auch genauso 1:1 an ePayBL und die Hintergrundsysteme übermittelt.
Hierbei muss zwingend auch für jede Position ein Kennzeichen für Haushaltsstelle und Objektnummer, sowie optional ein
Kontierungsobjekt (``HREF``; bspw. ``stsl=Steuerschlüssel;psp=gsb:Geschäftsbereich,auft:Innenauftrag,kst:Kostenstelle;``
) übermittelt werden.
Diese Daten sind vom Veranstalter entsprechend für jeden in der Veranstaltung angelegten Artikel innerhalb des Tabs
"Zusätzliche Einstellungen" der Produkteinstellungen zu hinterlegen.
Während diese Einstellung eine größtmögliche Menge an Kontierungsdaten überträgt und auch ein separates Verbuchen von
Leistungen auf unterschiedliche Haushaltsstellen erlaubt, so hat diese Option auch einen großen Nachteil: Der Kunde kann
nur eine Zahlung für seine Bestellung leisten.
Während sich dies nicht nach einem großen Problem anhört, so kann dies beim Kunden zu Frust führen. pretix bietet die
Option an, dass ein Veranstalter eine Bestellung jederzeit verändern kann: Ändern von Preisen von Positionen in einer
aufgegebenen Bestellung, Zubuchen und Entfernen von Bestellpositionen, etc. Hat der Kunde seine ursprüngliche Bestellung
jedoch schon bezahlt, kann pretix nicht mehr die komplette Bestellung mit den passenden Kontierungen übertragen - es
müsste nur ein Differenz-Abbild zwischen Ursprungsbestellung und aktueller Bestellung übertragen werden. Aber auch wenn
eine "Nachmeldung" möglich wäre, so wäre ein konkretes Auflösen für was jetzt genau gezahlt wird, nicht mehr möglich.
Daher gilt bei der Nutzung der Kontierung pro Position/Artikel: Der Kunde kann nur eine (erfolgreiche) Zahlung auf seine
Bestellung leisten.
Eine weitere Einschränkung dieses Modus ist, dass aktuell keine Gebühren-Positionen (Versandkosten, Zahlungs-, Storno-
oder Servicegebühren) in diesem Modus übertragen werden können. Bitte wenden Sie sich an uns, wenn Sie diese
Funktionalität benötigen.
Kontierung pro Zahlvorgang
^^^^^^^^^^^^^^^^^^^^^^^^^^
Dieser Modus verabschiedet sich vom behördlichen "Jede Position gehört genau zu einem Haushaltskonto und muss genau
zugeordnet werden". Stattdessen werden alle Bestellpositionen - inklusive eventuell definierter Gebühren - vermengt und
nur als ein großer Warenkorb, genauer gesagt: eine einzige Position an ePayBL sowie die Hintergrundsysteme gemeldet.
Während im "pro Postion/Artikel"-Modus jeder Artikel einzeln übermittelt wird und damit auch korrekt pro Artikel der
jeweilige Brutto- und Nettopreis, sowie der anfallende Steuerbetrag und ein Steuerkennzeichen (mit Hilfe des optionalen
``HREF``-Attributs) übermittelt werden, ist dies im "pro Zahlvorgang"-Modus nicht möglich.
Stattdessen übermittelt pretix nur einen Betrag für den gesamten Warenkorb: Bruttopreis == Nettopreis. Der Steuerbetrag
wird hierbei als 0 übermittelt.
Die Angabe einer Haushaltsstelle und Objektnummer, sowie optional der ``HREF``-Kontierungsinformationen ist jedoch
weiterhin notwendig - allerdings nicht mehr individuell für jeden Artikel/jede Position sondern nur für die gesamte
Bestellung. Diese Daten sind direkt in den ePayBL-Einstellungen der Veranstaltung unter Einstellungen -> Zahlung ->
ePayBL vorzunehmen
In der Praxis bedeutet dies, dass in einem angeschlossenen Haushaltssystem nicht nachvollzogen kann, welche Positionen
konkret erworben und bezahlt wurden - stattdessen kann nur der Fakt, dass etwas verkauft wurde erfasst werden.
Je nach Aufbau und Vorgaben der Finanzbuchhaltung kann dies jedoch ausreichend sein - wenn bspw. eine Ferienfahrt
angeboten wird und seitens der Haushaltssysteme nicht erfasst werden muss, wie viel vom Gesamtbetrag einer Bestellung
auf die Ferienfahrt an sich, auf einen Zubringerbus und einen Satz Bettwäsche entfallen ist, sondern (vereinfacht
gesagt) es ausreichend ist, dass "Eine Summe X für die Haushaltsstelle/Objektnummer geflossen ist".
Dieser Modus der Kontierung bietet Ihnen auch als Vorteil gegenüber dem vorhergehenden an, dass die Bestellungen der
Kunden jederzeit erweitert und verändert werden können - auch wenn die Ursprungsbestellung schon bezahlt wurde und nur
noch eine Differenz gezahlt wird.
Einschränkungen
---------------
Zum aktuellen Zeitpunkt erlaubt die pretix-Anbindung an ePayBL nicht das durchführen von Erstattungen von bereits
geleisteten Zahlungen. Der Prozess hierfür unterscheidet sich von Behörde zu Behörde und muss daher händisch
durchgeführt werden.
.. _ePayBL: https://www.epaybl.de/

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -18,6 +18,7 @@ If you want to **create** a plugin, please go to the
campaigns
certificates
digital
epaybl
exhibitors
shipping
imported_secrets

View File

@@ -1,4 +1,4 @@
sphinx==6.2.*
sphinx==7.0.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain

View File

@@ -1,5 +1,5 @@
-e ../
sphinx==6.2.*
sphinx==7.0.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain

View File

@@ -201,6 +201,10 @@ record for the subdomain ``pretix._domainkey`` with the following contents::
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
in this case, as pretix only retries email delivery for a certain time period.
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax

View File

@@ -22,77 +22,77 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.1",
]
dependencies = [
# Note that many of these are repeated as build-time dependencies down below -- change them too in case of updates!
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"celery==5.2.*",
"celery==5.3.*",
"chardet==5.1.*",
"cryptography>=3.4.2",
"css-inline==0.8.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django==3.2.*,>=3.2.18",
"Django==4.1.*",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
"django-filter==23.1",
"django-filter==23.2",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.4",
"django-formtools==2.4.1",
"django-hierarkey==1.1.*",
"django-hijack==3.3.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-localflavor==4.0",
"django-markup",
"django-mysql",
"django-oauth-toolkit==2.2.*",
"django-otp==1.1.*",
"django-phonenumber-field==7.0.*",
"django-otp==1.2.*",
"django-phonenumber-field==7.1.*",
"django-redis==5.2.*",
"django-scopes==1.2.*",
"django-scopes==2.0.*",
"django-statici18n==2.3.*",
"djangorestframework==3.14.*",
"dnspython==2.2.*",
"dnspython==2.3.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.2.*",
"kombu==5.3.*",
"libsass==0.22.*",
"lxml",
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.23.*",
"mt-940==4.30.*",
"oauthlib==3.2.*",
"openpyxl==3.1.*",
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.6.*",
"PyJWT==2.7.*",
"phonenumberslite==8.13.*",
"Pillow==9.5.*",
"protobuf==4.22.*",
"pretix-plugin-build",
"protobuf==4.23.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.21",
"pycryptodome==3.17.*",
"pypdf==3.8.*",
"pycryptodome==3.18.*",
"pypdf==3.9.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
"python-dateutil==2.8.*",
"python-u2flib-server==4.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==7.4.*",
"redis==4.5.*,>=4.5.4",
"reportlab==3.6.*",
"requests==2.28.*",
"reportlab==4.0.*",
"requests==2.31.*",
"sentry-sdk==1.15.*",
"sepaxml==2.6.*",
"slimit",
@@ -109,17 +109,12 @@ dependencies = [
[project.optional-dependencies]
memcached = ["pylibmc"]
mysql = ["mysqlclient"]
dev = [
"coverage",
"coveralls",
"django-debug-toolbar==4.0.*",
"django-formset-js-improved==0.5.0.3",
"django-oauth-toolkit==2.2.*",
"flake8==6.0.*",
"freezegun",
"isort==5.12.*",
"oauthlib==3.2.*",
"pep8-naming==0.13.*",
"potypo",
"pycodestyle==2.10.*",
@@ -130,7 +125,7 @@ dev = [
"pytest-mock==3.10.*",
"pytest-rerunfailures==11.*",
"pytest-sugar",
"pytest-xdist==3.2.*",
"pytest-xdist==3.3.*",
"pytest==7.3.*",
"responses",
]
@@ -140,31 +135,14 @@ build = "pretix._build:CustomBuild"
build_ext = "pretix._build:CustomBuildExt"
[build-system]
build-backend = "backend"
backend-path = ["_build"]
requires = [
"setuptools",
"setuptools-rust",
"wheel",
"importlib_metadata",
# These are runtime dependencies that we unfortunately need to be import in the step that generates
# all CSS and JS asset files. We should keep their versions in sync with the definition above.
"babel",
"Django==3.2.*,>=3.2.18",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
"django-formtools==2.4",
"django-hierarkey==1.1.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-phonenumber-field==7.0.*",
"django-statici18n==2.3.*",
"djangorestframework==3.14.*",
"libsass==0.22.*",
"phonenumberslite==8.13.*",
"pycountry",
"pyuca",
"slimit",
"tomli",
]
[project.urls]
@@ -182,4 +160,4 @@ version = {attr = "pretix.__version__"}
[tool.setuptools.packages.find]
where = ["src"]
include = ["pretix*"]
namespaces = false
namespaces = false

40
setup.cfg Normal file
View File

@@ -0,0 +1,40 @@
[check-manifest]
ignore =
env/**
doc/*
deployment/*
res/*
src/.update-locales
src/Makefile
src/manage.py
src/pretix/icons/*
src/pretix/static.dist/**
src/pretix/static/jsi18n/**
src/requirements.txt
src/requirements/*
src/tests/*
src/tests/api/*
src/tests/base/*
src/tests/control/*
src/tests/testdummy/*
src/tests/templates/*
src/tests/presale/*
src/tests/doc/*
src/tests/helpers/*
src/tests/media/*
src/tests/multidomain/*
src/tests/plugins/*
src/tests/plugins/badges/*
src/tests/plugins/banktransfer/*
src/tests/plugins/paypal/*
src/tests/plugins/paypal2/*
src/tests/plugins/pretixdroid/*
src/tests/plugins/stripe/*
src/tests/plugins/sendmail/*
src/tests/plugins/ticketoutputpdf/*
.*
CODE_OF_CONDUCT.md
CONTRIBUTING.md
Dockerfile
SECURITY.md

View File

@@ -19,8 +19,31 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import sys
from pathlib import Path
import setuptools
sys.path.append(str(Path.cwd() / 'src'))
def _CustomBuild(*args, **kwargs):
from pretix._build import CustomBuild
return CustomBuild(*args, **kwargs)
def _CustomBuildExt(*args, **kwargs):
from pretix._build import CustomBuildExt
return CustomBuildExt(*args, **kwargs)
cmdclass = {
'build': _CustomBuild,
'build_ext': _CustomBuildExt,
}
if __name__ == "__main__":
setuptools.setup()
setuptools.setup(
cmdclass=cmdclass,
)

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "4.19.0"
__version__ = "2023.7.0.dev0"

View File

@@ -30,7 +30,6 @@ from django.utils.translation import gettext_lazy as _ # NOQA
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
USE_I18N = True
USE_L10N = True
USE_TZ = True
INSTALLED_APPS = [
@@ -68,6 +67,7 @@ INSTALLED_APPS = [
'oauth2_provider',
'phonenumber_field',
'statici18n',
'django.forms', # after pretix.base for overrides
]
FORMAT_MODULE_PATH = [
@@ -80,6 +80,7 @@ ALL_LANGUAGES = [
('de-informal', _('German (informal)')),
('ar', _('Arabic')),
('zh-hans', _('Chinese (simplified)')),
('zh-hant', _('Chinese (traditional)')),
('cs', _('Czech')),
('da', _('Danish')),
('nl', _('Dutch')),
@@ -179,6 +180,8 @@ TEMPLATES = [
},
]
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static.dist')
STATICFILES_FINDERS = (

View File

@@ -45,6 +45,10 @@ def npm_install():
class CustomBuild(build):
def run(self):
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
# Only run this command on the pretix module, not on other modules even if it's registered globally
# in some cases
return build.run(self)
if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix._build_settings")
@@ -68,6 +72,10 @@ class CustomBuild(build):
class CustomBuildExt(build_ext):
def run(self):
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
# Only run this command on the pretix module, not on other modules even if it's registered globally
# in some cases
return build_ext.run(self)
if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching
npm_install()

View File

@@ -201,6 +201,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'),
('PATCH', 'api-v1:giftcard-detail'),
('GET', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'),

View File

@@ -59,7 +59,7 @@ class IdempotencyMiddleware:
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
idempotency_key = request.headers.get('X-Idempotency-Key', '')
with transaction.atomic():
with transaction.atomic(durable=True):
call, created = ApiCall.objects.select_for_update(of=OF_SELF).get_or_create(
auth_hash=auth_hash,
idempotency_key=idempotency_key,
@@ -75,7 +75,7 @@ class IdempotencyMiddleware:
if created:
resp = self.get_response(request)
with transaction.atomic():
with transaction.atomic(durable=True):
if resp.status_code in (409, 429, 500, 503):
# This is the exception: These calls are *meant* to be retried!
call.delete()

View File

@@ -19,3 +19,45 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import json
from rest_framework import serializers
class AsymmetricField(serializers.Field):
def __init__(self, read, write, **kwargs):
self.read = read
self.write = write
super().__init__(
required=self.write.required,
default=self.write.default,
initial=self.write.initial,
source=self.write.source if self.write.source != self.write.field_name else None,
label=self.write.label,
allow_null=self.write.allow_null,
error_messages=self.write.error_messages,
validators=self.write.validators,
**kwargs
)
def to_internal_value(self, data):
return self.write.to_internal_value(data)
def to_representation(self, value):
return self.read.to_representation(value)
def run_validation(self, data=serializers.empty):
return self.write.run_validation(data)
class CompatibleJSONField(serializers.JSONField):
def to_internal_value(self, data):
try:
return json.dumps(data)
except (TypeError, ValueError):
self.fail('invalid')
def to_representation(self, value):
if value:
return json.loads(value)
return value

View File

@@ -46,11 +46,15 @@ from rest_framework import serializers
from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.models.items import (
ItemMetaProperty, SubEventItem, SubEventItemVariation,
)
from pretix.base.models.tax import CustomRulesValidator
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
@@ -648,9 +652,16 @@ class SubEventSerializer(I18nAwareModelSerializer):
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
custom_rules = CompatibleJSONField(
validators=[CustomRulesValidator()],
required=False,
allow_null=True,
)
class Meta:
model = TaxRule
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes')
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name',
'keep_gross_if_rate_changes', 'custom_rules')
class EventSettingsSerializer(SettingsSerializer):
@@ -683,6 +694,7 @@ class EventSettingsSerializer(SettingsSerializer):
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'waiting_list_limit_per_user',
'max_items_per_order',
'reservation_time',
'contact_mail',
@@ -716,6 +728,7 @@ class EventSettingsSerializer(SettingsSerializer):
'payment_term_minutes',
'payment_term_last',
'payment_term_expire_automatically',
'payment_term_expire_delay_days',
'payment_term_accept_late',
'payment_explanation',
'payment_pending_hidden',
@@ -765,6 +778,7 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_footer_text',
'invoice_eu_currencies',
'invoice_logo_image',
'invoice_renderer_highlight_order_code',
'cancel_allow_user',
'cancel_allow_user_until',
'cancel_allow_user_unpaid_keep',
@@ -902,3 +916,23 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
else []
)
)
class MultiLineStringField(serializers.Field):
def to_representation(self, value):
return [v.strip() for v in value.splitlines()]
def to_internal_value(self, data):
if isinstance(data, list) and len(data) > 0:
return "\n".join(data)
else:
raise ValidationError('Invalid data type.')
class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
allowed_values = MultiLineStringField(allow_null=True)
class Meta:
model = ItemMetaProperty
fields = ('id', 'name', 'default', 'required', 'allowed_values')

View File

@@ -93,7 +93,7 @@ class JobRunSerializer(serializers.Serializer):
if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
self.fields["events"] = serializers.SlugRelatedField(
queryset=events,
required=True,
required=False,
allow_empty=False,
slug_field='slug',
many=True
@@ -156,8 +156,9 @@ class JobRunSerializer(serializers.Serializer):
def to_internal_value(self, data):
if isinstance(data, QueryDict):
data = data.copy()
for k, v in self.fields.items():
if isinstance(v, serializers.ManyRelatedField) and k not in data:
if isinstance(v, serializers.ManyRelatedField) and k not in data and k != "events":
data[k] = []
for fk in self.fields.keys():

View File

@@ -60,11 +60,15 @@ class NestedGiftCardSerializer(GiftCardSerializer):
class ReusableMediaSerializer(I18nAwareModelSerializer):
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True)
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
else:
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
required=False,
@@ -109,6 +113,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
model = ReusableMedium
fields = (
'id',
'organizer',
'created',
'updated',
'type',

View File

@@ -19,7 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import json
import logging
import os
from collections import Counter, defaultdict
@@ -39,6 +38,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.item import (
@@ -535,8 +535,9 @@ class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2
def to_representation(self, instance: Order):
t = None
for p in instance.payments.all():
t = p.provider
if instance.pk:
for p in instance.payments.all():
t = p.provider
return t
@@ -544,10 +545,10 @@ class OrderPaymentDateField(serializers.DateField):
# TODO: Remove after pretix 2.2
def to_representation(self, instance: Order):
t = None
for p in instance.payments.all():
t = p.payment_date or t
if instance.pk:
for p in instance.payments.all():
t = p.payment_date or t
if t:
return super().to_representation(t.date())
@@ -895,19 +896,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
return data
class CompatibleJSONField(serializers.JSONField):
def to_internal_value(self, data):
try:
return json.dumps(data)
except (TypeError, ValueError):
self.fail('invalid')
def to_representation(self, value):
if value:
return json.loads(value)
return value
class WrappedList:
def __init__(self, data):
self._data = data
@@ -1363,6 +1351,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answers.append(answ)
pos.answers = answers
pos.pseudonymization_id = "PREVIEW"
pos.checkins = []
pos_map[pos.positionid] = pos
else:
if pos.voucher:
@@ -1459,6 +1448,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if simulate:
order.fees = fees
order.positions = pos_map.values()
order.payments = []
order.refunds = []
return order # ignore payments
else:
order.save(update_fields=['total'])

View File

@@ -70,6 +70,8 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
def validate(self, data):
data = super().validate(data)
if 'order' in self.context:
data['order'] = self.context['order']
if data.get('addon_to'):
try:
data['addon_to'] = data['order'].positions.get(positionid=data['addon_to'])

View File

@@ -22,21 +22,23 @@
import logging
from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers import AsymmetricField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region
from pretix.base.models import (
Customer, Device, GiftCard, GiftCardTransaction, Membership,
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User,
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail
@@ -127,8 +129,52 @@ class MembershipSerializer(I18nAwareModelSerializer):
return super().update(instance, validated_data)
class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
queryset = self.get_queryset()
if isinstance(data, int):
try:
return queryset.get(pk=data)
except ObjectDoesNotExist:
self.fail('does_not_exist', pk_value=data)
elif isinstance(data, str):
try:
return queryset.get(
Q(secret=data)
| Q(pseudonymization_id=data)
| Q(pk__in=ReusableMedium.objects.filter(
organizer=self.context['organizer'],
type='barcode',
identifier=data
))
)
except ObjectDoesNotExist:
self.fail('does_not_exist', pk_value=data)
self.fail('incorrect_type', data_type=type(data).__name__)
class GiftCardSerializer(I18nAwareModelSerializer):
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
issuer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['owner_ticket'].queryset = OrderPosition.objects.filter(order__event__organizer=self.context['organizer'])
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
from pretix.api.serializers.media import (
NestedOrderPositionSerializer,
)
self.fields['owner_ticket'] = AsymmetricField(
NestedOrderPositionSerializer(read_only=True, context=self.context),
self.fields['owner_ticket'],
)
def validate(self, data):
data = super().validate(data)
@@ -137,8 +183,11 @@ class GiftCardSerializer(I18nAwareModelSerializer):
qs = GiftCard.objects.filter(
secret=s
).filter(
Q(issuer=self.context["organizer"]) | Q(
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
Q(issuer=self.context["organizer"]) |
Q(issuer__in=GiftCardAcceptance.objects.filter(
acceptor=self.context["organizer"],
active=True,
).values_list('issuer', flat=True))
)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
@@ -151,7 +200,8 @@ class GiftCardSerializer(I18nAwareModelSerializer):
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
'issuer')
class OrderEventSlugField(serializers.RelatedField):
@@ -162,11 +212,12 @@ class OrderEventSlugField(serializers.RelatedField):
class GiftCardTransactionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
acceptor = serializers.SlugRelatedField(slug_field='slug', read_only=True)
event = OrderEventSlugField(source='order', read_only=True)
class Meta:
model = GiftCardTransaction
fields = ('id', 'datetime', 'value', 'event', 'order', 'text')
fields = ('id', 'datetime', 'value', 'event', 'order', 'text', 'info', 'acceptor')
class EventSlugField(serializers.SlugRelatedField):
@@ -200,6 +251,8 @@ class DeviceSerializer(serializers.ModelSerializer):
unique_serial = serializers.CharField(read_only=True)
hardware_brand = serializers.CharField(read_only=True)
hardware_model = serializers.CharField(read_only=True)
os_name = serializers.CharField(read_only=True)
os_version = serializers.CharField(read_only=True)
software_brand = serializers.CharField(read_only=True)
software_version = serializers.CharField(read_only=True)
created = serializers.DateTimeField(read_only=True)
@@ -212,7 +265,7 @@ class DeviceSerializer(serializers.ModelSerializer):
fields = (
'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events',
'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model',
'software_brand', 'software_version', 'security_profile'
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
)

View File

@@ -63,7 +63,8 @@ class VoucherSerializer(I18nAwareModelSerializer):
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
'all_bundles_included')
read_only_fields = ('id', 'redeemed')
list_serializer_class = VoucherListSerializer

View File

@@ -39,7 +39,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
WaitingListEntry.clean_duplicate(full_data.get('email'), full_data.get('item'), full_data.get('variation'),
WaitingListEntry.clean_duplicate(event, full_data.get('email'), full_data.get('item'), full_data.get('variation'),
full_data.get('subevent'), self.instance.pk if self.instance else None)
WaitingListEntry.clean_itemvar(event, full_data.get('item'), full_data.get('variation'))
WaitingListEntry.clean_subevent(event, full_data.get('subevent'))

View File

@@ -35,8 +35,7 @@
import importlib
from django.apps import apps
from django.conf.urls import re_path
from django.urls import include
from django.urls import include, re_path
from rest_framework import routers
from pretix.api.views import cart
@@ -89,6 +88,7 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
event_router.register(r'cartpositions', cart.CartPositionViewSet)
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')

View File

@@ -396,7 +396,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
source_type='barcode', legacy_url_support=False):
source_type='barcode', legacy_url_support=False, simulate=False):
if not checkinlists:
raise ValidationError('No check-in list passed.')
@@ -433,6 +433,8 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
)
raw_barcode_for_checkin = None
from_revoked_secret = False
if simulate:
common_checkin_args['__fake_arg_to_prevent_this_from_being_saved'] = True
# 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or
# parent secret
@@ -472,13 +474,14 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
revoked_matches = list(
RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
if len(revoked_matches) == 0:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
if not simulate:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
for cl in checkinlists:
for k, s in cl.event.ticket_secret_generators.items():
@@ -492,12 +495,13 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
except:
pass
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if not simulate:
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if force and legacy_url_support and isinstance(auth, Device):
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
@@ -539,19 +543,20 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
from_revoked_secret = True
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[revoked_matches[0].event_id].pk,
'barcode': raw_barcode
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_REVOKED,
**common_checkin_args
)
if not simulate:
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[revoked_matches[0].event_id].pk,
'barcode': raw_barcode
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_REVOKED,
**common_checkin_args
)
return Response({
'status': 'error',
'reason': Checkin.REASON_REVOKED,
@@ -562,7 +567,9 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
else:
op_candidates = [media.linked_orderposition] + list(media.linked_orderposition.addons.all())
op_candidates = [media.linked_orderposition]
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
op_candidates += list(media.linked_orderposition.addons.all())
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
@@ -586,24 +593,25 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
# We choose the first match (regardless of product) for the logging since it's most likely to be the
# base product according to our order_by above.
op = op_candidates[0]
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': Checkin.REASON_AMBIGUOUS,
'reason_explanation': None,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[op.order.event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_AMBIGUOUS,
error_explanation=None,
**common_checkin_args,
)
if not simulate:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': Checkin.REASON_AMBIGUOUS,
'reason_explanation': None,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[op.order.event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_AMBIGUOUS,
error_explanation=None,
**common_checkin_args,
)
return Response({
'status': 'error',
'reason': Checkin.REASON_AMBIGUOUS,
@@ -650,6 +658,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
raw_barcode=raw_barcode_for_checkin,
raw_source_type=source_type,
from_revoked_secret=from_revoked_secret,
simulate=simulate,
)
except RequiredQuestionsError as e:
return Response({
@@ -662,23 +671,24 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400)
except CheckInError as e:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': e.code,
'reason_explanation': e.reason,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
Checkin.objects.create(
position=op,
successful=False,
error_reason=e.code,
error_explanation=e.reason,
**common_checkin_args,
)
if not simulate:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': e.code,
'reason_explanation': e.reason,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
Checkin.objects.create(
position=op,
successful=False,
error_reason=e.code,
error_explanation=e.reason,
**common_checkin_args,
)
return Response({
'status': 'error',
'reason': e.code,

View File

@@ -42,6 +42,8 @@ class InitializationRequestSerializer(serializers.Serializer):
token = serializers.CharField(max_length=190)
hardware_brand = serializers.CharField(max_length=190)
hardware_model = serializers.CharField(max_length=190)
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
software_brand = serializers.CharField(max_length=190)
software_version = serializers.CharField(max_length=190)
info = serializers.JSONField(required=False, allow_null=True)
@@ -50,6 +52,8 @@ class InitializationRequestSerializer(serializers.Serializer):
class UpdateRequestSerializer(serializers.Serializer):
hardware_brand = serializers.CharField(max_length=190)
hardware_model = serializers.CharField(max_length=190)
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
software_brand = serializers.CharField(max_length=190)
software_version = serializers.CharField(max_length=190)
info = serializers.JSONField(required=False, allow_null=True)
@@ -93,9 +97,14 @@ class InitializeView(APIView):
if device.initialized:
raise ValidationError({'token': ['This initialization token has already been used.']})
if device.revoked:
raise ValidationError({'token': ['This initialization token has been revoked.']})
device.initialized = now()
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
device.os_name = serializer.validated_data.get('os_name')
device.os_version = serializer.validated_data.get('os_version')
device.software_brand = serializer.validated_data.get('software_brand')
device.software_version = serializer.validated_data.get('software_version')
device.info = serializer.validated_data.get('info')
@@ -117,6 +126,8 @@ class UpdateView(APIView):
device = request.auth
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
device.os_name = serializer.validated_data.get('os_name')
device.os_version = serializer.validated_data.get('os_version')
device.software_brand = serializer.validated_data.get('software_brand')
device.software_version = serializer.validated_data.get('software_version')
device.info = serializer.validated_data.get('info')

View File

@@ -47,11 +47,13 @@ from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
CartPosition, Device, Event, SeatCategoryMapping, TaxRule, TeamAPIToken,
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
TaxRule, TeamAPIToken,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.quotas import QuotaAvailability
@@ -69,6 +71,8 @@ with scopes_disabled():
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
search = django_filters.rest_framework.CharFilter(method='search_qs')
date_from = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
date_to = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
class Meta:
model = Event
@@ -334,6 +338,8 @@ with scopes_disabled():
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
search = django_filters.rest_framework.CharFilter(method='search_qs')
date_from = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
date_to = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
class Meta:
model = SubEvent
@@ -522,6 +528,54 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
super().perform_destroy(instance)
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = ItemMetaPropertiesSerializer
queryset = ItemMetaProperty.objects.none()
write_permission = 'can_change_event_settings'
def get_queryset(self):
qs = self.request.event.item_meta_properties.all()
return qs
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.item_meta_property.deleted',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk}
)
instance.delete()
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.item_meta_property.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.item_meta_property.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
class EventSettingsView(views.APIView):
permission = None
write_permission = 'can_change_event_settings'

View File

@@ -133,7 +133,12 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
def exporters(self):
exporters = []
responses = register_data_exporters.send(self.request.event)
for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
raw_exporters = [
ex for ex in raw_exporters
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
return exporters
@@ -166,7 +171,7 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
if (
not isinstance(ex, OrganizerLevelExportMixin) or
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
)
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex, events=events)

View File

@@ -39,7 +39,8 @@ from pretix.api.serializers.media import (
)
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
Checkin, GiftCard, GiftCardTransaction, OrderPosition, ReusableMedium,
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
ReusableMedium,
)
from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts
@@ -135,12 +136,28 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
s = self.get_serializer(m)
return Response({"result": s.data})
except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"])
if mt:
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
if m:
s = self.get_serializer(m)
return Response({"result": s.data})
try:
with scopes_disabled():
m = ReusableMedium.objects.get(
organizer__in=GiftCardAcceptance.objects.filter(
acceptor=request.organizer,
active=True,
reusable_media=True,
).values_list('issuer', flat=True),
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
)
m.linked_orderposition = None # not relevant for cross-organizer
m.customer = None # not relevant for cross-organizer
s = self.get_serializer(m)
return Response({"result": s.data})
except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"])
if mt:
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
if m:
s = self.get_serializer(m)
return Response({"result": s.data})
return Response({"result": None})

View File

@@ -23,9 +23,9 @@ import datetime
import mimetypes
import os
from decimal import Decimal
from zoneinfo import ZoneInfo
import django_filters
import pytz
from django.db import transaction
from django.db.models import (
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
@@ -612,7 +612,7 @@ class OrderViewSet(viewsets.ModelViewSet):
status=status.HTTP_400_BAD_REQUEST
)
tz = pytz.timezone(self.request.event.settings.timezone)
tz = ZoneInfo(self.request.event.settings.timezone)
new_date = make_aware(datetime.datetime.combine(
new_date,
datetime.time(hour=23, minute=59, second=59)
@@ -661,7 +661,16 @@ class OrderViewSet(viewsets.ModelViewSet):
with language(order.locale, self.request.event.settings.region):
payment = order.payments.last()
# OrderCreateSerializer creates at most one payment
if payment and payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
order.log_action(
'pretix.event.order.payment.confirmed', {
'local_id': payment.local_id,
'provider': payment.provider,
},
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
)
order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID:
order_paid.send(self.request.event, order=order)

View File

@@ -155,7 +155,9 @@ class GiftCardViewSet(viewsets.ModelViewSet):
qs = self.request.organizer.accepted_gift_cards
else:
qs = self.request.organizer.issued_gift_cards.all()
return qs
return qs.prefetch_related(
'issuer'
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -166,7 +168,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
value = serializer.validated_data.pop('value')
inst = serializer.save(issuer=self.request.organizer)
inst.transactions.create(value=value)
inst.transactions.create(value=value, acceptor=self.request.organizer)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
@@ -179,18 +181,32 @@ class GiftCardViewSet(viewsets.ModelViewSet):
if 'include_accepted' in self.request.GET:
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
old_value = serializer.instance.value
value = serializer.validated_data.pop('value')
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode)
diff = value - old_value
inst.transactions.create(value=diff)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': diff}
)
value = serializer.validated_data.pop('value', None)
if any(k != 'value' for k in self.request.data):
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode)
inst.log_action(
'pretix.giftcards.modified',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
else:
inst = serializer.instance
if 'value' in self.request.data and value is not None:
old_value = serializer.instance.value
diff = value - old_value
inst.transactions.create(value=diff, acceptor=self.request.organizer)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': diff}
)
return inst
@action(detail=True, methods=["POST"])
@@ -203,18 +219,21 @@ class GiftCardViewSet(viewsets.ModelViewSet):
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
request.data.get('text', '')
)
info = serializers.JSONField(required=False, allow_null=True).to_internal_value(
request.data.get('info', {})
)
if gc.value + value < Decimal('0.00'):
return Response({
'value': ['The gift card does not have sufficient credit for this operation.']
}, status=status.HTTP_409_CONFLICT)
gc.transactions.create(value=value, text=text)
gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer)
gc.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': value, 'text': text}
)
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
def perform_destroy(self, instance):
raise MethodNotAllowed("Gift cards cannot be deleted.")
@@ -235,7 +254,7 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
return get_object_or_404(qs, pk=self.kwargs.get('giftcard'))
def get_queryset(self):
return self.giftcard.transactions.select_related('order', 'order__event')
return self.giftcard.transactions.select_related('order', 'order__event').prefetch_related('acceptor')
class TeamViewSet(viewsets.ModelViewSet):

View File

@@ -189,6 +189,19 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
return d
class ParametrizedWaitingListEntryWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
return {
'notification_id': logentry.pk,
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
'waitinglistentry': logentry.object_id,
'action': logentry.action_type,
}
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
def register_default_webhook_events(sender, **kwargs):
return (
@@ -321,6 +334,22 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.testmode.deactivated',
_('Test-Mode of shop has been deactivated'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.added',
_('Waiting list entry added'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.changed',
_('Waiting list entry changed'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.deleted',
_('Waiting list entry deleted'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.voucher_assigned',
_('Waiting list entry received voucher'),
),
)

View File

@@ -117,13 +117,15 @@ def oidc_validate_and_complete_config(config):
scopes=", ".join(provider_config.get("scopes_supported", []))
))
for k, v in config.items():
if k.endswith('_field') and v:
if v not in provider_config.get("claims_supported", []): # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
field=v,
fields=", ".join(provider_config.get("claims_supported", []))
))
if "claims_supported" in provider_config:
claims_supported = provider_config.get("claims_supported", [])
for k, v in config.items():
if k.endswith('_field') and v:
if v not in claims_supported: # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
field=v,
fields=", ".join(provider_config.get("claims_supported", []))
))
config['provider_config'] = provider_config
return config

View File

@@ -37,8 +37,8 @@ import tempfile
from collections import OrderedDict, namedtuple
from decimal import Decimal
from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import pytz
from defusedcsv import csv
from django import forms
from django.conf import settings
@@ -68,7 +68,7 @@ class BaseExporter:
self.events = event
self.event = None
e = self.events.first()
self.timezone = e.timezone if e else pytz.timezone(settings.TIME_ZONE)
self.timezone = e.timezone if e else ZoneInfo(settings.TIME_ZONE)
else:
self.events = Event.objects.filter(pk=event.pk)
self.timezone = event.timezone
@@ -157,6 +157,13 @@ class BaseExporter:
"""
raise NotImplementedError() # NOQA
def available_for_user(self, user) -> bool:
"""
Allows to do additional checks whether an exporter is available based on the user who calls it. Note that
``user`` may be ``None`` e.g. during API usage.
"""
return True
class OrganizerLevelExportMixin:
@property

View File

@@ -58,6 +58,7 @@ class EventDataExporter(ListExporter):
_("Short form"),
_("Shop is live"),
_("Event currency"),
_("Timezone"),
_("Event start time"),
_("Event end time"),
_("Admission time"),
@@ -75,16 +76,18 @@ class EventDataExporter(ListExporter):
for e in self.events.all():
m = e.meta_data
tz = e.timezone
yield [
str(e.name),
e.slug,
_('Yes') if e.live else _('No'),
e.currency,
date_format(e.date_from, 'SHORT_DATETIME_FORMAT'),
date_format(e.date_to, 'SHORT_DATETIME_FORMAT') if e.date_to else '',
date_format(e.date_admission, 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
date_format(e.presale_start, 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
date_format(e.presale_end, 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
str(e.timezone),
date_format(e.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
date_format(e.date_to.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_to else '',
date_format(e.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
date_format(e.presale_start.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
date_format(e.presale_end.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
str(e.location),
e.geo_lat or '',
e.geo_lon or '',

View File

@@ -103,7 +103,9 @@ class InvoiceExporterMixin:
qs = qs.annotate(
has_payment_with_provider=Exists(
OrderPayment.objects.filter(
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider'))
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider')),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
)
)
)

View File

@@ -34,8 +34,8 @@
from collections import OrderedDict
from decimal import Decimal
from zoneinfo import ZoneInfo
import pytz
from django import forms
from django.db.models import (
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
@@ -49,18 +49,24 @@ from django.utils.timezone import get_current_timezone, now
from django.utils.translation import (
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
)
from openpyxl.cell import WriteOnlyCell
from openpyxl.comments import Comment
from openpyxl.styles import Font, PatternFill
from pretix.base.models import (
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
OrderPosition, Question,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.models.orders import (
OrderFee, OrderPayment, OrderRefund, Transaction,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
@@ -320,7 +326,7 @@ class OrderListExporter(MultiSheetListExporter):
yield self.ProgressSetTotal(total=qs.count())
for order in qs.order_by('datetime').iterator():
tz = pytz.timezone(self.event_object_cache[order.event_id].settings.timezone)
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
@@ -453,7 +459,7 @@ class OrderListExporter(MultiSheetListExporter):
yield self.ProgressSetTotal(total=qs.count())
for op in qs.order_by('order__datetime').iterator():
order = op.order
tz = pytz.timezone(order.event.settings.timezone)
tz = ZoneInfo(order.event.settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
order.code,
@@ -625,7 +631,7 @@ class OrderListExporter(MultiSheetListExporter):
for op in ops:
order = op.order
tz = pytz.timezone(self.event_object_cache[order.event_id].settings.timezone)
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
order.code,
@@ -759,6 +765,185 @@ class OrderListExporter(MultiSheetListExporter):
return '{}_orders'.format(self.event.slug)
class TransactionListExporter(ListExporter):
identifier = 'transactions'
verbose_name = gettext_lazy('Order transaction data')
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all substantial changes to orders, i.e. all changes to '
'products, prices or tax rates. The information is only accurate for changes made with '
'pretix versions released after October 2021.')
@cached_property
def providers(self):
return dict(get_all_payment_providers())
@property
def additional_form_fields(self):
d = [
('date_range',
DateFrameField(
label=_('Date range'),
include_future_frames=False,
required=False,
help_text=_('Only include transactions created within this date range.')
)),
]
d = OrderedDict(d)
return d
@cached_property
def event_object_cache(self):
return {e.pk: e for e in self.events}
def get_filename(self):
if self.is_multievent:
return '{}_transactions'.format(self.organizer.slug)
else:
return '{}_transactions'.format(self.event.slug)
def iterate_list(self, form_data):
qs = Transaction.objects.filter(
order__event__in=self.events,
)
if form_data.get('date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
if dt_start:
qs = qs.filter(datetime__gte=dt_start)
if dt_end:
qs = qs.filter(datetime__lt=dt_end)
qs = qs.select_related(
'order', 'order__event', 'item', 'variation', 'subevent',
).order_by(
'datetime', 'id',
)
headers = [
_('Event'),
_('Event slug'),
_('Currency'),
_('Order code'),
_('Order date'),
_('Order time'),
_('Transaction date'),
_('Transaction time'),
_('Old data'),
_('Position ID'),
_('Quantity'),
_('Product ID'),
_('Product'),
_('Variation ID'),
_('Variation'),
_('Fee type'),
_('Internal fee type'),
pgettext('subevent', 'Date ID'),
pgettext('subevent', 'Date'),
_('Price'),
_('Tax rate'),
_('Tax rule ID'),
_('Tax rule'),
_('Tax value'),
_('Gross total'),
_('Tax total'),
]
if form_data.get('_format') == 'xlsx':
for i in range(len(headers)):
headers[i] = WriteOnlyCell(self.__ws, value=headers[i])
if i in (0, 12, 14, 18, 22):
headers[i].fill = PatternFill(start_color="FFB419", end_color="FFB419", fill_type="solid")
headers[i].comment = Comment(
text=_(
"This value is supplied for informational purposes, it is not part of the original transaction "
"data and might have changed since the transaction."
),
author='system'
)
headers[i].font = Font(bold=True)
yield headers
yield self.ProgressSetTotal(total=qs.count())
for t in qs.iterator():
row = [
str(t.order.event.name),
t.order.event.slug,
t.order.event.currency,
t.order.code,
t.order.datetime.astimezone(self.timezone).strftime('%Y-%m-%d'),
t.order.datetime.astimezone(self.timezone).strftime('%H:%M:%S'),
t.datetime.astimezone(self.timezone).strftime('%Y-%m-%d'),
t.datetime.astimezone(self.timezone).strftime('%H:%M:%S'),
_('Converted from legacy version') if t.migrated else '',
t.positionid,
t.count,
t.item_id,
str(t.item),
t.variation_id or '',
str(t.variation) if t.variation_id else '',
t.fee_type,
t.internal_type,
t.subevent_id or '',
str(t.subevent) if t.subevent else '',
t.price,
t.tax_rate,
t.tax_rule_id or '',
str(t.tax_rule.internal_name or t.tax_rule.name) if t.tax_rule_id else '',
t.tax_value,
t.price * t.count,
t.tax_value * t.count,
]
if form_data.get('_format') == 'xlsx':
for i in range(len(row)):
if t.order.testmode:
row[i] = WriteOnlyCell(self.__ws, value=remove_invalid_excel_chars(row[i]))
row[i].fill = PatternFill(start_color="FFB419", end_color="FFB419", fill_type="solid")
yield row
def prepare_xlsx_sheet(self, ws):
self.__ws = ws
ws.freeze_panes = 'A2'
ws.column_dimensions['A'].width = 25
ws.column_dimensions['B'].width = 10
ws.column_dimensions['C'].width = 10
ws.column_dimensions['D'].width = 10
ws.column_dimensions['E'].width = 15
ws.column_dimensions['F'].width = 15
ws.column_dimensions['G'].width = 15
ws.column_dimensions['H'].width = 15
ws.column_dimensions['I'].width = 15
ws.column_dimensions['J'].width = 10
ws.column_dimensions['K'].width = 10
ws.column_dimensions['L'].width = 10
ws.column_dimensions['M'].width = 25
ws.column_dimensions['N'].width = 10
ws.column_dimensions['O'].width = 25
ws.column_dimensions['P'].width = 20
ws.column_dimensions['Q'].width = 20
ws.column_dimensions['R'].width = 10
ws.column_dimensions['S'].width = 25
ws.column_dimensions['T'].width = 15
ws.column_dimensions['U'].width = 10
ws.column_dimensions['V'].width = 10
ws.column_dimensions['W'].width = 20
ws.column_dimensions['X'].width = 15
class PaymentListExporter(ListExporter):
identifier = 'paymentlist'
verbose_name = gettext_lazy('Payments and refunds')
@@ -843,7 +1028,7 @@ class PaymentListExporter(ListExporter):
yield self.ProgressSetTotal(total=len(objs))
for obj in objs:
tz = pytz.timezone(obj.order.event.settings.timezone)
tz = ZoneInfo(obj.order.event.settings.timezone)
if isinstance(obj, OrderPayment) and obj.payment_date:
d2 = obj.payment_date.astimezone(tz).date().strftime('%Y-%m-%d')
elif isinstance(obj, OrderRefund) and obj.execution_date:
@@ -962,7 +1147,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
def iterate_list(self, form_data):
qs = GiftCardTransaction.objects.filter(
card__issuer=self.organizer,
).order_by('datetime').select_related('card', 'order', 'order__event')
).order_by('datetime').select_related('card', 'order', 'order__event', 'acceptor')
if form_data.get('date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
@@ -978,6 +1163,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
_('Amount'),
_('Currency'),
_('Order'),
_('Organizer'),
]
yield headers
@@ -989,6 +1175,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
obj.value,
obj.card.currency,
obj.order.full_code if obj.order else None,
str(obj.acceptor or ""),
]
yield row
@@ -1022,7 +1209,7 @@ class GiftcardRedemptionListExporter(ListExporter):
yield headers
for obj in objs:
tz = pytz.timezone(obj.order.event.settings.timezone)
tz = ZoneInfo(obj.order.event.settings.timezone)
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
row = [
obj.order.event.slug,
@@ -1165,6 +1352,16 @@ def register_multievent_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_ordertransactionlist")
def register_ordertransactionlist_exporter(sender, **kwargs):
return TransactionListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_ordertransactionlist")
def register_multievent_ordertransactionlist_exporter(sender, **kwargs):
return TransactionListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
def register_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter

View File

@@ -20,8 +20,8 @@
# <https://www.gnu.org/licenses/>.
#
from collections import OrderedDict
from zoneinfo import ZoneInfo
import pytz
from django import forms
from django.db.models import F, Q
from django.dispatch import receiver
@@ -137,7 +137,7 @@ class WaitingListExporter(ListExporter):
# which event should be used to output dates in columns "Start date" and "End date"
event_for_date_columns = entry.subevent if entry.subevent else entry.event
tz = pytz.timezone(entry.event.settings.timezone)
tz = ZoneInfo(entry.event.settings.timezone)
datetime_format = '%Y-%m-%d %H:%M:%S'
row = [

View File

@@ -167,6 +167,7 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
class PrefixForm(forms.Form):
prefix = forms.CharField(widget=forms.HiddenInput)
template_name = "django/forms/table.html"
class SafeSessionWizardView(SessionWizardView):

View File

@@ -38,10 +38,10 @@ import logging
from datetime import timedelta
from decimal import Decimal
from io import BytesIO
from zoneinfo import ZoneInfo
import dateutil.parser
import pycountry
import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
@@ -61,6 +61,7 @@ from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import countries
from django_countries.fields import Country, CountryField
from geoip2.errors import AddressNotFoundError
from phonenumber_field.formfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber
from phonenumber_field.widgets import PhoneNumberPrefixWidget
@@ -356,9 +357,12 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def guess_country_from_request(request, event):
if settings.HAS_GEOIP:
g = GeoIP2()
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:
return Country(res['country_code'])
try:
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:
return Country(res['country_code'])
except AddressNotFoundError:
pass
return guess_country(event)
@@ -573,6 +577,7 @@ class BaseQuestionsForm(forms.Form):
the attendee name for admission tickets, if the corresponding setting is enabled,
as well as additional questions defined by the organizer.
"""
address_validation = False
def __init__(self, *args, **kwargs):
"""
@@ -732,7 +737,7 @@ class BaseQuestionsForm(forms.Form):
initial = answers[0]
else:
initial = None
tz = pytz.timezone(event.settings.timezone)
tz = ZoneInfo(event.settings.timezone)
help_text = rich_text(q.help_text)
label = escape(q.question) # django-bootstrap3 calls mark_safe
required = q.required and not self.all_optional
@@ -920,8 +925,14 @@ class BaseQuestionsForm(forms.Form):
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
def clean(self):
from pretix.base.addressvalidation import \
validate_address # local import to prevent impact on startup time
d = super().clean()
if self.address_validation:
self.cleaned_data = d = validate_address(d, True)
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.'))

View File

@@ -0,0 +1,63 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from bootstrap3.renderers import (
FieldRenderer as BaseFieldRenderer,
InlineFieldRenderer as BaseInlineFieldRenderer,
)
from django.forms import (
CheckboxInput, CheckboxSelectMultiple, ClearableFileInput, RadioSelect,
SelectDateWidget,
)
class FieldRenderer(BaseFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html
class InlineFieldRenderer(BaseInlineFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html

View File

@@ -20,6 +20,8 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import re
import unicodedata
from collections import defaultdict
from decimal import Decimal
from io import BytesIO
@@ -28,6 +30,7 @@ from typing import Tuple
import bleach
import vat_moss.exchange_rates
from bidi.algorithm import get_display
from django.contrib.staticfiles import finders
from django.db.models import Sum
from django.dispatch import receiver
@@ -53,7 +56,8 @@ from pretix.base.models import Event, Invoice, Order, OrderPayment
from pretix.base.services.currencies import SOURCE_NAMES
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ThumbnailingImageReader
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
@@ -79,7 +83,12 @@ class NumberedCanvas(Canvas):
def draw_page_number(self, page_count):
self.saveState()
self.setFont(self.font_regular, 8)
self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,))
text = pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,)
try:
text = get_display(reshaper.reshape(text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, text)
self.restoreState()
@@ -139,8 +148,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
"""
self.stylesheet = self._get_stylesheet()
self._register_fonts()
self.stylesheet = self._get_stylesheet()
def _get_stylesheet(self):
"""
@@ -148,6 +157,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='Bold', fontName=self.font_bold, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='BoldRight', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT))
stylesheet.add(ParagraphStyle(name='BoldRightNoSplit', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT,
splitLongWords=False))
stylesheet.add(ParagraphStyle(name='NormalRight', fontName=self.font_regular, fontSize=10, leading=12, alignment=TA_RIGHT))
stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
textColor=colors.white, alignment=TA_CENTER))
@@ -155,6 +168,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10))
stylesheet.add(ParagraphStyle(name='FineprintRight', fontName=self.font_regular, fontSize=8, leading=10, alignment=TA_RIGHT))
return stylesheet
def _register_fonts(self):
@@ -168,6 +182,32 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
for family, styles in get_fonts().items():
if family == self.event.settings.invoice_renderer_font:
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
self.font_regular = family
if 'italic' in styles:
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
if 'bold' in styles:
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
self.font_bold = family + ' B'
if 'bolditalic' in styles:
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
def _normalize(self, text):
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
def _upper(self, val):
# We uppercase labels, but not in every language
if get_language().startswith('el'):
@@ -247,10 +287,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None):
return bleach.clean(
return self._normalize(bleach.clean(
text,
tags=tags or []
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
class PaidMarker(Flowable):
@@ -291,7 +331,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
canvas.restoreState()
@@ -324,13 +364,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_invoice_from_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Invoice from')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice from'))))
canvas.drawText(textobject)
def _draw_invoice_to_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Invoice to')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice to'))))
canvas.drawText(textobject)
logo_width = 25 * mm
@@ -358,51 +398,51 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_metadata(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Order code')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Order code'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.order.full_code)
textobject.textLine(self._normalize(self.invoice.order.full_code))
canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8)
if self.invoice.is_cancellation:
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation number')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation number'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
textobject.textLine(self._normalize(self.invoice.number))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.refers.number)
textobject.textLine(self._normalize(self.invoice.refers.number))
else:
textobject.textLine(self._upper(pgettext('invoice', 'Invoice number')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice number'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
textobject.textLine(self._normalize(self.invoice.number))
textobject.moveCursor(0, 5)
if self.invoice.is_cancellation:
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation date')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation date'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice date')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice date'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
textobject.textLine(self._normalize(date_format(self.invoice.refers.date, "DATE_FORMAT")))
textobject.moveCursor(0, 5)
else:
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Invoice date')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice date'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
textobject.moveCursor(0, 5)
canvas.drawText(textobject)
@@ -415,19 +455,19 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event_label(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event'))))
canvas.drawText(textobject)
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=[]).strip()
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
return txt
@@ -453,7 +493,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else:
p_str = shorten(self.invoice.event.name)
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p = Paragraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
@@ -462,14 +502,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_footer(self, canvas):
canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
def _draw_testmode(self, canvas):
if self.invoice.order.testmode:
canvas.saveState()
canvas.setFont('OpenSansBd', 30)
canvas.setFont(self.font_bold, 30)
canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, self._normalize(gettext('TEST MODE')))
canvas.restoreState()
def _on_first_page(self, canvas: Canvas, doc):
@@ -517,22 +557,22 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference),
),
)),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_vat_id:
story.append(Paragraph(
pgettext('invoice', 'Customer VAT ID') + ': ' +
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
self._clean_text(self.invoice.invoice_to_vat_id),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(Paragraph(
pgettext('invoice', 'Beneficiary') + ':<br />' +
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal']
))
@@ -552,10 +592,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [
NextPageTemplate('FirstPage'),
Paragraph(
(
self._normalize(
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
self.stylesheet['Heading1']
),
Spacer(1, 5 * mm),
@@ -577,17 +617,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
]
if has_taxes:
tdata = [(
pgettext('invoice', 'Description'),
pgettext('invoice', 'Qty'),
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net'),
pgettext('invoice', 'Gross'),
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
)]
else:
tdata = [(
pgettext('invoice', 'Description'),
pgettext('invoice', 'Qty'),
pgettext('invoice', 'Amount'),
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
)]
def _group_key(line):
@@ -634,13 +674,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes:
tdata.append([
pgettext('invoice', 'Invoice total'), '', '', '',
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
tdata.append([
pgettext('invoice', 'Invoice total'), '',
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.65, .20, .15)]
@@ -649,12 +689,16 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum - total, self.invoice.event.currency)
])
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum, self.invoice.event.currency)
])
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)]
)
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)]
)
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
@@ -667,19 +711,24 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
).aggregate(
s=Sum('amount')
)['s'] or Decimal('0.00')
tdata.append([pgettext('invoice', 'Paid by gift card')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(giftcard_sum, self.invoice.event.currency)
])
tdata.append([pgettext('invoice', 'Remaining amount')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(total - giftcard_sum, self.invoice.event.currency)
])
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)]
)
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
)
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
elif self.invoice.payment_provider_stamp:
pm = PaidMarker(
text=self.invoice.payment_provider_stamp,
text=self._normalize(self.invoice.payment_provider_stamp),
color=colors.HexColor(self.event.settings.theme_color_success),
font=self.font_bold,
size=16
)
tdata[-1][-2] = pm
@@ -692,7 +741,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.payment_provider_text:
story.append(Paragraph(
self.invoice.payment_provider_text,
self._normalize(self.invoice.payment_provider_text),
self.stylesheet['Normal']
))
@@ -716,10 +765,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]
thead = [
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net value'),
pgettext('invoice', 'Gross value'),
pgettext('invoice', 'Tax'),
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
Paragraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
Paragraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
Paragraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
''
]
tdata = [thead]
@@ -730,7 +779,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue
tax = taxvalue_map[idx]
tdata.append([
localize(rate) + " % " + name,
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency),
@@ -749,7 +798,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
Paragraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
table
]))
@@ -766,7 +815,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax
tdata.append([
localize(rate) + " % " + name,
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), ''
])
@@ -776,12 +825,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([
Spacer(1, height=2 * mm),
Paragraph(
pgettext(
self._normalize(pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
self.stylesheet['Fineprint']
),
Spacer(1, height=3 * mm),
@@ -790,14 +839,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm))
story.append(Paragraph(
story.append(Paragraph(self._normalize(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total)),
total=fmt(foreign_total))),
self.stylesheet['Fineprint']
))
@@ -843,7 +892,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
self._clean_text(l)
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
p = Paragraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas)
@@ -859,8 +908,12 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _get_first_page_frames(self, doc):
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
if self.event.settings.invoice_renderer_highlight_order_code:
margin_top = 100 * mm
else:
margin_top = 95 * mm
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 95 * mm,
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - margin_top,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
@@ -871,25 +924,35 @@ class Modern1Renderer(ClassicInvoiceRenderer):
# the font size until it fits.
begin_top = 100 * mm
def _draw(label, value, value_size, x, width):
def _draw(label, value, value_size, x, width, bold=False, sublabel=None):
if canvas.stringWidth(value, self.font_regular, value_size) > width and value_size > 6:
return False
textobject = canvas.beginText(x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(label)
textobject.textLine(self._normalize(label))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, value_size)
textobject.textLine(value)
textobject.setFont(self.font_bold if bold else self.font_regular, value_size)
textobject.textLine(self._normalize(value))
if sublabel:
textobject.moveCursor(0, 1)
textobject.setFont(self.font_regular, 8)
textobject.textLine(self._normalize(sublabel))
return textobject
value_size = 10
while value_size >= 5:
if self.event.settings.invoice_renderer_highlight_order_code:
kwargs = dict(bold=True, sublabel=pgettext('invoice', '(Please quote at all times.)'))
else:
kwargs = {}
objects = [
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm)
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
]
p = Paragraph(
date_format(self.invoice.date, "DATE_FORMAT"),
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
)
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -920,9 +983,9 @@ class Modern1Renderer(ClassicInvoiceRenderer):
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
if self.invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation date'))
textobject.textLine(self._normalize(pgettext('invoice', 'Cancellation date')))
else:
textobject.textLine(pgettext('invoice', 'Invoice date'))
textobject.textLine(self._normalize(pgettext('invoice', 'Invoice date')))
canvas.drawText(textobject)

View File

@@ -24,7 +24,7 @@ Django, for theoretically very valid reasons, creates migrations for *every sing
we change on a model. Even the `help_text`! This makes sense, as we don't know if any
database backend unknown to us might actually use this information for its database schema.
However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
However, pretix only supports PostgreSQL and SQLite and we can be pretty
certain that some changes to models will never require a change to the database. In this case,
not creating a migration for certain changes will save us some performance while applying them
*and* allow for a cleaner git history. Win-win!

View File

@@ -22,7 +22,7 @@
import json
import sys
import pytz
import pytz_deprecation_shim
from django.core.management.base import BaseCommand
from django.utils.timezone import override
from django_scopes import scope
@@ -60,7 +60,7 @@ class Command(BaseCommand):
sys.exit(1)
locale = options.get("locale", None)
timezone = pytz.timezone(options['timezone']) if options.get('timezone') else None
timezone = pytz_deprecation_shim.timezone(options['timezone']) if options.get('timezone') else None
with scope(organizer=o):
if options['event_slug']:

View File

@@ -51,7 +51,7 @@ class Command(BaseCommand):
del options['skip_checks']
del options['print_sql']
if options['print_sql']:
if options.get('print_sql'):
connection.force_debug_cursor = True
logger = logging.getLogger("django.db.backends")
logger.setLevel(logging.DEBUG)

View File

@@ -21,8 +21,8 @@
#
from collections import OrderedDict
from urllib.parse import urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import pytz
from django.conf import settings
from django.http import Http404, HttpRequest, HttpResponse
from django.middleware.common import CommonMiddleware
@@ -98,9 +98,9 @@ class LocaleMiddleware(MiddlewareMixin):
tzname = request.user.timezone
if tzname:
try:
timezone.activate(pytz.timezone(tzname))
timezone.activate(ZoneInfo(tzname))
request.timezone = tzname
except pytz.UnknownTimeZoneError:
except ZoneInfoNotFoundError:
pass
else:
timezone.deactivate()
@@ -249,7 +249,7 @@ class SecurityMiddleware(MiddlewareMixin):
h = {
'default-src': ["{static}"],
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com', 'https://pay.google.com'],
'object-src': ["'none'"],
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'style-src': ["{static}", "{media}"],

View File

@@ -2,6 +2,8 @@
# Generated by Django 1.10.4 on 2017-02-03 14:21
from __future__ import unicode_literals
from zoneinfo import ZoneInfo
import django.core.validators
import django.db.migrations.operations.special
import django.db.models.deletion
@@ -26,7 +28,7 @@ def forwards42(apps, schema_editor):
for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value')
}
for order in Order.objects.all():
tz = pytz.timezone(etz.get(order.event_id, 'UTC'))
tz = ZoneInfo(etz.get(order.event_id, 'UTC'))
order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59)
order.save()

View File

@@ -2,9 +2,9 @@
# Generated by Django 1.10.2 on 2016-10-19 17:57
from __future__ import unicode_literals
import pytz
from zoneinfo import ZoneInfo
from django.db import migrations
from django.utils import timezone
def forwards(apps, schema_editor):
@@ -15,7 +15,7 @@ def forwards(apps, schema_editor):
for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value')
}
for order in Order.objects.all():
tz = pytz.timezone(etz.get(order.event_id, 'UTC'))
tz = ZoneInfo(etz.get(order.event_id, 'UTC'))
order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59)
order.save()

View File

@@ -3,7 +3,6 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import migrations, models
from django_mysql.checks import mysql_connections
def set_attendee_name_parts(apps, schema_editor):
@@ -24,40 +23,12 @@ def set_attendee_name_parts(apps, schema_editor):
ia.save(update_fields=['name_parts'])
def check_mysqlversion(apps, schema_editor):
errors = []
any_conn_works = False
conns = list(mysql_connections())
found = 'Unknown version'
for alias, conn in conns:
if hasattr(conn, 'mysql_is_mariadb') and conn.mysql_is_mariadb and hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (10, 2, 7):
any_conn_works = True
else:
found = 'MariaDB ' + '.'.join(str(v) for v in conn.mysql_version)
elif hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (5, 7):
any_conn_works = True
else:
found = 'MySQL ' + '.'.join(str(v) for v in conn.mysql_version)
if conns and not any_conn_works:
raise ImproperlyConfigured(
'As of pretix 2.2, you need MySQL 5.7+ or MariaDB 10.2.7+ to run pretix. However, we detected a '
'database connection to {}'.format(found)
)
return errors
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0101_auto_20181025_2255'),
]
operations = [
migrations.RunPython(
check_mysqlversion, migrations.RunPython.noop
),
migrations.RenameField(
model_name='cartposition',
old_name='attendee_name',

View File

@@ -1,8 +1,7 @@
# Generated by Django 3.2.4 on 2021-09-30 10:25
from datetime import datetime
from datetime import datetime, timezone
from django.db import migrations, models
from pytz import UTC
class Migration(migrations.Migration):
@@ -15,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='invoice',
name='sent_to_customer',
field=models.DateTimeField(blank=True, null=True, default=UTC.localize(datetime(1970, 1, 1, 0, 0, 0, 0))),
field=models.DateTimeField(blank=True, null=True, default=datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc)),
preserve_default=False,
),
]

View File

@@ -50,6 +50,6 @@ class Migration(migrations.Migration):
],
options={
'unique_together': {('event', 'secret')},
} if 'mysql' not in settings.DATABASES['default']['ENGINE'] else {}
}
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.18 on 2023-05-04 12:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0237_question_valid_string_length'),
]
operations = [
migrations.AddField(
model_name='giftcard',
name='owner_ticket',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='owned_gift_cards', to='pretixbase.orderposition'),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.18 on 2023-05-11 11:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0238_giftcard_owner_ticket'),
]
operations = [
migrations.AddField(
model_name='giftcardtransaction',
name='acceptor',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='gift_card_transactions', to='pretixbase.organizer'),
),
migrations.AddField(
model_name='giftcardtransaction',
name='info',
field=models.JSONField(null=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.18 on 2023-05-16 11:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0239_giftcard_info'),
]
operations = [
migrations.AddField(
model_name='voucher',
name='all_addons_included',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='voucher',
name='all_bundles_included',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.19 on 2023-05-25 15:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0240_auto_20230516_1119'),
]
operations = [
migrations.AddField(
model_name='itemmetaproperty',
name='allowed_values',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='itemmetaproperty',
name='required',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 3.2.18 on 2023-05-12 10:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0241_itemmetaproperties_required_values'),
]
operations = [
migrations.RenameField(
model_name='giftcardacceptance',
old_name='collector',
new_name='acceptor',
),
migrations.AddField(
model_name='giftcardacceptance',
name='active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='giftcardacceptance',
name='reusable_media',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='giftcardacceptance',
name='issuer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gift_card_acceptor_acceptance', to='pretixbase.organizer'),
),
migrations.AlterUniqueTogether(
name='giftcardacceptance',
unique_together={('issuer', 'acceptor')},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.1.9 on 2023-06-26 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0242_auto_20230512_1008'),
]
operations = [
migrations.AddField(
model_name='device',
name='os_name',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='device',
name='os_version',
field=models.CharField(max_length=190, null=True),
),
]

View File

@@ -178,7 +178,7 @@ class LoggedModel(models.Model, LoggingMixin):
return LogEntry.objects.filter(
content_type=self.logs_content_type, object_id=self.pk
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')
).select_related('user', 'event', 'event__organizer', 'oauth_application', 'api_token', 'device')
class LockModel:

View File

@@ -39,6 +39,7 @@ from pretix.base.models.fields import MultiStringField
from pretix.base.models.organizer import Organizer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.helpers.countries import FastCountryField
from pretix.helpers.names import build_name
class CustomerSSOProvider(LoggedModel):
@@ -120,14 +121,23 @@ class Customer(LoggedModel):
if self.email:
self.email = self.email.lower()
if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
kwargs['update_fields'] = {'last_modified'}.union(kwargs['update_fields'])
if not self.identifier:
self.assign_identifier()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
if self.name_parts:
self.name_cached = self.name
name = self.name
if self.name_cached != name:
self.name_cached = name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
else:
self.name_cached = ""
self.name_parts = {}
if self.name_cached != "" or self.name_parts != {}:
self.name_cached = ""
self.name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached', 'name_parts'}.union(kwargs['update_fields'])
super().save(**kwargs)
def anonymize(self):
@@ -171,15 +181,11 @@ class Customer(LoggedModel):
@property
def name(self):
if not self.name_parts:
return ""
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
raise TypeError("Invalid name given.")
return scheme['concatenation'](self.name_parts).strip()
return build_name(self.name_parts, fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
@property
def name_all_components(self):
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
def __str__(self):
s = f'#{self.identifier}'
@@ -302,15 +308,11 @@ class AttendeeProfile(models.Model):
@property
def attendee_name(self):
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
@property
def attendee_name_all_components(self):
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
@property
def state_name(self):

View File

@@ -98,6 +98,8 @@ class Gate(LoggedModel):
if not Gate.objects.filter(organizer=self.organizer, identifier=code).exists():
self.identifier = code
break
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
return super().save(*args, **kwargs)
@@ -141,6 +143,14 @@ class Device(LoggedModel):
max_length=190,
null=True, blank=True
)
os_name = models.CharField(
max_length=190,
null=True, blank=True
)
os_version = models.CharField(
max_length=190,
null=True, blank=True
)
software_brand = models.CharField(
max_length=190,
null=True, blank=True
@@ -173,6 +183,8 @@ class Device(LoggedModel):
def save(self, *args, **kwargs):
if not self.device_id:
self.device_id = (self.organizer.devices.aggregate(m=Max('device_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'device_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
def permission_set(self) -> set:

View File

@@ -40,8 +40,9 @@ from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta
from operator import attrgetter
from urllib.parse import urljoin
from zoneinfo import ZoneInfo
import pytz
import pytz_deprecation_shim
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
@@ -214,7 +215,7 @@ class EventMixin:
@property
def timezone(self):
return pytz.timezone(self.settings.timezone)
return pytz_deprecation_shim.timezone(self.settings.timezone)
@property
def effective_presale_end(self):
@@ -290,19 +291,19 @@ class EventMixin:
return safe_string(json.dumps(eventdict))
@classmethod
def annotated(cls, qs, channel='web'):
def annotated(cls, qs, channel='web', voucher=None):
from pretix.base.models import Item, ItemVariation, Quota
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel).filter(
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
Q(variations__isnull=True)
& Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
sq_active_variation = ItemVariation.objects.filter(
q_variation = (
Q(active=True)
& Q(sales_channels__contains=channel)
& Q(hide_without_voucher=False)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(item__active=True)
@@ -310,10 +311,23 @@ class EventMixin:
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
& Q(item__sales_channels__contains=channel)
& Q(item__hide_without_voucher=False)
& Q(item__require_bundling=False)
& Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
)
if voucher:
if voucher.variation_id:
q_variation &= Q(pk=voucher.variation_id)
elif voucher.item_id:
q_variation &= Q(item_id=voucher.item_id)
elif voucher.quota_id:
q_variation &= Q(quotas__in=[voucher.quota_id])
if not voucher or not voucher.show_hidden_items:
q_variation &= Q(hide_without_voucher=False)
q_variation &= Q(item__hide_without_voucher=False)
sq_active_variation = ItemVariation.objects.filter(q_variation).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
@@ -625,6 +639,7 @@ class Event(EventMixin, LoggedModel):
"""
self.settings.invoice_renderer = 'modern1'
self.settings.invoice_include_expire_date = True
self.settings.invoice_renderer_highlight_order_code = True
self.settings.ticketoutput_pdf__enabled = True
self.settings.ticketoutput_passbook__enabled = True
self.settings.event_list_type = 'calendar'
@@ -759,7 +774,7 @@ class Event(EventMixin, LoggedModel):
"""
The last datetime of payments for this event.
"""
tz = pytz.timezone(self.settings.timezone)
tz = ZoneInfo(self.settings.timezone)
return make_aware(datetime.combine(
self.settings.get('payment_term_last', as_type=RelativeDateWrapper).datetime(self).date(),
time(hour=23, minute=59, second=59)
@@ -1132,8 +1147,8 @@ class Event(EventMixin, LoggedModel):
irs = self.get_invoice_renderers()
return irs[self.settings.invoice_renderer]
def subevents_annotated(self, channel):
return SubEvent.annotated(self.subevents, channel)
def subevents_annotated(self, channel, voucher=None):
return SubEvent.annotated(self.subevents, channel, voucher)
def subevents_sorted(self, queryset):
ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
@@ -1262,6 +1277,9 @@ class Event(EventMixin, LoggedModel):
return not self.orders.exists() and not self.invoices.exists()
def delete_sub_objects(self):
from .checkin import Checkin
Checkin.all.filter(successful=False, list__event=self).delete()
self.cartposition_set.filter(addon_to__isnull=False).delete()
self.cartposition_set.all().delete()
self.vouchers.all().delete()
@@ -1453,10 +1471,10 @@ class SubEvent(EventMixin, LoggedModel):
return qs_annotated
@classmethod
def annotated(cls, qs, channel='web'):
def annotated(cls, qs, channel='web', voucher=None):
from .items import SubEventItem, SubEventItemVariation
qs = super().annotated(qs, channel)
qs = super().annotated(qs, channel, voucher=voucher)
qs = qs.annotate(
disabled_items=Coalesce(
Subquery(

View File

@@ -19,10 +19,11 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import zoneinfo
from datetime import datetime, timedelta
import pytz
from dateutil.rrule import rrulestr
from dateutil.tz import datetime_exists
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
@@ -108,12 +109,9 @@ class AbstractScheduledExport(LoggedModel):
self.schedule_next_run = None
return
try:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz)
except pytz.exceptions.AmbiguousTimeError:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz, is_dst=False)
except pytz.exceptions.NonExistentTimeError:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time) + timedelta(hours=1), tz)
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz)
if not datetime_exists(self.schedule_next_run):
self.schedule_next_run += timedelta(hours=1)
class ScheduledEventExport(AbstractScheduledExport):
@@ -136,4 +134,4 @@ class ScheduledOrganizerExport(AbstractScheduledExport):
@property
def tz(self):
return pytz.timezone(self.timezone)
return zoneinfo.ZoneInfo(self.timezone)

View File

@@ -25,7 +25,9 @@ from django.conf import settings
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Sum
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.html import format_html
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
@@ -44,14 +46,19 @@ def gen_giftcard_secret(length=8):
class GiftCardAcceptance(models.Model):
issuer = models.ForeignKey(
'Organizer',
related_name='gift_card_collector_acceptance',
related_name='gift_card_acceptor_acceptance',
on_delete=models.CASCADE
)
collector = models.ForeignKey(
acceptor = models.ForeignKey(
'Organizer',
related_name='gift_card_issuer_acceptance',
on_delete=models.CASCADE
)
active = models.BooleanField(default=True)
reusable_media = models.BooleanField(default=False)
class Meta:
unique_together = (('issuer', 'acceptor'),)
class GiftCard(LoggedModel):
@@ -66,6 +73,13 @@ class GiftCard(LoggedModel):
on_delete=models.PROTECT,
null=True, blank=True
)
owner_ticket = models.ForeignKey(
'OrderPosition',
related_name='owned_gift_cards',
on_delete=models.PROTECT,
null=True, blank=True,
verbose_name=_('Owned by ticket holder')
)
issuance = models.DateTimeField(
auto_now_add=True,
)
@@ -105,7 +119,7 @@ class GiftCard(LoggedModel):
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
def accepted_by(self, organizer):
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, acceptor=organizer, active=True).exists()
def save(self, *args, **kwargs):
if not self.secret:
@@ -153,6 +167,61 @@ class GiftCardTransaction(models.Model):
on_delete=models.PROTECT
)
text = models.TextField(blank=True, null=True)
info = models.JSONField(
null=True, blank=True,
)
acceptor = models.ForeignKey(
'Organizer',
related_name='gift_card_transactions',
on_delete=models.PROTECT,
null=True, blank=True
)
class Meta:
ordering = ("datetime",)
def save(self, *args, **kwargs):
if not self.pk and not self.acceptor:
raise ValueError("`acceptor` should be set on all new gift card transactions.")
super().save(*args, **kwargs)
def display(self, customer_facing=True):
from ..signals import gift_card_transaction_display
for receiver, response in gift_card_transaction_display.send(self, transaction=self, customer_facing=customer_facing):
if response:
return response
if self.order_id:
if not self.text:
if not customer_facing:
return format_html(
'<a href="{}">{}</a>',
reverse(
"control:event.order",
kwargs={
"event": self.order.event.slug,
"organizer": self.order.event.organizer.slug,
"code": self.order.code,
}
),
self.order.full_code
)
return self.order.full_code
else:
return self.text
else:
if self.text:
return format_html(
'<em>{}:</em> {}',
_('Manual transaction'),
self.text,
)
else:
return _('Manual transaction')
def display_backend(self):
return self.display(customer_facing=False)
def display_presale(self):
return self.display(customer_facing=True)

View File

@@ -251,14 +251,20 @@ class Invoice(models.Model):
raise ValueError('Every invoice needs to be connected to an order')
if not self.event:
self.event = self.order.event
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'event'}.union(kwargs['update_fields'])
if not self.organizer:
self.organizer = self.order.event.organizer
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'organizer'}.union(kwargs['update_fields'])
if not self.prefix:
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
if self.is_cancellation:
self.prefix = self.event.settings.invoice_numbers_prefix_cancellations or self.prefix
if '%' in self.prefix:
self.prefix = self.date.strftime(self.prefix)
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'prefix'}.union(kwargs['update_fields'])
if not self.invoice_no:
if self.order.testmode:
@@ -276,8 +282,13 @@ class Invoice(models.Model):
# Suppress duplicate key errors and try again
if i == 9:
raise
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'invoice_no'}.union(kwargs['update_fields'])
self.full_invoice_no = self.prefix + self.invoice_no
if self.full_invoice_no != self.prefix + self.invoice_no:
self.full_invoice_no = self.prefix + self.invoice_no
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'full_invoice_no'}.union(kwargs['update_fields'])
return super().save(*args, **kwargs)
def delete(self, *args, **kwargs):

View File

@@ -40,9 +40,10 @@ from collections import Counter, OrderedDict
from datetime import date, datetime, time, timedelta
from decimal import Decimal, DecimalException
from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import dateutil.parser
import pytz
from dateutil.tz import datetime_exists
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import (
@@ -927,22 +928,22 @@ class Item(LoggedModel):
)
if self.validity_dynamic_duration_days:
replace_date += timedelta(days=self.validity_dynamic_duration_days)
valid_until = tz.localize(valid_until.replace(
valid_until = valid_until.replace(
year=replace_date.year,
month=replace_date.month,
day=replace_date.day,
hour=23, minute=59, second=59, microsecond=0,
tzinfo=None,
))
tzinfo=tz,
)
elif self.validity_dynamic_duration_days:
replace_date = valid_until.date() + timedelta(days=self.validity_dynamic_duration_days - 1)
valid_until = tz.localize(valid_until.replace(
valid_until = valid_until.replace(
year=replace_date.year,
month=replace_date.month,
day=replace_date.day,
hour=23, minute=59, second=59, microsecond=0,
tzinfo=None
))
tzinfo=tz
)
if self.validity_dynamic_duration_hours:
valid_until += timedelta(hours=self.validity_dynamic_duration_hours)
@@ -950,6 +951,9 @@ class Item(LoggedModel):
if self.validity_dynamic_duration_minutes:
valid_until += timedelta(minutes=self.validity_dynamic_duration_minutes)
if not datetime_exists(valid_until):
valid_until += timedelta(hours=1)
return requested_start, valid_until
else:
@@ -1589,6 +1593,8 @@ class Question(LoggedModel):
if not Question.objects.filter(event=self.event, identifier=code).exists():
self.identifier = code
break
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
@@ -1678,7 +1684,7 @@ class Question(LoggedModel):
try:
dt = dateutil.parser.parse(answer)
if is_naive(dt):
dt = make_aware(dt, pytz.timezone(self.event.settings.timezone))
dt = make_aware(dt, ZoneInfo(self.event.settings.timezone))
except:
raise ValidationError(_('Invalid datetime input.'))
else:
@@ -1736,6 +1742,8 @@ class QuestionOption(models.Model):
if not QuestionOption.objects.filter(question__event=self.question.event, identifier=code).exists():
self.identifier = code
break
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
@staticmethod
@@ -2001,6 +2009,15 @@ class ItemMetaProperty(LoggedModel):
verbose_name=_("Name"),
)
default = models.TextField(blank=True)
required = models.BooleanField(
default=False, verbose_name=_("Required for products"),
help_text=_("If checked, this property must be set in each product. Does not apply if a default value is set.")
)
allowed_values = models.TextField(
null=True, blank=True,
verbose_name=_("Valid values"),
help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
)
class Meta:
ordering = ("name",)

View File

@@ -31,7 +31,7 @@ from i18nfield.fields import I18nCharField
from pretix.base.models import Customer
from pretix.base.models.base import LoggedModel
from pretix.base.models.organizer import Organizer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.helpers.names import build_name
class MembershipType(LoggedModel):
@@ -160,15 +160,7 @@ class Membership(models.Model):
@property
def attendee_name(self):
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
def is_valid(self, ev=None):
if ev:

View File

@@ -42,10 +42,10 @@ from collections import Counter
from datetime import datetime, time, timedelta
from decimal import Decimal
from typing import Any, Dict, List, Union
from zoneinfo import ZoneInfo
import dateutil
import pycountry
import pytz
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models, transaction
@@ -82,6 +82,7 @@ from pretix.base.signals import order_gracefully_delete
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import format_map
from ...helpers.names import build_name
from ._transactions import (
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
)
@@ -460,14 +461,20 @@ class Order(LockModel, LoggedModel):
return '{event}-{code}'.format(event=self.event.slug.upper(), code=self.code)
def save(self, **kwargs):
if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'last_modified'}.union(kwargs['update_fields'])
if not self.code:
self.assign_code()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'code'}.union(kwargs['update_fields'])
if not self.datetime:
self.datetime = now()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'datetime'}.union(kwargs['update_fields'])
if not self.expires:
self.set_expires()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'expires'}.union(kwargs['update_fields'])
is_new = not self.pk
update_fields = kwargs.get('update_fields', [])
@@ -495,7 +502,7 @@ class Order(LockModel, LoggedModel):
def set_expires(self, now_dt=None, subevents=None):
now_dt = now_dt or now()
tz = pytz.timezone(self.event.settings.timezone)
tz = ZoneInfo(self.event.settings.timezone)
mode = self.event.settings.get('payment_term_mode')
if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
@@ -830,7 +837,7 @@ class Order(LockModel, LoggedModel):
@property
def is_expired_by_time(self):
return (
self.status == Order.STATUS_PENDING and self.expires < now()
self.status == Order.STATUS_PENDING and not self.require_approval and self.expires < now()
and not self.event.settings.get('payment_term_expire_automatically')
)
@@ -869,7 +876,7 @@ class Order(LockModel, LoggedModel):
@property
def payment_term_last(self):
tz = pytz.timezone(self.event.settings.timezone)
tz = ZoneInfo(self.event.settings.timezone)
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if self.event.has_subevents:
@@ -889,6 +896,28 @@ class Order(LockModel, LoggedModel):
), tz)
return term_last
@property
def payment_term_expire_date(self):
delay = self.event.settings.get('payment_term_expire_delay_days', as_type=int)
if not delay: # performance saver + backwards compatibility
return self.expires
term_last = self.payment_term_last
if term_last and self.expires > term_last: # backwards compatibility
return self.expires
expires = self.expires.date() + timedelta(days=delay)
tz = ZoneInfo(self.event.settings.timezone)
expires = make_aware(datetime.combine(
expires,
time(hour=23, minute=59, second=59)
), tz)
if term_last:
return min(expires, term_last)
else:
return expires
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
error_messages = {
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
@@ -1229,7 +1258,7 @@ class QuestionAnswer(models.Model):
try:
d = dateutil.parser.parse(self.answer)
if self.orderposition:
tz = pytz.timezone(self.orderposition.order.event.settings.timezone)
tz = ZoneInfo(self.orderposition.order.event.settings.timezone)
d = d.astimezone(tz)
return date_format(d, "SHORT_DATETIME_FORMAT")
except ValueError:
@@ -1441,25 +1470,29 @@ class AbstractPosition(models.Model):
else self.variation.quotas.filter(subevent=self.subevent))
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
update_fields = kwargs.get('update_fields', set())
if 'attendee_name_parts' in update_fields:
update_fields.append('attendee_name_cached')
self.attendee_name_cached = self.attendee_name
kwargs['update_fields'] = {'attendee_name_cached'}.union(kwargs['update_fields'])
name = self.attendee_name
if name != self.attendee_name_cached:
self.attendee_name_cached = name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'attendee_name_cached'}.union(kwargs['update_fields'])
if self.attendee_name_parts is None:
self.attendee_name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'attendee_name_parts'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
@property
def attendee_name(self):
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.event.settings.name_scheme)
@property
def attendee_name_all_components(self):
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.event.settings.name_scheme)
@property
def state_name(self):
@@ -1830,6 +1863,8 @@ class OrderPayment(models.Model):
def save(self, *args, **kwargs):
if not self.local_id:
self.local_id = (self.order.payments.aggregate(m=Max('local_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
def create_external_refund(self, amount=None, execution_date=None, info='{}'):
@@ -2028,6 +2063,8 @@ class OrderRefund(models.Model):
def save(self, *args, **kwargs):
if not self.local_id:
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
@@ -2446,14 +2483,20 @@ class OrderPosition(AbstractPosition):
assign_ticket_secret(
event=self.order.event, position=self, force_invalidate=True, save=False
)
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'secret'}.union(kwargs['update_fields'])
if not self.blocked:
if not self.blocked and self.blocked is not None:
self.blocked = None
elif not isinstance(self.blocked, list) or any(not isinstance(b, str) for b in self.blocked):
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'blocked'}.union(kwargs['update_fields'])
elif self.blocked and (not isinstance(self.blocked, list) or any(not isinstance(b, str) for b in self.blocked)):
raise TypeError("blocked needs to be a list of strings")
if not self.pseudonymization_id:
self.assign_pseudonymization_id()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'pseudonymization_id'}.union(kwargs['update_fields'])
if not self.get_deferred_fields():
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
@@ -2834,8 +2877,12 @@ class CartPosition(AbstractPosition):
if self.is_bundled:
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
if bundle:
listed_price = bundle.designated_price
price_after_voucher = bundle.designated_price
if self.addon_to.voucher_id and self.addon_to.voucher.all_bundles_included:
listed_price = Decimal('0.00')
price_after_voucher = Decimal('0.00')
else:
listed_price = bundle.designated_price
price_after_voucher = bundle.designated_price
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
self.listed_price = listed_price
@@ -2935,10 +2982,17 @@ class InvoiceAddress(models.Model):
self.order.touch()
if self.name_parts:
self.name_cached = self.name
name = self.name
if self.name_cached != name:
self.name_cached = self.name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
else:
self.name_cached = ""
self.name_parts = {}
if self.name_cached != "" or self.name_parts != {}:
self.name_cached = ""
self.name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached', 'name_parts'}.union(kwargs['update_fields'])
super().save(**kwargs)
def describe(self):
@@ -2980,15 +3034,11 @@ class InvoiceAddress(models.Model):
@property
def name(self):
if not self.name_parts:
return ""
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
raise TypeError("Invalid name given.")
return scheme['concatenation'](self.name_parts).strip()
return build_name(self.name_parts, fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
@property
def name_all_components(self):
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
def for_js(self):
d = {}
@@ -3088,11 +3138,7 @@ class BlockedTicketSecret(models.Model):
updated = models.DateTimeField(auto_now=True)
class Meta:
if 'mysql' not in settings.DATABASES['default']['ENGINE']:
# MySQL does not support indexes on TextField(). Django knows this and just ignores db_index, but it will
# not silently ignore the UNIQUE index, causing this table to fail. I'm so glad we're deprecating MySQL
# in a few months, so we'll just live without an unique index until then.
unique_together = (('event', 'secret'),)
unique_together = (('event', 'secret'),)
@receiver(post_delete, sender=CachedTicket)

View File

@@ -35,12 +35,12 @@
import string
from datetime import date, datetime, time
import pytz
import pytz_deprecation_shim
from django.conf import settings
from django.core.mail import get_connection
from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.db.models import Q
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -102,6 +102,7 @@ class Organizer(LoggedModel):
is_new = not self.pk
obj = super().save(*args, **kwargs)
if is_new:
kwargs.pop('update_fields', None) # does not make sense here
self.set_defaults()
else:
self.get_cache().clear()
@@ -140,7 +141,7 @@ class Organizer(LoggedModel):
@property
def timezone(self):
return pytz.timezone(self.settings.timezone)
return pytz_deprecation_shim.timezone(self.settings.timezone)
@cached_property
def all_logentries_link(self):
@@ -156,17 +157,19 @@ class Organizer(LoggedModel):
return self.cache.get_or_set(
key='has_gift_cards',
timeout=15,
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.exists()
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.filter(active=True).exists()
)
@property
def accepted_gift_cards(self):
from .giftcards import GiftCard, GiftCardAcceptance
return GiftCard.objects.annotate(
accepted=Exists(GiftCardAcceptance.objects.filter(issuer=OuterRef('issuer'), collector=self))
).filter(
Q(issuer=self) | Q(accepted=True)
return GiftCard.objects.filter(
Q(issuer=self) |
Q(issuer__in=GiftCardAcceptance.objects.filter(
acceptor=self,
active=True,
).values_list('issuer', flat=True))
)
@property

View File

@@ -22,9 +22,12 @@
import json
from decimal import Decimal
import jsonschema
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.deconstruct import deconstructible
from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.fields import I18nCharField
@@ -135,6 +138,25 @@ def cc_to_vat_prefix(country_code):
return country_code
@deconstructible
class CustomRulesValidator:
def __call__(self, value):
if not isinstance(value, dict):
try:
val = json.loads(value)
except ValueError:
raise ValidationError(_('Your layout file is not a valid JSON file.'))
else:
val = value
with open(finders.find('schema/tax-rules-custom.schema.json'), 'r') as f:
schema = json.loads(f.read())
try:
jsonschema.validate(val, schema)
except jsonschema.ValidationError as e:
e = str(e).replace('%', '%%')
raise ValidationError(_('Your set of rules is not valid. Error message: {}').format(e))
class TaxRule(LoggedModel):
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
internal_name = models.CharField(

View File

@@ -296,6 +296,14 @@ class Voucher(LoggedModel):
verbose_name=_("Shows hidden products that match this voucher"),
default=True
)
all_addons_included = models.BooleanField(
verbose_name=_("Offer all add-on products for free when redeeming this voucher"),
default=False
)
all_bundles_included = models.BooleanField(
verbose_name=_("Include all bundled products without a designated price when redeeming this voucher"),
default=False
)
objects = ScopedManager(organizer='event__organizer')
@@ -494,7 +502,10 @@ class Voucher(LoggedModel):
return seat
def save(self, *args, **kwargs):
self.code = self.code.upper()
if self.code != self.code.upper():
self.code = self.code.upper()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'code'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
self.event.cache.set('vouchers_exist', True)

View File

@@ -35,9 +35,9 @@ from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import User, Voucher
from pretix.base.services.mail import SendMailException, mail, render_mail
from pretix.base.settings import PERSON_NAME_SCHEMES
from ...helpers.format import format_map
from ...helpers.names import build_name
from .base import LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation
@@ -119,32 +119,35 @@ class WaitingListEntry(LoggedModel):
def clean(self):
try:
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_duplicate(self.event, self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
except ObjectDoesNotExist:
raise ValidationError('Invalid input')
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
update_fields = kwargs.get('update_fields', set())
if 'name_parts' in update_fields:
update_fields.append('name_cached')
self.name_cached = self.name
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
name = self.name
if name != self.name_cached:
self.name_cached = name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
if self.name_parts is None:
self.name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_parts'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
@property
def name(self):
if not self.name_parts:
return None
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.name_parts).strip()
return build_name(self.name_parts, fallback_scheme=lambda: self.event.settings.name_scheme)
@property
def name_all_components(self):
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.event.settings.name_scheme)
def send_voucher(self, quota_cache=None, user=None, auth=None):
availability = (
@@ -215,7 +218,7 @@ class WaitingListEntry(LoggedModel):
'waitinglistentry': self.pk,
'subevent': self.subevent.pk if self.subevent else None,
}, user=user, auth=auth)
self.log_action('pretix.waitinglist.voucher', user=user, auth=auth)
self.log_action('pretix.event.orders.waitinglist.voucher_assigned', user=user, auth=auth)
self.voucher = v
self.save()
@@ -308,9 +311,9 @@ class WaitingListEntry(LoggedModel):
raise ValidationError(_('The subevent does not belong to this event.'))
@staticmethod
def clean_duplicate(email, item, variation, subevent, pk):
def clean_duplicate(event, email, item, variation, subevent, pk):
if WaitingListEntry.objects.filter(
item=item, variation=variation, email__iexact=email, voucher__isnull=True, subevent=subevent
).exclude(pk=pk).exists():
).exclude(pk=pk).count() >= event.settings.waiting_list_limit_per_user:
raise ValidationError(_('You are already on this waiting list! We will notify '
'you as soon as we have a ticket available for you.'))

View File

@@ -28,6 +28,7 @@ import pycountry
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator
from django.db.models import Q
from django.utils import formats
from django.utils.functional import cached_property
from django.utils.translation import (
@@ -42,8 +43,8 @@ from phonenumbers import SUPPORTED_REGIONS
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.questions import guess_country
from pretix.base.models import (
ItemVariation, OrderPosition, Question, QuestionAnswer, QuestionOption,
Seat, SubEvent,
Customer, ItemVariation, OrderPosition, Question, QuestionAnswer,
QuestionOption, Seat, SubEvent,
)
from pretix.base.services.pricing import get_price
from pretix.base.settings import (
@@ -210,7 +211,7 @@ class SubeventColumn(ImportColumn):
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = self.event.timezone.localize(d)
d = d.replace(tzinfo=self.event.timezone)
try:
se = self.event.subevents.get(
active=True,
@@ -660,7 +661,7 @@ class ValidFrom(ImportColumn):
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = self.event.timezone.localize(d)
d = d.replace(tzinfo=self.event.timezone)
return d
except (ValueError, TypeError):
pass
@@ -683,7 +684,7 @@ class ValidUntil(ImportColumn):
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = self.event.timezone.localize(d)
d = d.replace(tzinfo=self.event.timezone)
return d
except (ValueError, TypeError):
pass
@@ -826,6 +827,28 @@ class QuestionColumn(ImportColumn):
a.options.add(*a._options)
class CustomerColumn(ImportColumn):
identifier = 'customer'
verbose_name = gettext_lazy('Customer')
def clean(self, value, previous_values):
if value:
try:
value = self.event.organizer.customers.get(
Q(identifier=value) | Q(email__iexact=value) | Q(external_identifier=value)
)
except Customer.MultipleObjectsReturned:
value = self.event.organizer.customers.get(
Q(identifier=value)
)
except Customer.DoesNotExist:
raise ValidationError(_('No matching customer was found.'))
return value
def assign(self, value, order, position, invoice_address, **kwargs):
order.customer = value
def get_all_columns(event):
default = []
if event.has_subevents:
@@ -837,6 +860,10 @@ def get_all_columns(event):
Variation(event),
InvoiceAddressCompany(event),
]
if event.settings.customer_accounts:
default += [
CustomerColumn(event),
]
scheme = PERSON_NAME_SCHEMES.get(event.settings.name_scheme)
for n, l, w in scheme['fields']:
default.append(InvoiceAddressNamePart(event, n, l))

Some files were not shown because too many files have changed in this diff Show More