Compare commits

...

529 Commits

Author SHA1 Message Date
Raphael Michel
951e498089 Use Stripe's new connect onboardning 2020-10-14 18:42:34 +02:00
Raphael Michel
b4a7729cb5 Fix duplicate invoice cancellation when changing free order to paid 2020-10-14 18:08:12 +02:00
Raphael Michel
f2e5e89970 Order import: Allow to reference question options by value 2020-10-14 16:29:32 +02:00
Raphael Michel
4fd773caf6 Change calendar restriction from "100 events" to "50 future events" 2020-10-14 11:39:40 +02:00
Raphael Michel
6402f0d86e Add addresses to check-in lists 2020-10-14 11:34:41 +02:00
Raphael Michel
f5d93eaffa s/WhiteList/AllowList/g 2020-10-14 11:23:47 +02:00
Raphael Michel
3f40a8e6fa Fix import of attendee addresses 2020-10-14 11:09:41 +02:00
Raphael Michel
b947467589 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-10-13 18:13:07 +02:00
Raphael Michel
810f3d7d31 Minor improvements to security profiles 2020-10-13 18:02:56 +02:00
Raphael Michel
e8f3ad633a Add device security profiles (#1806) 2020-10-13 17:40:25 +02:00
Raphael Michel
301849f771 Merge pull request #1800 from pretix-translations/weblate-pretix-pretix 2020-10-13 16:30:40 +02:00
Tobias Sundgren
ee6a595e26 Translated on translate.pretix.eu (Swedish)
Currently translated at 11.0% (414 of 3762 strings)

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

powered by weblate
2020-10-12 12:22:17 +02:00
Tobias Sundgren
980296e38a Translated on translate.pretix.eu (Swedish)
Currently translated at 10.8% (405 of 3762 strings)

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

powered by weblate
2020-10-12 12:22:17 +02:00
Tobias Sundgren
0a62ee0e33 Translated on translate.pretix.eu (Swedish)
Currently translated at 10.6% (400 of 3762 strings)

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

powered by weblate
2020-10-12 12:22:17 +02:00
Raphael Michel
afc1013d69 Fix infinite price adjustment loop when combining free prices, country-dependent tax rates, and vouchers 2020-10-12 12:15:58 +02:00
Raphael Michel
16cf3cec76 Add tests for device API 2020-10-12 12:15:58 +02:00
Raphael Michel
0105b9642d Add compat function for date.fromisocalendar for Python 3.6-3.7 2020-10-12 12:15:58 +02:00
Raphael Michel
3ec15fa529 Fix widget test cases 2020-10-11 20:42:35 +02:00
Raphael Michel
703eebab47 Remove tests for deleted pretixdroid code 2020-10-11 20:32:25 +02:00
Raphael Michel
3aec3a52fc Merge pull request #1805 from Chessmasterrr/patch-1
Update plugin-development documentation
2020-10-11 14:48:11 +02:00
Chessmaster
fd93cac8cf Update plugins.rst
Fixed small typo and update references
2020-10-11 13:58:01 +02:00
Raphael Michel
e139924696 Add reason based classes to events in widget 2020-10-09 16:12:05 +02:00
Raphael Michel
da725c0bff Backend forms: Do not allow to click empty labels 2020-10-09 12:19:44 +02:00
Raphael Michel
dca61447cf Fix widget bug with large number of variations 2020-10-08 16:19:48 +02:00
Raphael Michel
f54bf3f1ea Specific error message for payment_ended 2020-10-08 09:28:53 +02:00
Raphael Michel
3cef9bac26 Restrict collapsed days to large numbers 2020-10-07 15:02:12 +02:00
Raphael Michel
4f20849e4b Rewrite subquery in gift card list 2020-10-07 10:53:59 +02:00
Raphael Michel
758981fc1b Use internal name of product in OrderFilterForm 2020-10-07 10:49:20 +02:00
Raphael Michel
9b671d6370 Week calendar: Collapse days on mobile (except the current day) 2020-10-07 10:41:30 +02:00
Raphael Michel
3bfaf55094 Check-in list API: Show items in event default language 2020-10-07 10:04:26 +02:00
Raphael Michel
3708dab656 Add allow_entry_after_exit field to subevent editing 2020-10-07 09:25:53 +02:00
Raphael Michel
14ad7716bd Fix missing copy button for add-ons if only system fields match 2020-10-06 18:45:53 +02:00
Raphael Michel
985d3c3993 Add date column to web-based check-in list 2020-10-06 18:45:53 +02:00
Raphael Michel
fa2222e629 Remove pretixdroid code 2020-10-06 18:45:53 +02:00
Raphael Michel
13eabdd7f4 Fix missing date column in list of refunds 2020-10-06 18:45:53 +02:00
Raphael Michel
4fd748e6d9 Merge pull request #1796 from pretix-translations/weblate-pretix-pretix 2020-10-06 16:00:19 +02:00
Tobias Sundgren
f48ded0165 Translated on translate.pretix.eu (Swedish)
Currently translated at 8.1% (305 of 3762 strings)

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

powered by weblate
2020-10-06 15:02:14 +02:00
Tobias Sundgren
903ea09140 Translated on translate.pretix.eu (Swedish)
Currently translated at 8.1% (304 of 3762 strings)

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

powered by weblate
2020-10-06 15:02:14 +02:00
Maarten van den Berg
fadc610b8e Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3762 of 3762 strings)

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

powered by weblate
2020-10-06 15:02:14 +02:00
Maarten van den Berg
ac4b8a392b Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3762 of 3762 strings)

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

powered by weblate
2020-10-06 15:02:14 +02:00
ethan.wang
22d986a709 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 82.4% (3100 of 3762 strings)

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

powered by weblate
2020-10-06 15:02:14 +02:00
Martin Gross
bca34145f1 Translated on translate.pretix.eu (Danish)
Currently translated at 42.6% (1603 of 3762 strings)

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

powered by weblate
2020-10-06 15:02:14 +02:00
Mie Frydensbjerg
97af6f7311 Translated on translate.pretix.eu (Danish)
Currently translated at 42.6% (1603 of 3762 strings)

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

powered by weblate
2020-10-06 15:02:14 +02:00
Martin Gross
67156a67aa Translated on translate.pretix.eu (Danish)
Currently translated at 42.6% (1602 of 3762 strings)

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

powered by weblate
2020-10-06 15:02:14 +02:00
Raphael Michel
4ed872d4ef Calendar stuff is hard 2020-10-06 15:02:05 +02:00
Martin Gross
5cd6cba0a2 Fix accidential removal of gitcard-filter 2020-10-06 14:46:53 +02:00
Martin Gross
72bb5bd177 Only include Confirmed and Refunded Payments/completed Refunds in Giftcard-report 2020-10-06 14:30:39 +02:00
Raphael Michel
d392e14a96 Fix date() usage 2020-10-06 12:50:37 +02:00
Raphael Michel
d7459b3b83 Fix years with 52 weeks 2020-10-06 12:49:02 +02:00
Martin Gross
b4778b5845 Allow to unselect product or quota from voucher (#1799) 2020-10-06 09:08:15 +02:00
Raphael Michel
5a09759cb9 Fix *yet* another time range issue 2020-10-05 17:39:41 +02:00
Raphael Michel
2fbaa90d76 Clarify docstring 2020-10-05 17:33:19 +02:00
Raphael Michel
93f10d33a9 Fix breakpoints of calendar week selection 2020-10-05 17:32:12 +02:00
Raphael Michel
e9a972ad60 Show dates in calendar week selection 2020-10-05 17:29:19 +02:00
Raphael Michel
a31f0c1bc8 Fix one more bug in calendar view 2020-10-05 17:29:11 +02:00
Raphael Michel
1b0c2f3bb7 Fix bug in previous commit 2020-10-05 17:17:28 +02:00
Raphael Michel
766428c469 Improve support for cross-midnight time slots 2020-10-05 17:14:49 +02:00
Raphael Michel
d85583f70a Fix missing field description 2020-10-05 17:13:39 +02:00
Raphael Michel
ee801bd717 Safety guard against unpaid giftcards 2020-10-05 16:09:24 +02:00
Martin Gross
af0e8ec992 Fix test, Ref: 3cbcf663e5 2020-10-05 13:45:53 +02:00
Raphael Michel
bc3325c1cb Order overview PDF: Print net and gross 2020-10-02 16:45:47 +02:00
Martin Gross
753c331887 Shutting up isort 2020-10-02 14:04:22 +02:00
Martin Gross
cfc9055ec1 Fix giftcard API doc 2020-10-02 13:58:19 +02:00
Raphael Michel
c131a2ac3a Import: Do not allow duplicate secrets even across events 2020-10-02 10:29:27 +02:00
Raphael Michel
17fe3355d1 pretixPOS: ZVT support 2020-09-29 19:10:48 +02:00
Martin Gross
0381d42d41 Fix flag for Swedish language (fixes #1795) 2020-09-29 11:11:25 +02:00
Raphael Michel
b73db911e9 Add profile OAuth scope 2020-09-28 16:11:43 +02:00
Raphael Michel
1f3d4a2810 Fix name finding in placeholders 2020-09-28 16:11:30 +02:00
Raphael Michel
3cbcf663e5 OAuth: Add profile-only access 2020-09-28 16:01:59 +02:00
Raphael Michel
ae0637a3d6 Merge pull request #1789 from pretix-translations/weblate-pretix-pretix 2020-09-28 12:15:22 +02:00
Raphael Michel
a6a9c08a0a Translated on translate.pretix.eu (German)
Currently translated at 99.9% (3761 of 3762 strings)

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

powered by weblate
2020-09-28 12:15:08 +02:00
Raphael Michel
f3b3d0b8f7 Translated on translate.pretix.eu (German (informal))
Currently translated at 99.9% (3761 of 3762 strings)

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

powered by weblate
2020-09-28 12:14:57 +02:00
Raphael Michel
9490f20a6c Revert "Do not allow to cancel gift card positions"
This reverts commit 951e99d0da.
2020-09-28 12:13:04 +02:00
Raphael Michel
4555a917b2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-09-28 12:10:15 +02:00
Raphael Michel
951e99d0da Do not allow to cancel gift card positions 2020-09-28 11:51:06 +02:00
Raphael Michel
d0b002cf0c Issue gift cards after order change 2020-09-28 11:48:55 +02:00
Felix Rindt
4fb0b948ec Add name scheme with salutation (#1779) 2020-09-28 11:41:59 +02:00
Felix Rindt
2384478b45 Support required-if and display-dependency for more elements (#1788) 2020-09-28 10:42:03 +02:00
Raphael Michel
f3a2d0cb03 Merge pull request #1787 from pretix-translations/weblate-pretix-pretix 2020-09-28 09:24:07 +02:00
Maarten van den Berg
1b11d88442 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3757 of 3757 strings)

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

powered by weblate
2020-09-28 07:00:13 +02:00
Maarten van den Berg
954951ddfa Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3757 of 3757 strings)

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

powered by weblate
2020-09-28 07:00:12 +02:00
Raphael Michel
c01b96bdfc Allow to sort list of quotas 2020-09-25 19:08:42 +02:00
Raphael Michel
c78e88a1ba Allow to create devices through the API (#1785) 2020-09-25 18:16:18 +02:00
Raphael Michel
4cb18218b2 Do not show event time on invoice 2020-09-25 18:01:50 +02:00
Raphael Michel
450d017c32 Order API: Add `send_email` parameter to mark_paid and
payments/confirm
2020-09-25 15:12:06 +02:00
Raphael Michel
655977e33d Fix typo 2020-09-24 12:55:51 +02:00
Raphael Michel
0cb0620df0 Fix crash from previous deployment 2020-09-24 12:36:00 +02:00
Raphael Michel
c8bf069650 Merge pull request #1784 from pretix-translations/weblate-pretix-pretix 2020-09-24 12:16:28 +02:00
Raphael Michel
e65087fd68 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3757 of 3757 strings)

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

powered by weblate
2020-09-24 12:16:16 +02:00
Raphael Michel
d67d389b9d Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3757 of 3757 strings)

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

powered by weblate
2020-09-24 12:16:16 +02:00
Raphael Michel
0e805e50f9 Widget: consistent rendering of date ranges 2020-09-24 12:15:54 +02:00
Raphael Michel
a4d133731e Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-09-24 10:59:11 +02:00
Raphael Michel
c74e7fd4fb Show time slot end time in calendars 2020-09-24 10:58:48 +02:00
Raphael Michel
0e405d2327 Widget: Align wording with standalone calendar 2020-09-24 10:58:48 +02:00
Raphael Michel
035c707427 Calendar: Show fully booked instead of sold out for free events 2020-09-24 10:58:48 +02:00
Raphael Michel
787e7ec993 Merge pull request #1783 from pretix-translations/weblate-pretix-pretix 2020-09-24 10:58:38 +02:00
Svyatoslav
09a9b4a456 Translated on translate.pretix.eu (Russian)
Currently translated at 78.1% (100 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ru/

powered by weblate
2020-09-23 19:00:12 +02:00
Svyatoslav
e2547c2761 Translated on translate.pretix.eu (Russian)
Currently translated at 29.7% (1116 of 3756 strings)

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

powered by weblate
2020-09-23 19:00:12 +02:00
Martin Gross
c7b2baf40f Correct file type for shredder export zip (Fixes #1781, Z#153584) 2020-09-23 13:41:47 +02:00
Felix Rindt
59595c9db8 Add setting to disable copy-answers-button (#1778) 2020-09-22 18:09:44 +02:00
Nils Schneider
2f8baecd68 Fix #1127 -- Set loglevel from configfile (#1777) 2020-09-22 17:40:56 +02:00
Raphael Michel
a76f74b161 Download reminders: Fix incomplete only() call 2020-09-21 18:26:38 +02:00
Raphael Michel
f2518101ef Fix broken test after deprecation of pretixdroid 2020-09-21 18:23:25 +02:00
Raphael Michel
ec667545e8 Fix crash/bug in order data export 2020-09-21 18:23:25 +02:00
Raphael Michel
afb789226c Webhooks: Fix crash if object was deleted in meantime 2020-09-21 18:23:25 +02:00
Felix Rindt
bca7a6db93 Mail service: Allow to attach arbitrary cached files (#1774) 2020-09-21 17:45:29 +02:00
Felix Rindt
429ad4da37 Refactor primary color to settings variable (#1775) 2020-09-21 17:44:43 +02:00
Raphael Michel
cd6e6004af Merge pull request #1773 from pretix-translations/weblate-pretix-pretix 2020-09-21 17:43:59 +02:00
Tobias Sundgren
e9d5665a3d Translated on translate.pretix.eu (Swedish)
Currently translated at 7.0% (262 of 3756 strings)

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

powered by weblate
2020-09-19 20:00:13 +02:00
Tobias Sundgren
4cbc30a7ea Translated on translate.pretix.eu (Swedish)
Currently translated at 6.9% (261 of 3756 strings)

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

powered by weblate
2020-09-18 18:20:26 +02:00
Tobias Sundgren
2b0388c2ee Translated on translate.pretix.eu (Swedish)
Currently translated at 5.8% (218 of 3756 strings)

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

powered by weblate
2020-09-18 18:20:26 +02:00
Tobias Sundgren
06b8826e57 Translated on translate.pretix.eu (Swedish)
Currently translated at 100.0% (128 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/sv/

powered by weblate
2020-09-18 18:20:26 +02:00
Tobias Sundgren
7c6f0f45a3 Translated on translate.pretix.eu (Swedish)
Currently translated at 5.7% (213 of 3756 strings)

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

powered by weblate
2020-09-18 18:20:26 +02:00
Tobias Sundgren
93399f51b3 Translated on translate.pretix.eu (Swedish)
Currently translated at 100.0% (128 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/sv/

powered by weblate
2020-09-18 18:20:26 +02:00
Pernilla Näsfors Östmar
87bd54b233 Translated on translate.pretix.eu (Swedish)
Currently translated at 100.0% (128 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/sv/

powered by weblate
2020-09-18 18:20:26 +02:00
Tobias Sundgren
3fb237f434 Translated on translate.pretix.eu (Swedish)
Currently translated at 5.6% (212 of 3756 strings)

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

powered by weblate
2020-09-18 18:20:26 +02:00
Maarten van den Berg
d7640d25f5 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3756 of 3756 strings)

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

powered by weblate
2020-09-18 18:20:26 +02:00
Maarten van den Berg
1669d3f5c7 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3756 of 3756 strings)

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

powered by weblate
2020-09-18 18:20:26 +02:00
Maarten van den Berg
5aa3f3e772 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.6% (3741 of 3756 strings)

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

powered by weblate
2020-09-18 18:20:26 +02:00
Martin Gross
b7a2f0257f Translated on translate.pretix.eu (French)
Currently translated at 80.5% (103 of 128 strings)

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

powered by weblate
2020-09-18 18:20:26 +02:00
Martin Gross
5c0f29f959 Translated on translate.pretix.eu (French)
Currently translated at 61.2% (2297 of 3756 strings)

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

powered by weblate
2020-09-18 18:20:26 +02:00
Raphael Michel
59655dca82 Fix redemption of multiple gift cards 2020-09-18 18:20:08 +02:00
Raphael Michel
af2b4ebb4b Properly deprecate pretixdroid 2020-09-18 16:28:19 +02:00
Martin Gross
d5b3528f92 Localize Stripe Elements to Order Page Language 2020-09-18 16:26:10 +02:00
Martin Gross
0a1b41235b Add Seat to Order Data Export - but better 2020-09-18 16:25:49 +02:00
Felix Rindt
8ca544064b Fix #1759: order import increasing positionids (#1776) 2020-09-18 16:16:33 +02:00
Raphael Michel
1e2b305376 Quota and check-in list list: Include time of subevent 2020-09-18 13:12:04 +02:00
Raphael Michel
bfa20e995a Order API: Include ID of check-in 2020-09-18 13:12:04 +02:00
Martin Gross
e7fd0f116b Add Seat to Order Data Export 2020-09-17 09:53:26 +02:00
Raphael Michel
e836da09cd Fix bug in a combination of timeouts and custom auth backends 2020-09-16 18:09:53 +02:00
Raphael Michel
22c6553a48 Fix API for organizers and events with a . in their slug 2020-09-16 16:37:55 +02:00
pretix translation bot
ea5fc3df40 Translations update from Weblate (#1772)
Co-authored-by: Martin Gross <martin@pc-coholic.de>
2020-09-15 10:38:28 +02:00
pretix translation bot
7977b6dc15 Translations update from Weblate (#1771)
Co-authored-by: Mie Frydensbjerg <mif@aarhus.dk>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2020-09-15 09:55:20 +02:00
Raphael Michel
59df5fe052 Fix manifest config 2020-09-14 19:00:29 +02:00
Raphael Michel
c4e00e7601 Bump to 3.12.0.dev0 2020-09-14 18:23:42 +02:00
Raphael Michel
b3fd515652 Bump to 3.11.0 2020-09-14 18:22:03 +02:00
Raphael Michel
6ee54f6cc1 Add sentence to structure guide 2020-09-14 18:21:09 +02:00
Raphael Michel
403dc9cf98 Sort exported check-ins 2020-09-14 16:28:56 +02:00
Raphael Michel
eb462b1950 Fix default item 2020-09-14 16:13:13 +02:00
Raphael Michel
cb6d50e570 Merge pull request #1770 from pretix-translations/weblate-pretix-pretix 2020-09-14 14:04:01 +02:00
Raphael Michel
32a2a99b7b Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3756 of 3756 strings)

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

powered by weblate
2020-09-14 14:00:24 +02:00
Raphael Michel
7859985ff0 Translated on translate.pretix.eu (German)
Currently translated at 99.9% (3755 of 3756 strings)

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

powered by weblate
2020-09-14 14:00:23 +02:00
Raphael Michel
7b7cf76028 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-09-14 14:00:14 +02:00
Raphael Michel
4fda9b2205 Fix typo 2020-09-14 13:59:36 +02:00
Raphael Michel
ede93f1669 Extend word list 2020-09-14 13:47:39 +02:00
Raphael Michel
4af4dbd9cf Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-09-14 13:46:26 +02:00
Raphael Michel
820f6d52a5 Fix incorrect help text 2020-09-14 13:45:58 +02:00
Raphael Michel
8d04974caa Merge pull request #1769 from pretix-translations/weblate-pretix-pretix 2020-09-14 13:44:58 +02:00
Raphael Michel
8021c9e865 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3750 of 3750 strings)

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

powered by weblate
2020-09-14 13:44:33 +02:00
Raphael Michel
874ea6750b Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3750 of 3750 strings)

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

powered by weblate
2020-09-14 13:44:33 +02:00
Felix Rindt
8f2c125435 Payment term in minutes (#1760)
Co-authored-by: Raphael Michel <michel@rami.io>
2020-09-14 13:44:28 +02:00
Raphael Michel
2f21dc8c3c Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-09-14 13:22:10 +02:00
Raphael Michel
05135c779c Add export of all check-in scans 2020-09-14 13:21:24 +02:00
Raphael Michel
f08f06ddff Fix infinite recursion 2020-09-11 18:30:35 +02:00
Raphael Michel
9c28537d1c CSV exports: Fix running when file is opened in binary mode 2020-09-11 09:17:11 +02:00
Raphael Michel
95b1a4a434 Fix German language error 2020-09-10 20:23:14 +02:00
Raphael Michel
019801e9dc Cronjob: Fix issue in waiting list handling 2020-09-09 22:15:41 +02:00
Martin Gross
3e97fd87d1 Fix typo in process_waitinglist 2020-09-09 22:10:17 +02:00
Raphael Michel
8bcc0e4641 Cronjob: Try sync execute for quotas 2020-09-09 21:53:52 +02:00
Raphael Michel
878becfee9 Cronjob: Allow keyboard interrupt 2020-09-09 21:52:03 +02:00
Raphael Michel
273c34999c Cronjob: Ignore old events for waiting list 2020-09-09 21:50:55 +02:00
Raphael Michel
3f4f9d98de Increase length of multiple-choice country fields 2020-09-09 20:46:42 +02:00
Raphael Michel
d703eeb770 Show device serial in backend 2020-09-09 16:30:14 +02:00
Felix Rindt
b7754d8737 Command line exporter: support multiple and all events (#1766) 2020-09-09 16:19:12 +02:00
Raphael Michel
ed62ecaccb Adjust "mobile" size of widget 2020-09-09 09:22:06 +02:00
Raphael Michel
5c3ef3f2b9 Widget: Fix CSS issue during load 2020-09-08 18:46:47 +02:00
Raphael Michel
f3282807e2 Add documentation for check-in type 2020-09-08 18:26:50 +02:00
Raphael Michel
52944ff3a3 Fix obscure crash in log entry view 2020-09-07 11:27:23 +02:00
Raphael Michel
23a9018988 Remove break-all from email CSS 2020-09-07 11:10:11 +02:00
Raphael Michel
936c771d5e Fix FF Android detection 2020-09-07 10:31:23 +02:00
Raphael Michel
d064a7affa Add verbose output to runperiodic management command 2020-09-06 22:34:40 +02:00
Raphael Michel
4b894eb433 Merge pull request #1764 from pretix/seat-orgchoice
Support seat choice by organizer
2020-09-06 19:06:33 +02:00
Raphael Michel
6c7ef89779 Add docs for pretix Webinar API 2020-09-06 17:26:20 +02:00
Raphael Michel
e8f3a66a8e Add signal pretix.control.signals.event_dashboard_top 2020-09-06 17:25:47 +02:00
Raphael Michel
d999971249 Allow to disable self-choice seating 2020-09-06 17:25:47 +02:00
Raphael Michel
fb701f25f4 Merge pull request #1761 from pretix-translations/weblate-pretix-pretix 2020-09-04 15:40:20 +02:00
Svyatoslav
913596459a Translated on translate.pretix.eu (Russian)
Currently translated at 29.8% (1114 of 3733 strings)

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

powered by weblate
2020-09-04 05:01:43 +02:00
Svyatoslav
04e9ea1ae7 Translated on translate.pretix.eu (Latvian)
Currently translated at 29.5% (1100 of 3733 strings)

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

powered by weblate
2020-09-04 05:01:43 +02:00
Maarten van den Berg
5dc09019ff Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3733 of 3733 strings)

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

powered by weblate
2020-09-04 05:01:43 +02:00
Maarten van den Berg
7c3671b383 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3733 of 3733 strings)

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

powered by weblate
2020-09-04 05:01:43 +02:00
Raphael Michel
b91c16538e Improve mobile shopping cart experience 2020-09-04 05:01:22 +02:00
Raphael Michel
9ba517109d Allow to inspect logs by device 2020-09-03 14:40:19 +02:00
Raphael Michel
5f86fbc21d Allow to filter event log by device 2020-09-03 14:30:21 +02:00
Raphael Michel
fae35cc56f Improve error handling of check-in scans 2020-09-03 14:30:18 +02:00
Raphael Michel
78c5eb4516 Merge branch 'master' of github.com:pretix/pretix 2020-09-02 18:58:22 +02:00
Raphael Michel
860f4c36a4 Name length validation 2020-09-02 18:13:42 +02:00
Martin Gross
311dcfaab0 Add notice that list-view is not available for eventseries with more than 100 dates. 2020-09-02 15:13:59 +02:00
Martin Gross
83a6041a32 Expose Order Time in OrderData-Export 2020-09-02 11:07:50 +02:00
Raphael Michel
a2f9bb73ad Fix failing tests 2020-09-01 22:13:19 +02:00
Raphael Michel
fb92c9dd64 Remove obsolete restriction from documentation 2020-09-01 21:52:03 +02:00
Raphael Michel
aa1910fd70 Add pseudonymization ID to search fields 2020-09-01 16:51:07 +02:00
Raphael Michel
f66c266ff7 Fix debugging code 2020-09-01 15:53:56 +02:00
Raphael Michel
7cc5179e85 Fix #1040 -- Work around firefox bug in widget 2020-09-01 15:46:31 +02:00
Raphael Michel
f633cc3103 Fix errors around subevent editing 2020-09-01 15:06:03 +02:00
Raphael Michel
5e212c83e4 Add Kosovo to country list 2020-08-31 13:23:13 +02:00
Raphael Michel
2d5768aa20 Fix missing fields in CheckinListOrderPositionSerializer 2020-08-29 12:38:23 +02:00
Martin Gross
8ea66bc05b First try at working around Stripe's iDEAL idempotency issues 2020-08-28 23:21:35 +02:00
Raphael Michel
eba17e22fb Show beneficiary on order confirmation page 2020-08-27 14:13:31 +02:00
Raphael Michel
620c956ef8 Delete vouchers when deleting events 2020-08-27 12:41:12 +02:00
Raphael Michel
35debba865 Further attempt at more efificent query 2020-08-26 16:42:34 +02:00
Raphael Michel
a635ea527e Fix failing tests 2020-08-26 16:33:31 +02:00
Raphael Michel
6e76db40ed Order API: More efficient query for ?subevent_after_qs= 2020-08-26 15:43:22 +02:00
Raphael Michel
7956074d8b API: Add exclude parameter to check-in lists 2020-08-26 15:20:44 +02:00
Raphael Michel
7a3418e32f SubEvent: Automatically bump all orders when date is changed 2020-08-26 11:00:43 +02:00
Raphael Michel
0bfc436970 Check-in list: Redirect back toe diting after save 2020-08-25 16:19:11 +02:00
Raphael Michel
b6fc02255d Allow to clone check-in lists 2020-08-25 15:52:46 +02:00
Raphael Michel
a06f94fde1 Clarify "disabled" checkbox 2020-08-25 14:08:27 +02:00
Raphael Michel
d5073f416c Merge pull request #1757 from pretix-translations/weblate-pretix-pretix 2020-08-25 12:20:32 +02:00
Dennis Lichtenthäler
b32ea0dec4 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3733 of 3733 strings)

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

powered by weblate
2020-08-25 12:15:52 +02:00
Dennis Lichtenthäler
932851cf96 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3733 of 3733 strings)

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

powered by weblate
2020-08-25 12:15:51 +02:00
Dennis Lichtenthäler
4513cd7ec3 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (128 of 128 strings)

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

powered by weblate
2020-08-25 11:54:59 +02:00
Dennis Lichtenthäler
261878b3fe Translated on translate.pretix.eu (German)
Currently translated at 100.0% (128 of 128 strings)

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

powered by weblate
2020-08-25 11:54:59 +02:00
Dennis Lichtenthäler
31590f7e6c Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3733 of 3733 strings)

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

powered by weblate
2020-08-25 11:54:59 +02:00
Dennis Lichtenthäler
ebe7560f14 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3733 of 3733 strings)

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

powered by weblate
2020-08-25 11:54:58 +02:00
Raphael Michel
c5b722ebc1 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-08-25 11:54:50 +02:00
Raphael Michel
5ea961819d Remove field Seat.name 2020-08-25 11:54:19 +02:00
Martin Gross
983d734c6a Add .swi to allowed MT940 file extensions 2020-08-25 11:30:51 +02:00
Raphael Michel
c1bca2f207 isort fix 2020-08-24 17:37:38 +02:00
Raphael Michel
118f0f55e9 Widget: Do not disable products with require_voucher if a voucher is
given
2020-08-24 17:31:57 +02:00
Raphael Michel
d1146add38 Allow to re-check-in someone through the backend 2020-08-24 17:27:06 +02:00
Raphael Michel
fc18788cb8 Order API: Add `subevent_after` query filter 2020-08-21 19:06:05 +02:00
Raphael Michel
a2eb4444b4 Order API: Add `exclude` query parameter 2020-08-21 18:38:24 +02:00
Raphael Michel
606d13e303 Check-in list API: Add `subevent_match` filter 2020-08-21 17:20:37 +02:00
Raphael Michel
d90fcee5e1 Fix crash related to vouchers and seats
PRETIXEU-2PY
2020-08-21 16:12:04 +02:00
Raphael Michel
e9a4c3845a Fix crash when processing refund for empty order 2020-08-21 16:07:52 +02:00
Raphael Michel
018fac2361 Merge pull request #1756 from pretix/felix-patch 2020-08-21 15:19:45 +02:00
Raphael Michel
41dd71879e Allow to filter items with query parameters on event page 2020-08-21 15:18:37 +02:00
Felix Rindt
738e5d07aa reorder signal docs 2020-08-20 20:42:31 +02:00
Felix Rindt
a22451140b fix typo 2020-08-20 20:42:23 +02:00
Raphael Michel
6759506838 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-08-20 13:52:24 +02:00
Raphael Michel
82bb3f3b6e RelativeDate: Allow to specify "minutes before x" 2020-08-20 13:51:55 +02:00
Raphael Michel
cdb8a92a47 Merge pull request #1754 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-08-20 13:49:28 +02:00
Svyatoslav
7597344897 Translated on translate.pretix.eu (Russian)
Currently translated at 29.8% (1110 of 3731 strings)

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

powered by weblate
2020-08-19 11:30:39 +02:00
Raphael Michel
c94d384e86 Improve algorithm for {name} placeholder (#1745)
Co-authored-by: Felix Rindt <felix@rindt.me>
2020-08-19 11:30:34 +02:00
Raphael Michel
b2357b7e29 Merge pull request #1751 from pretix-translations/weblate-pretix-pretix 2020-08-19 11:30:02 +02:00
Raphael Michel
c7d1e5d069 Allow to reduce the interval of some cronjobs (#1753) 2020-08-19 11:29:53 +02:00
Svyatoslav
754d498938 Translated on translate.pretix.eu (Russian)
Currently translated at 29.5% (1102 of 3731 strings)

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

powered by weblate
2020-08-18 18:21:44 +02:00
Maarten van den Berg
ec7fc05108 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3731 of 3731 strings)

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

powered by weblate
2020-08-18 17:34:51 +02:00
Maarten van den Berg
bbba0df6c4 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3731 of 3731 strings)

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

powered by weblate
2020-08-18 17:34:51 +02:00
Maarten van den Berg
65e87455ec Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 98.5% (3674 of 3731 strings)

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

powered by weblate
2020-08-18 17:34:51 +02:00
Maarten van den Berg
e6d09baacc Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3731 of 3731 strings)

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

powered by weblate
2020-08-18 17:34:51 +02:00
Raphael Michel
fbd38fef58 Fix issue in previous commit 2020-08-18 17:34:39 +02:00
Raphael Michel
253f944951 Quota cache refresh: work in chunks 2020-08-18 16:46:37 +02:00
Raphael Michel
7f30f753d7 Actually do not show date on invoices if not shown on frontpage 2020-08-18 13:57:35 +02:00
Raphael Michel
8789a42dc1 Fix tax calculation for negative fees 2020-08-17 15:56:03 +02:00
Raphael Michel
e7740b1735 Fix crash on addons without tax rule
PRETIXEU-2MZ
2020-08-17 09:39:59 +02:00
Raphael Michel
586da71a64 Remove Raphael's personal mail address from the README 2020-08-16 19:21:24 +02:00
Martin Gross
68697f0c6a Update po files
[CI skip]

Signed-off-by: Martin Gross <gross@rami.io>
2020-08-13 14:29:53 +02:00
pretix translation bot
a2e1bc9c20 Translations update from Weblate (#1744)
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
Co-authored-by: Martin Gross <martin@pc-coholic.de>
2020-08-13 14:26:14 +02:00
Martin Gross
89a82d75a9 Add eMail-Template for free approval orders (#1750)
Co-authored-by: Felix Rindt <felix@rindt.me>
2020-08-13 14:24:39 +02:00
Martin Gross
a2414081af Widget: Do not preset item quantity to 1 if there is only one item but an active seating plan 2020-08-13 11:21:24 +02:00
Martin Gross
c812d39b39 Filter BankImportJobs explicitly by organizer 2020-08-13 11:10:27 +02:00
Martin Gross
c6f3fdd8e4 Move reserved explanation out of tooltip 2020-08-12 16:37:59 +02:00
Martin Gross
30e0f5ebc7 Show seat in checkout_question-fragment when adequate 2020-08-11 17:54:52 +02:00
Martin Gross
f767f2f644 Fix encoding of Umlauts in widget (and hopefully don't break it...) 2020-08-10 17:23:05 +02:00
Martin Gross
750c3c5201 Allow for gt and gte selection of change_allow_user_price (#1746) 2020-08-07 11:54:27 +02:00
Raphael Michel
7d9220ae3e Fix issue in 69879bdae 2020-08-06 10:21:57 +02:00
Raphael Michel
69879bdae0 Fix API bug: Do not delete SubEventItems on PATCH request 2020-08-06 09:28:35 +02:00
Raphael Michel
0e245b41ee Fix duplicate call of form_success 2020-08-05 15:18:40 +02:00
Raphael Michel
2839ee1ffd Fix error in b6f47f6f4 2020-08-05 14:41:12 +02:00
Raphael Michel
d72a03c434 Allow to adjust ticket cache duration 2020-08-05 13:23:20 +02:00
Raphael Michel
b6f47f6f4a API: More validation in custom fields on event serializers 2020-08-05 11:26:11 +02:00
Raphael Michel
ca2dd0d6b6 Limit maximum length of event names in email senders 2020-08-05 11:23:27 +02:00
Raphael Michel
c4415beb8c Force Django version to be at least 3.0.9 2020-08-05 11:22:35 +02:00
Raphael Michel
35c8684cd4 Prevent issues with order fees and TaxRule.zero() 2020-08-04 14:07:26 +02:00
Raphael Michel
9bb5c57792 Fix possible crash in migration 2020-08-04 11:47:55 +02:00
Felix Rindt
1c8699662d Allow to create invoices before bank transfer runs (#1734)
Co-authored-by: Raphael Michel <michel@rami.io>
2020-08-04 10:53:59 +02:00
Felix Rindt
9b367cb28b Allow to set multiple confirm texts (#1735) 2020-08-04 10:20:55 +02:00
Felix Rindt
896ba5b06b Fix #1740 - Do not group gift card positions (#1743)
Co-authored-by: Raphael Michel <michel@rami.io>
2020-08-04 10:18:04 +02:00
Raphael Michel
8f3dbba859 PDF renderer: Fail silently if bidirectional string handling failes 2020-08-03 18:20:03 +02:00
Felix Rindt
bf5b92c465 Copy answers button for addon products (#1733) 2020-08-03 18:15:23 +02:00
Raphael Michel
aef09003d9 Make global_html_* signals actually global 2020-08-03 12:32:29 +02:00
Raphael Michel
9d22e833a6 Merge pull request #1737 from pretix-translations/weblate-pretix-pretix 2020-07-31 09:25:00 +02:00
Abdullah
1e121c0f75 Translated on translate.pretix.eu (Arabic)
Currently translated at 78.1% (100 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ar/

powered by weblate
2020-07-31 09:24:49 +02:00
Abdullah
373755a502 Translated on translate.pretix.eu (Arabic)
Currently translated at 87.6% (3260 of 3720 strings)

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

powered by weblate
2020-07-31 09:24:49 +02:00
pajowu
6f694b60ca Add minimum postgres version to docs (#1738) 2020-07-31 09:24:43 +02:00
Felix Rindt
77f76195c8 isort 5.0 config/docs (#1736) 2020-07-30 17:57:26 +02:00
Raphael Michel
355dd4463b Fix typo in docs 2020-07-30 17:57:10 +02:00
Raphael Michel
c0c39223aa Addendum to c15344ced 2020-07-30 17:47:05 +02:00
Raphael Michel
db7f8d9658 Try to run apt-get update before installations 2020-07-30 16:33:14 +02:00
Raphael Michel
c15344ced2 Docker: Pass environment variables when calling supervisord 2020-07-30 16:22:37 +02:00
Raphael Michel
0f3f15a736 Upgrade requests version 2020-07-29 18:30:25 +02:00
Raphael Michel
478f6e3029 Add a !default command to our _variables.scss 2020-07-29 18:30:25 +02:00
Raphael Michel
4c77e2f16e Add signals global_html_head, global_html_page_header, and global_html_footer 2020-07-29 18:30:25 +02:00
Felix Rindt
80b6a3d27d Fix #1675 -- Allow '0' as answer to number questions (#1732)
Co-authored-by: Raphael Michel <michel@rami.io>
2020-07-28 16:32:06 +02:00
Raphael Michel
89e8d3d12f Allow to disable some e-mails depending on sales channel (#1726)
Co-authored-by: Raphael Michel <michel@rami.io>
2020-07-28 09:26:18 +02:00
Raphael Michel
baf8a4ae18 Update src/pretix/control/forms/event.py 2020-07-28 09:25:10 +02:00
Raphael Michel
2cdaf07c46 Update src/pretix/control/forms/event.py 2020-07-28 09:24:53 +02:00
Raphael Michel
cf76a2e24d Fix typo in docs 2020-07-28 09:23:44 +02:00
Raphael Michel
559b4a8e66 Merge pull request #1730 from pretix-translations/weblate-pretix-pretix 2020-07-27 18:04:19 +02:00
Abdullah
59bf11b98d Translated on translate.pretix.eu (Arabic)
Currently translated at 87.6% (3257 of 3720 strings)

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

powered by weblate
2020-07-27 18:03:45 +02:00
Yaling
5b3551fb60 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 82.8% (3081 of 3720 strings)

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

powered by weblate
2020-07-27 18:03:45 +02:00
Raphael Michel
72a5008513 Allow to remove a product from all sales channels 2020-07-27 18:03:26 +02:00
Raphael Michel
c5ace8447d Fix country fields always being required 2020-07-27 18:03:10 +02:00
Raphael Michel
28b82841c2 Use edgecase words correctly 2020-07-27 17:55:19 +02:00
Raphael Michel
fbe10a981b Add INV to edgecase words 2020-07-27 17:47:31 +02:00
Raphael Michel
21c22aa63a Revert "Next attempt to fix spellcheck"
This reverts commit fb4116012f.
2020-07-27 17:47:15 +02:00
Raphael Michel
fb4116012f Next attempt to fix spellcheck 2020-07-27 16:28:51 +02:00
Raphael Michel
53fe4a32cd PyPI CI: Add check-manifest and twine check 2020-07-27 16:25:23 +02:00
Raphael Michel
ff066898d4 Fix RTL issue 2020-07-27 16:25:23 +02:00
Felix Rindt
cbb848b3fa style 2020-07-24 18:47:59 +02:00
Felix Rindt
98dfdd8b01 no ugettext 2020-07-24 18:44:35 +02:00
Felix Rindt
0e95a7863f tests for placed and paid mails 2020-07-24 18:44:24 +02:00
Raphael Michel
0913f5bc18 Merge pull request #1729 from pretix/project-setup-things 2020-07-24 18:12:34 +02:00
Raphael Michel
d1eb4c4cce Add documentation on additional indices 2020-07-24 18:10:25 +02:00
Felix Rindt
4a0a3aff59 rename to download_reminder 2020-07-24 17:57:25 +02:00
Raphael Michel
83908fde45 [experimental] restructure order search query for different performance characteristics 2020-07-24 17:48:50 +02:00
Felix Rindt
143ac10991 rebase migration 2020-07-24 16:59:24 +02:00
Felix Rindt
413cbec4b9 code format 2020-07-24 16:58:05 +02:00
Felix Rindt
b168516d78 user guide 2020-07-24 16:58:05 +02:00
Felix Rindt
d0ccc42aff add test for ticket reminder (oops) 2020-07-24 16:58:05 +02:00
Felix Rindt
7aa793f4f7 fix name 2020-07-24 16:58:05 +02:00
Felix Rindt
1b48b519e3 add migration 2020-07-24 16:58:05 +02:00
Felix Rindt
5f502776b1 send canonical mails depending on sales channel 2020-07-24 16:58:05 +02:00
Felix Rindt
985e1ac9bf Fix TypeError: Unknown option(s) for shell command: skip_checks. 2020-07-24 16:53:51 +02:00
Felix Rindt
df1014d62f modernize isort config for v5.0 2020-07-24 15:55:24 +02:00
Felix Rindt
062afc42d3 change _decimal to decimal 2020-07-24 15:55:05 +02:00
Raphael Michel
1fb861a117 New attempt at improving CheckinList.checkin_count 2020-07-24 15:41:41 +02:00
Raphael Michel
0a2346778d Revert "Refactor query for check-in count"
This reverts commit 60eee25cd1.
2020-07-24 15:37:45 +02:00
Raphael Michel
605a21a0cf Typeahead: Remove ordering of orders to improve query performance 2020-07-24 15:29:26 +02:00
Raphael Michel
1cfec9cc99 Revert "Typeahead: No substring match in admin sessions"
This reverts commit 2626259492.
2020-07-24 15:28:14 +02:00
Raphael Michel
0a97b0ce67 Add progress bar for checkin list export 2020-07-24 13:54:38 +02:00
Raphael Michel
60eee25cd1 Refactor query for check-in count 2020-07-24 13:54:30 +02:00
Raphael Michel
779ec6c3f6 Metrics: Return accurate counts for less interesting models 2020-07-24 13:53:59 +02:00
Raphael Michel
988eb85c05 Fix exporter issue 2020-07-24 11:40:45 +02:00
Raphael Michel
556cb7c46d Add INV to wordlist 2020-07-24 11:01:29 +02:00
Raphael Michel
86e3c30633 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-07-24 10:44:45 +02:00
Raphael Michel
6276f213b9 Increae field size for CachedFile.file 2020-07-24 10:44:08 +02:00
Raphael Michel
524f6c9975 Merge pull request #1727 from pretix-translations/weblate-pretix-pretix 2020-07-24 10:43:45 +02:00
Andreas Teuber
125a14c8e9 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 82.9% (3080 of 3714 strings)

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

powered by weblate
2020-07-24 09:35:20 +02:00
Yaling
c7f0e6f652 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 82.9% (3080 of 3714 strings)

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

powered by weblate
2020-07-24 09:35:20 +02:00
Andreas Teuber
1e58ef6f9e Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 82.2% (3052 of 3714 strings)

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

powered by weblate
2020-07-24 09:35:20 +02:00
Raphael Michel
41127ce978 Fix issue in OrderListExporter when mixing subevents with non-subevents 2020-07-24 09:35:11 +02:00
Raphael Michel
99b42d201e Add missing tqdm binary 2020-07-24 09:08:22 +02:00
Raphael Michel
265da6c746 Progress bar instead of acks_late for event cancellation 2020-07-23 21:54:03 +02:00
Raphael Michel
d58c8559fc Long-running async tasks: Expose running state 2020-07-23 21:39:13 +02:00
Raphael Michel
b5dca762f0 Cancelling events: Fix send_waitinglist flag 2020-07-23 21:38:58 +02:00
Raphael Michel
a310c33497 Add progress bar to some large exports 2020-07-23 21:35:58 +02:00
Raphael Michel
fc5c3caf66 Fix memory usage in exporters by using chunked iterators 2020-07-23 20:39:49 +02:00
Raphael Michel
bff1041878 Excel export: Use openpyxl's constant memory implementation 2020-07-23 20:37:15 +02:00
Raphael Michel
2626259492 Typeahead: No substring match in admin sessions 2020-07-23 18:20:41 +02:00
Martin Gross
18415c62bb Cancellations now use up to date invoice issuer information and do not copy the information over from the original invoice. 2020-07-23 18:02:18 +02:00
Raphael Michel
85f546a3a6 Ignore deadlock when writing quota caches 2020-07-23 17:48:56 +02:00
Raphael Michel
829b0041fc Use database replica for check-in count for statistical purposes 2020-07-23 17:48:31 +02:00
Raphael Michel
4968a6d995 Do not count exists for checkin count 2020-07-23 17:48:18 +02:00
Raphael Michel
033deb7cf2 Add seat information to check-in list export 2020-07-23 12:26:54 +02:00
Felix Rindt
e23e88f5c3 Create invoice exporter mixin (#1725)
* Create invoice exporter mixin

* code style
2020-07-22 17:22:56 +02:00
Raphael Michel
c3745e792b Fix PaymentProviderForm issue 2020-07-22 16:09:57 +02:00
Raphael Michel
735d4564f8 Allow to change length of invoice numbers 2020-07-21 18:11:39 +02:00
Raphael Michel
b305ac012c Fix price field when increasing number of bundles in cart 2020-07-21 17:23:30 +02:00
Raphael Michel
7bd9a01f5e Fix error in price calculation in connection with free prices and bundles 2020-07-21 17:23:08 +02:00
Raphael Michel
8bebea61f1 Improve performance of quota cache task 2020-07-21 16:58:18 +02:00
Raphael Michel
6714ab24ee Force-upgrade hierarkey 2020-07-21 16:58:12 +02:00
Raphael Michel
a54dbc0110 Allow file upload in payment provider settings 2020-07-21 11:52:46 +02:00
Raphael Michel
19fa2fb016 CSP: Remove child-src, as it is redundant with frame-src and will get deprecated again 2020-07-21 10:59:13 +02:00
Raphael Michel
12b5d6663e Adjust widget tests 2020-07-21 10:09:51 +02:00
Raphael Michel
ca4db5f628 Widget: respect item.allow_waitinglist 2020-07-21 09:46:30 +02:00
Raphael Michel
b6a343a623 Add umzubuchen to German spelling list 2020-07-20 17:28:19 +02:00
Raphael Michel
dc451cdeea Merge pull request #1722 from pretix-translations/weblate-pretix-pretix 2020-07-20 17:24:51 +02:00
Raphael Michel
6732d13439 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3714 of 3714 strings)

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

powered by weblate
2020-07-20 17:24:32 +02:00
Raphael Michel
5bf67ba613 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3714 of 3714 strings)

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

powered by weblate
2020-07-20 17:24:31 +02:00
TimPrd
8885b50972 Translated on translate.pretix.eu (French)
Currently translated at 62.3% (2306 of 3699 strings)

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

powered by weblate
2020-07-20 16:43:06 +02:00
Raphael Michel
940566ab93 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-07-20 16:42:55 +02:00
Raphael Michel
e7b9c49620 Allow customers to change to a different product variation (#1719) 2020-07-20 16:36:24 +02:00
Raphael Michel
c8ef825de5 Try again to fix export tests 2020-07-20 14:56:03 +02:00
Raphael Michel
5b25a68599 Fix tests broken in 684212780 2020-07-20 14:27:37 +02:00
Raphael Michel
e26a07d44d Add documentation on URL interpolation in digital content module 2020-07-20 11:43:05 +02:00
Martin Gross
6842127802 Absent/Checked Out persons in Checkin lists (#1721) 2020-07-20 10:41:39 +02:00
Raphael Michel
3c5948d2e0 Allow selecting the same add-on multiple times (#1717) 2020-07-20 10:21:12 +02:00
Raphael Michel
ed3542e219 Fix error in quota statistics 2020-07-20 10:10:36 +02:00
Raphael Michel
e439b20618 Fix crash if gift card does not exist 2020-07-17 17:44:01 +02:00
Raphael Michel
5c1fe6f68c Bump to 3.11.0.dev0 2020-07-17 12:56:57 +02:00
Raphael Michel
e4e91523a0 Bump to 3.10.0 2020-07-17 12:56:11 +02:00
Raphael Michel
00827700de Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-07-17 09:50:28 +02:00
Raphael Michel
0a87225a9a Add missing fields to API 2020-07-17 09:31:25 +02:00
Raphael Michel
9371d221bf Merge pull request #1720 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-07-17 08:56:50 +02:00
Dennis Lichtenthäler
08a3c846b6 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3700 of 3700 strings)

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

powered by weblate
2020-07-17 00:00:15 +02:00
Raphael Michel
1c84de9ab2 E-Mail: Do not use .prettify(), it does not preserve the original whitespsace 2020-07-16 17:45:17 +02:00
Raphael Michel
980f4012bc Make sure correct language is active when generating email attachments 2020-07-16 12:14:43 +02:00
Raphael Michel
591d70eabe Merge pull request #1718 from pretix-translations/weblate-pretix-pretix
Co-authored-by: Raphael Michel <michel@rami.io>
2020-07-16 09:03:15 +02:00
Raphael Michel
2c4609604d Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3700 of 3700 strings)

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

powered by weblate
2020-07-16 09:00:34 +02:00
Raphael Michel
30c2b8b03f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3700 of 3700 strings)

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

powered by weblate
2020-07-16 09:00:33 +02:00
Raphael Michel
a685af6433 Clone button for products 2020-07-16 08:53:18 +02:00
Raphael Michel
f179a220bc Widget: Properly escape voucher codes 2020-07-16 08:42:40 +02:00
Raphael Michel
b61893e3b1 Do not even import excluded plugins 2020-07-16 08:42:29 +02:00
Raphael Michel
d3282a1acb Fix OrderChangeManager.change_price() for items without tax rule 2020-07-15 09:14:58 +02:00
Raphael Michel
c585946e72 Drop "Presale ::" from event page title 2020-07-14 16:53:26 +02:00
Raphael Michel
b6245b97ca Validate max length of attendee address 2020-07-14 16:26:46 +02:00
Raphael Michel
51720c3afe Fix irregular behaviour on second use of widget 2020-07-14 09:15:07 +02:00
Andreas Teuber
4746b8e456 Ask only for VAT ID if company is inside EU (#1709)
Co-authored-by: Andreas Teuber <andreas.teuber@passiv.de>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2020-07-13 18:04:09 +02:00
Raphael Michel
b2401f7641 Fix grammar error in German translation 2020-07-13 15:57:33 +02:00
Raphael Michel
b4cd11ef94 Fix creation of tax rules with custom rules 2020-07-13 15:57:33 +02:00
Raphael Michel
33682e1b38 Fix incorrect preview/history of subject rendering 2020-07-13 15:57:33 +02:00
Raphael Michel
e10e3300ba Fix ineffective translation of string 2020-07-12 11:01:15 +02:00
Raphael Michel
4f0eadfd6e Fix language file 2020-07-09 15:18:16 +02:00
Raphael Michel
0f9ec2ce7d Fix ZeroDivisionError in question statistics 2020-07-09 14:56:46 +02:00
Raphael Michel
93e3cf1d99 Update locales 2020-07-09 14:53:16 +02:00
Raphael Michel
3affaa8c85 Show local time if browser timezone is different 2020-07-09 14:51:16 +02:00
Raphael Michel
fddf134755 Update spelling wordlist 2020-07-09 10:55:34 +02:00
Raphael Michel
568398e4e7 Merge pull request #1715 from pretix-translations/weblate-pretix-pretix 2020-07-09 10:51:46 +02:00
Raphael Michel
fc08531639 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3700 of 3700 strings)

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

powered by weblate
2020-07-09 10:51:25 +02:00
Raphael Michel
bd25ce238d Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3700 of 3700 strings)

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

powered by weblate
2020-07-09 10:51:24 +02:00
Raphael Michel
c88ce8a9a8 Translated on translate.pretix.eu (German)
Currently translated at 99.4% (3673 of 3696 strings)

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

powered by weblate
2020-07-09 10:32:34 +02:00
Raphael Michel
004403e2c8 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-07-09 10:32:20 +02:00
Raphael Michel
0cde5288ac Fix trailing whitespace 2020-07-09 10:31:46 +02:00
Raphael Michel
e585da2901 Merge pull request #1704 from pretix-translations/weblate-pretix-pretix
Co-authored-by: Frank <webappconcept@gmail.com>
Co-authored-by: Dennis Lichtenthäler <lichtenthaeler@rami.io>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: oocf <oswaldocerna@gmail.com>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2020-07-09 10:31:23 +02:00
Raphael Michel
f369fca091 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-07-09 10:20:39 +02:00
Raphael Michel
e0e638ac8c Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-07-09 10:20:39 +02:00
Dennis Lichtenthäler
4abe906511 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-07-09 10:20:38 +02:00
Maarten van den Berg
855a47776f Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-07-09 10:20:24 +02:00
Maarten van den Berg
c63c499a95 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 99.8% (3676 of 3684 strings)

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

powered by weblate
2020-07-09 10:20:23 +02:00
Maarten van den Berg
f06a23ae95 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-07-09 10:20:09 +02:00
oocf
2b68a22aad Translated on translate.pretix.eu (Spanish)
Currently translated at 83.0% (3058 of 3684 strings)

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

powered by weblate
2020-07-09 10:19:56 +02:00
Raphael Michel
86d42cc524 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-07-09 10:19:36 +02:00
Dennis Lichtenthäler
6e8040ac9d Translated on translate.pretix.eu (German)
Currently translated at 100.0% (125 of 125 strings)

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

powered by weblate
2020-07-09 10:19:35 +02:00
Dennis Lichtenthäler
c742c9979b Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-07-09 10:19:19 +02:00
Frank
15f3880fcd Translated on translate.pretix.eu (Italian)
Currently translated at 17.2% (633 of 3684 strings)

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

powered by weblate
2020-07-09 10:18:50 +02:00
Raphael Michel
6bf5d8cb5e Revert "Update po files"
This reverts commit 27d772f52f.
2020-07-09 10:07:18 +02:00
Raphael Michel
27d772f52f Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-07-09 09:58:56 +02:00
Raphael Michel
6e9d921af6 Allow country specific tax rules (#1714) 2020-07-08 15:00:13 +02:00
Martin Gross
1c9a1b5e02 Mark invoice as dirty when changing subevent through OCM. 2020-07-07 16:03:55 +02:00
Raphael Michel
640c05729b Backend: Improve asynctask status feedback 2020-07-07 10:42:06 +02:00
Raphael Michel
fc9e5166da Order data and invoice data export: Add payment types 2020-07-07 09:14:06 +02:00
Raphael Michel
b1eb5bb3df Fix incorrect link 2020-07-06 15:37:16 +02:00
Raphael Michel
f690d74be7 Event list: Respect event date in "running" filter 2020-07-03 17:46:41 +02:00
Raphael Michel
c52fdc95a7 Allow to disable display of foreign currencies on invoices 2020-07-03 16:44:26 +02:00
Raphael Michel
039ca36233 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-07-03 14:58:08 +02:00
Raphael Michel
9920a47580 Multi export: Allow restriction to organizer 2020-07-03 11:56:36 +02:00
Raphael Michel
74acfbe2fd Fix issue with new pycountry version 2020-07-02 20:11:39 +02:00
Raphael Michel
b0b8f32cb9 Always show net total in backend 2020-07-02 19:32:11 +02:00
Raphael Michel
066cf510e3 Add tax total to cart view even if not showing net prices 2020-07-02 19:31:43 +02:00
Raphael Michel
aca963d960 Fix tax rate changes if there hasn't been a tax rate before 2020-07-02 19:24:18 +02:00
Raphael Michel
582c7b50f7 Do not parse list in rich_text_snippet 2020-07-02 12:00:18 +02:00
Martin Gross
d6b185193e Fix Timezone in Checkinlist 2020-07-01 12:24:04 +02:00
Raphael Michel
fb92d500be Fix accidentally commited settings changes 2020-07-01 10:43:37 +02:00
Raphael Michel
27ed9ae4fd Adjust colors in question statistics 2020-06-30 23:57:47 +02:00
Raphael Michel
06fbf56c04 Question stats: Show percentage 2020-06-30 23:18:12 +02:00
Raphael Michel
d843fc1545 Fix colors in graph of boolean questions 2020-06-30 23:04:17 +02:00
Raphael Michel
cf2af3c94d Import/startup performance improvements 2020-06-30 11:36:30 +02:00
Martin Gross
5f50aa95eb Add TaxRule selection in OrderPositionChange (#1700)
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2020-06-30 11:13:33 +02:00
Raphael Michel
626e332886 Fix issue displaying unlimited quota 2020-06-30 10:04:45 +02:00
Gamy
507e1a5b83 Added various missing punctuation marks, changed "following" to "selected" to match the display. 2020-06-30 09:20:11 +02:00
MrGamy
0d1aa2f96e Localization string unification changes
unifying the text bit 'Go to shop' to display 'shop' with a lowercase s in base.html
2020-06-29 18:02:13 +02:00
Raphael Michel
df6038e39b Merge pull request #1703 from pretix/quota_release_after_exit
Allow to release quota after exit scans
2020-06-29 14:35:39 +02:00
Raphael Michel
922f12f55e Allow to release quota after exit scans 2020-06-26 16:49:19 +02:00
Raphael Michel
fdea190d72 Merge pull request #1702 from pretix-translations/weblate-pretix-pretix
Co-authored-by: Frank <webappconcept@gmail.com>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2020-06-25 12:46:43 +02:00
Maarten van den Berg
34fe34d50a Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-06-24 19:40:23 +02:00
Frank
3412c1d2a9 Translated on translate.pretix.eu (Italian)
Currently translated at 16.9% (623 of 3684 strings)

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

powered by weblate
2020-06-24 19:40:23 +02:00
Maarten van den Berg
6f58f30d92 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.8% (3676 of 3684 strings)

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

powered by weblate
2020-06-24 19:40:23 +02:00
Frank
19b5610503 Translated on translate.pretix.eu (Italian)
Currently translated at 15.4% (569 of 3684 strings)

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

powered by weblate
2020-06-24 19:40:23 +02:00
Raphael Michel
55670b92a8 Merge branch 'master' of github.com:pretix/pretix 2020-06-24 19:40:12 +02:00
Raphael Michel
e5cc15ffac Improve responsiveness of organizer page 2020-06-24 19:39:57 +02:00
Martin Gross
249e6978ea Display subevent time for each item in addon and question step 2020-06-24 17:06:25 +02:00
Martin Gross
a223d57124 Reduce minimal waitinglist voucher validity to 1 hour 2020-06-24 13:26:38 +02:00
Raphael Michel
2a5c24482e Question list: Drop pagination, allow to mix ordering with system fields 2020-06-23 13:05:54 +02:00
Martin Gross
868292f9b3 Fix All Invoice-export (missing file extension; missing "all payment providers") 2020-06-23 10:58:39 +02:00
Raphael Michel
5c24fd966a Fix locale error 2020-06-22 16:09:55 +02:00
Raphael Michel
61490a9ee8 Merge pull request #1701 from pretix-translations/weblate-pretix-pretix 2020-06-22 09:42:55 +02:00
Frank
3c3333c485 Translated on translate.pretix.eu (Italian)
Currently translated at 15.3% (565 of 3684 strings)

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

powered by weblate
2020-06-22 09:42:19 +02:00
Frank
4c19002be6 Translated on translate.pretix.eu (Italian)
Currently translated at 14.7% (541 of 3684 strings)

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

powered by weblate
2020-06-20 19:11:38 +02:00
Frank
2c49eaeef8 Translated on translate.pretix.eu (Italian)
Currently translated at 14.7% (540 of 3684 strings)

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

powered by weblate
2020-06-20 19:11:38 +02:00
Raphael Michel
481e29c3b2 Allow to explicitly disable products for certain subevents 2020-06-20 19:10:44 +02:00
Raphael Michel
0aebde62eb Fix missing variation attributes when copying items 2020-06-20 18:21:12 +02:00
Raphael Michel
49e44f68ba Merge pull request #1699 from pretix-translations/weblate-pretix-pretix 2020-06-19 13:10:52 +02:00
Frank
84dc9f241d Translated on translate.pretix.eu (Italian)
Currently translated at 14.6% (538 of 3684 strings)

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

powered by weblate
2020-06-19 13:07:28 +02:00
Frank
07b05f4a44 Translated on translate.pretix.eu (Italian)
Currently translated at 13.6% (500 of 3684 strings)

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

powered by weblate
2020-06-19 01:00:12 +02:00
Raphael Michel
d1c1aed1f2 Widget: Improve support for week calendars 2020-06-18 17:43:22 +02:00
Raphael Michel
de9f7248cc Widget: Fix issue navigating back to month calendar 2020-06-18 16:38:58 +02:00
Raphael Michel
0d45706608 Try to fix widget bug in IE introduced in ebb1cc1be 2020-06-18 12:33:26 +02:00
Raphael Michel
016dd88e8b Remove Italian from incubating languages 2020-06-18 12:13:28 +02:00
Raphael Michel
6362c27cba Merge pull request #1697 from pretix-translations/weblate-pretix-pretix 2020-06-18 12:12:57 +02:00
Frank
7396f29b82 Translated on translate.pretix.eu (Italian)
Currently translated at 11.6% (427 of 3684 strings)

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

powered by weblate
2020-06-18 12:00:52 +02:00
Frank
ff9f6b6a36 Translated on translate.pretix.eu (Italian)
Currently translated at 10.3% (378 of 3684 strings)

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

powered by weblate
2020-06-17 18:08:14 +02:00
Raphael Michel
7359b5543d Set calendar as default for event series 2020-06-17 18:07:54 +02:00
Raphael Michel
74a0cafa0f Revert "Switch to calendar at 25 subevents already"
This reverts commit 8001063347.
2020-06-17 18:07:07 +02:00
Raphael Michel
16472e915d PDF: Show event time on default layout 2020-06-17 12:27:38 +02:00
Martin Gross
ec6844f900 Display Boxoffice iZettle payment details 2020-06-16 22:11:54 +02:00
Raphael Michel
e6455f8204 Organizer-level export: Fix incorrect event queryset 2020-06-16 12:03:59 +02:00
Raphael Michel
4c48fcd861 Move export down in organizer navigation 2020-06-16 11:39:44 +02:00
Raphael Michel
adfd7834fb Add subevent date to ticket filename 2020-06-16 11:37:47 +02:00
Raphael Michel
a4b8315487 Merge pull request #1695 from pretix-translations/weblate-pretix-pretix 2020-06-16 11:07:08 +02:00
Frank
8594fecad4 Translated on translate.pretix.eu (Italian)
Currently translated at 9.0% (331 of 3684 strings)

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

powered by weblate
2020-06-16 11:06:46 +02:00
Frank
61979f0c40 Translated on translate.pretix.eu (Italian)
Currently translated at 8.4% (311 of 3684 strings)

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

powered by weblate
2020-06-16 11:06:46 +02:00
Frank
df5f8a340b Translated on translate.pretix.eu (Italian)
Currently translated at 8.4% (310 of 3684 strings)

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

powered by weblate
2020-06-16 11:06:46 +02:00
Frank
651e797264 Translated on translate.pretix.eu (Italian)
Currently translated at 7.9% (290 of 3684 strings)

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

powered by weblate
2020-06-16 11:06:46 +02:00
Frank
d40010fab6 Translated on translate.pretix.eu (Italian)
Currently translated at 7.4% (271 of 3684 strings)

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

powered by weblate
2020-06-16 11:06:45 +02:00
Frank
89c3d59e6d Translated on translate.pretix.eu (Italian)
Currently translated at 7.0% (257 of 3684 strings)

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

powered by weblate
2020-06-16 11:06:45 +02:00
Frank
b30569a941 Translated on translate.pretix.eu (Italian)
Currently translated at 100.0% (125 of 125 strings)

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

powered by weblate
2020-06-16 11:06:45 +02:00
Frank
fe230fe56d Translated on translate.pretix.eu (Italian)
Currently translated at 6.4% (236 of 3684 strings)

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

powered by weblate
2020-06-16 11:06:45 +02:00
Martin Gross
0b20d3f6f8 Organizer/MultiEvent-Exports (#1684)
Co-authored-by: Raphael Michel <michel@rami.io>
2020-06-16 11:06:40 +02:00
Raphael Michel
e895c13b28 Re-label "Buy tickets" button 2020-06-16 10:35:07 +02:00
Martin Gross
5cc0bd5d36 Optional PayPal Reference Prefix (Z#2359330) (#1696)
* Optional PayPal Reference Prefix (Z#2359330)

* Move prefix to end
2020-06-15 18:23:01 +02:00
Raphael Michel
569379e508 Order API: Add search 2020-06-15 15:12:09 +02:00
Raphael Michel
d975a68641 Allow to turn off CSP reporting 2020-06-15 15:12:09 +02:00
Raphael Michel
c992de341f Revert "PayPal: Add additional protection against invalid sessions"
This reverts commit 99e02bde36.
2020-06-14 12:29:20 +02:00
Raphael Michel
11cc27dbd6 Fix crash when trying to refund an order with a disabled payment mehtod 2020-06-12 14:01:15 +02:00
Raphael Michel
90e70eae25 Fix test (see 45f120b0c) 2020-06-12 13:58:49 +02:00
Raphael Michel
9eacd38ec7 PayPal: Improve handling of exceptions form paypalrestsdk 2020-06-12 13:21:44 +02:00
Raphael Michel
d1c96aa77c PayPal: Remove unused session key 2020-06-12 13:21:44 +02:00
Raphael Michel
99e02bde36 PayPal: Add additional protection against invalid sessions 2020-06-12 13:21:44 +02:00
Raphael Michel
e7da2aec53 PayPal: Fix critical bug leading to wrong order being paid in a rare session constellation 2020-06-12 13:21:44 +02:00
Raphael Michel
d0c6f0f0e9 Allow to shred data 30 days after event (instead of 60) 2020-06-11 10:44:27 +02:00
Raphael Michel
3ae148956f Add spellcheck lists 2020-06-10 18:24:00 +02:00
Raphael Michel
e0436039d2 Merge pull request #1694 from pretix-translations/weblate-pretix-pretix 2020-06-10 18:15:40 +02:00
Raphael Michel
29510b8617 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-06-10 18:13:54 +02:00
Raphael Michel
2d88da3a67 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3684 of 3684 strings)

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

powered by weblate
2020-06-10 18:13:53 +02:00
Raphael Michel
fbb88602d4 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-06-10 18:02:48 +02:00
Raphael Michel
7d7820a4ee Merge pull request #1693 from pretix/series-creation 2020-06-10 18:02:16 +02:00
Raphael Michel
8001063347 Switch to calendar at 25 subevents already 2020-06-10 18:01:52 +02:00
Raphael Michel
0c3a200355 Do not auto-create a first subevent 2020-06-10 17:59:59 +02:00
Maico Timmerman
ebb1cc1be7 Fix #1689 -- Widget: disable buy button without selected products (#1692) 2020-06-10 17:58:10 +02:00
Raphael Michel
45f120b0c3 API: Modified settings endpoint for devices 2020-06-10 17:45:31 +02:00
Martin Gross
bae0e45d00 Add W-indicator to Week-Calendar Dropdown 2020-06-10 12:59:21 +02:00
Martin Gross
057fd95706 Fix op.address_format() when no zip code is provided 2020-06-10 11:01:08 +02:00
Martin Gross
597d4aa206 Fix attendee_address 2020-06-10 10:32:20 +02:00
Raphael Michel
7f9b245eb5 Fix Preview button for ticket formats 2020-06-09 09:54:28 +02:00
Raphael Michel
42490c6dec New event series selection field 2020-06-08 16:20:50 +02:00
Raphael Michel
60c0b7da12 If date isn't shown on front page, don't show it on invoices 2020-06-08 14:49:06 +02:00
Raphael Michel
7d41922274 Minor improvemnet to widget views 2020-06-07 12:07:03 +02:00
Raphael Michel
fc7fbf31c5 Keep cached tickets around for a shorter interval 2020-06-05 12:10:10 +02:00
Raphael Michel
da5433325c Fix hardcoded ID in test 2020-06-05 10:03:16 +02:00
Raphael Michel
939a38d53b Fix subsequent issue on event level domains 2020-06-04 20:32:21 +02:00
Raphael Michel
a57280004e Fix another multidomain issue 2020-06-04 20:15:26 +02:00
Raphael Michel
ce896cec8f Fix bug in previosu commit 2020-06-04 19:05:07 +02:00
Raphael Michel
effc9723f1 Add event meta data fields to order search form 2020-06-04 18:39:30 +02:00
Raphael Michel
cd5f6b66a1 Do not cache event/organizer instance in multi domain middleware (might be harmful) 2020-06-04 18:33:15 +02:00
Raphael Michel
0d35064d21 Order create API: Fix addon_to in simulated mode
PRETIXEU-275
2020-06-02 16:48:52 +02:00
Raphael Michel
314ce5467f Disable autocomplete for all date/time picker fields 2020-06-02 12:42:12 +02:00
Raphael Michel
d97ef380a4 Bump to 3.10.0.dev0 2020-06-02 12:33:17 +02:00
371 changed files with 110362 additions and 87676 deletions

View File

@@ -31,7 +31,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt install enchant hunspell aspell-en
run: sudo apt update && sudo apt install enchant hunspell aspell-en
- name: Install Dependencies
run: pip3 install --no-use-pep517 -Ur doc/requirements.txt
- name: Spellcheck docs

View File

@@ -29,7 +29,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt install gettext
run: sudo apt update && sudo apt install gettext
- name: Install Dependencies
run: pip3 install --no-use-pep517 -Ur src/requirements.txt
- name: Compile messages
@@ -54,7 +54,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
run: sudo apt update && sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
- name: Install Dependencies
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
- name: Spellcheck translations

View File

@@ -31,7 +31,7 @@ jobs:
- name: Install Dependencies
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
- name: Run isort
run: isort -c -rc -df .
run: isort -c .
working-directory: ./src
flake:
name: flake8

View File

@@ -55,7 +55,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt install gettext mysql-client
run: sudo apt update && sudo apt install gettext mysql-client
- name: Install Python dependencies
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
- name: Run checks

View File

@@ -20,15 +20,17 @@ pypi:
- cp /keys/.pypirc ~/.pypirc
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- pip install -U pip wheel setuptools check-manifest twine
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
- cd src
- python setup.py sdist
- pip install dist/pretix-*.tar.gz
- python -m pretix migrate
- python -m pretix check
- python setup.py sdist upload
- python setup.py bdist_wheel upload
- check-manifest
- python setup.py sdist bdist_wheel
- twine check dist/*
- twine upload dist/*
tags:
- python3
only:

View File

@@ -29,7 +29,7 @@ RUN apt-get update && \
mkdir /etc/pretix && \
mkdir /data && \
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
echo 'pretixuser ALL=(ALL) NOPASSWD: /usr/bin/supervisord' >> /etc/sudoers && \
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
mkdir /static
ENV LC_ALL=C.UTF-8 \

View File

@@ -19,9 +19,8 @@ Reinventing ticket presales, one ticket at a time.
Project status & release cycle
------------------------------
While there is always a lot to do and improve on, pretix by now has been in use for more than a dozen
conferences that sold over ten thousand tickets combined without major problems. We therefore think of
pretix as being stable and ready to use.
While there is always a lot to do and improve on, pretix by now has been in use for thousands of events
conferences that sold millions of tickets combined. We therefore think of pretix as being stable and ready to use.
If you want to use or extend pretix, we strongly recommend to follow our `blog`_. We will announce all
releases there. You can always find the latest stable version on PyPI or in the ``release/X.Y`` branch of
@@ -30,9 +29,13 @@ the sense that it does not break your data, but its APIs might change without p
To get started using pretix on your own server, look at the `installation guide`_ in our documentation.
This project is 100 percent free and open source software. If you are interested in commercial support,
hosting services or supporting this project financially, please go to `pretix.eu`_ or contact us at
support@pretix.eu.
Support
-------
This project is 100 percent free and open source software. You are welcome to ask questions in the GitHub
repository. Private support via email or phone is only offered to customers of our pretix Hosted or pretix
Enterprise offerings. If you are interested in commercial support, hosting services or supporting this project
financially, please go to `pretix.eu`_ or contact us at support@pretix.eu.
Contributing
------------
@@ -52,8 +55,8 @@ License
The code in this repository is published under the terms of the Apache License.
See the LICENSE file for the complete license text.
This project is maintained by Raphael Michel <mail@raphaelmichel.de>. See the
AUTHORS file for a list of all the awesome folks who contributed to this project.
This project is maintained by Raphael Michel. See the AUTHORS file for a list of all
the awesome folks who contributed to this project.
.. _installation guide: https://docs.pretix.eu/en/latest/admin/installation/index.html
.. _developer documentation: https://docs.pretix.eu/en/latest/development/index.html

View File

@@ -19,7 +19,7 @@ fi
python3 -m pretix migrate --noinput
if [ "$1" == "all" ]; then
exec sudo /usr/bin/supervisord -n -c /etc/supervisord.conf
exec sudo -E /usr/bin/supervisord -n -c /etc/supervisord.conf
fi
if [ "$1" == "webworker" ]; then

View File

@@ -95,6 +95,11 @@ Example::
proxy that actively removes and re-adds the header to make sure the correct value is set.
Defaults to ``off``.
``csp_log``
Log violations of the Content Security Policy (CSP). Defaults to ``on``.
``loglevel``
Set console and file loglevel (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` or ``CRITICAL``). Defaults to ``INFO``.
Locale settings
---------------
@@ -337,6 +342,15 @@ application. If you want to use sentry, you need to set a DSN in the configurati
You will be given this value by your sentry installation.
Caching
-------
You can adjust some caching settings to control how much storage pretix uses::
[cache]
tickets=48 ; Number of hours tickets (PDF, passbook, …) are cached
Secret length
-------------

View File

@@ -12,3 +12,4 @@ This documentation is for everyone who wants to install pretix on a server.
config
maintainance
scaling
indexes

73
doc/admin/indexes.rst Normal file
View File

@@ -0,0 +1,73 @@
Additional database indices
===========================
If you have a large pretix database, some features such as search for orders or events might turn pretty slow.
For PostgreSQL, we have compiled a list of additional database indexes that you can add to speed things up.
Just like any index, they in turn make write operations insignificantly slower and cause the database to use
more disk space.
The indexes aren't automatically created by pretix since Django does not allow us to do so only on PostgreSQL
(and they won't work on other databases). Also, they're really not necessary if you're not having tens of
thousands of records in your database.
However, this also means they won't automatically adapt if some of the referred fields change in future updates of pretix
and you might need to re-check this page and change them manually.
Here is the currently recommended set of commands::
CREATE EXTENSION pg_trgm;
CREATE INDEX CONCURRENTLY pretix_addidx_event_slug
ON pretixbase_event
USING gin (upper("slug") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_event_name
ON pretixbase_event
USING gin (upper("name") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_order_code
ON pretixbase_order
USING gin (upper("code") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_voucher_code
ON pretixbase_voucher
USING gin (upper("code") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_invoice_nu1
ON "pretixbase_invoice" (UPPER("invoice_no"));
CREATE INDEX CONCURRENTLY pretix_addidx_invoice_nu2
ON "pretixbase_invoice" (UPPER("full_invoice_no"));
CREATE INDEX CONCURRENTLY pretix_addidx_organizer_name
ON pretixbase_organizer
USING gin (upper("name") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_organizer_slug
ON pretixbase_organizer
USING gin (upper("slug") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_order_email
ON pretixbase_order
USING gin (upper("email") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_order_comment
ON pretixbase_order
USING gin (upper("comment") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_name
ON pretixbase_orderposition
USING gin (upper("attendee_name_cached") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_scret
ON pretixbase_orderposition
USING gin (upper("secret") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email
ON pretixbase_orderposition
USING gin (upper("attendee_email") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_ia_name
ON pretixbase_invoiceaddress
USING gin (upper("name_cached") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_ia_company
ON pretixbase_invoiceaddress
USING gin (upper("company") gin_trgm_ops);
Also, if you use our ``pretix-shipping`` plugin::
CREATE INDEX CONCURRENTLY pretix_addidx_sa_name
ON pretix_shipping_shippingaddress
USING gin (upper("name") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_sa_company
ON pretix_shipping_shippingaddress
USING gin (upper("company") gin_trgm_ops);

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`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `PostgreSQL`_ 9.5+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ 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
@@ -290,7 +290,7 @@ to re-build your custom image after you pulled ``pretix/standalone`` if you want
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-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

@@ -23,7 +23,7 @@ installation guides):
* 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`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `PostgreSQL`_ 9.5+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ 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
@@ -308,7 +308,7 @@ example::
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-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

@@ -92,7 +92,8 @@ pretix_task_duration_seconds
pretix_model_instances
Gauge. Measures number of instances of a certain model within the database, labeled with
the ``model`` name.
the ``model`` name. Starting with pretix 3.11, these numbers might only be approximate for
most tables when running on PostgreSQL to mitigate performance impact.
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
.. _Prometheus: https://prometheus.io/

View File

@@ -7,9 +7,6 @@ This part of the documentation contains information about the REST-style API
exposed by pretix since version 1.5 that can be used by third-party programs
to interact with pretix and its data structures.
Currently, the API provides mostly read-only capabilities, but it will be extended
in functionality over time.
.. toctree::
:maxdepth: 2

View File

@@ -25,7 +25,7 @@ Obtaining an authorization grant
--------------------------------
To authorize a new user, link or redirect them to the ``authorize`` endpoint, passing your client ID as a query
parameter. Additionally, you can pass a scope (currently either ``read``, ``write``, or ``read write``)
parameter. Additionally, you can pass a scope (currently either ``read``, ``write``, ``read write`` or ``profile``)
and an URL the user should be redirected to after successful or failed authorization. You also need to pass the
``response_type`` parameter with a value of ``code``. Example::
@@ -47,11 +47,9 @@ You will need this ``code`` parameter to perform the next step.
On a failed registration, a query string like ``?error=access_denied`` will be appended to the redirection URL.
.. note:: In this step, the user is allowed to restrict your access to certain organizer accounts. If you try to
re-authenticate the user later, the user might be instantly redirected back to you if authorization is already
given and would therefore be unable to review their organizer restriction settings. You can append the
``approval_prompt=force`` query parameter if you want to make sure the user actively needs to confirm the
authorization.
.. note:: By default, the user is asked to give permission on every call to this URL. If you **only** request the
``profile`` scope, i.e. no access to organizer data, you can pass the ``approval_prompt=auto`` parameter
to skip user interaction on subsequen calls.
Getting an access token
-----------------------
@@ -193,10 +191,11 @@ If you need the user's meta data, you can fetch it here:
Content-Type: application/json
{
email: "admin@localhost",
fullname: "John Doe",
locale: "de",
timezone: "Europe/Berlin"
"email": "admin@localhost",
"fullname": "John Doe",
"locale": "de",
"is_staff": false,
"timezone": "Europe/Berlin"
}
:statuscode 200: no error

View File

@@ -56,6 +56,10 @@ rules object Custom check-in
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
``allow_entry_after_exit``, and ``rules`` attributes have been added.
.. versionchanged:: 3.11
The ``subevent_match`` and ``exclude`` query parameters have been added.
Endpoints
---------
@@ -109,6 +113,8 @@ Endpoints
:query integer page: The page number in case of a multi-page result set, default is 1
:query integer subevent: Only return check-in lists of the sub-event with the given ID
:query integer subevent_match: Only return check-in lists that are valid for the sub-event with the given ID (i.e. also lists valid for all subevents)
:query string exclude: Exclude a field from the output, e.g. ``checkin_count``. Can be used as a performance optimization. Can be passed multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
@@ -602,6 +608,7 @@ Order position endpoints
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
questions that have not been filled. Defaults to ``false``.
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
list.

View File

@@ -0,0 +1,224 @@
.. spelling:: fullname
.. _`rest-devices`:
Devices
=======
See also :ref:`rest-deviceauth`.
Device resource
----------------
The device resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
device_id integer Internal ID of the device within this organizer
unique_serial string Unique identifier of this device
name string Device name
all_events boolean Whether this device has access to all events
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)
software_brand string Device software product (read-only)
software_version string Device software version (read-only)
created datetime Creation time
initialized datetime Time of initialization (or ``null``)
initialization_token string Token for initialization
revoked boolean Whether this device no longer has access
security_profile string The name of a supported security profile restricting API access
===================================== ========================== =======================================================
Device endpoints
----------------
.. http:get:: /api/v1/organizers/(organizer)/devices/
Returns a list of all devices within a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/devices/ 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": [
{
"device_id": 1,
"unique_serial": "UOS3GNZ27O39V3QS",
"initialization_token": "frkso3m2w58zuw70",
"all_events": false,
"limit_events": [
"museum"
],
"revoked": false,
"name": "Scanner",
"created": "2020-09-18T14:17:40.971519Z",
"initialized": "2020-09-18T14:17:44.190021Z",
"security_profile": "full",
"hardware_brand": "Zebra",
"hardware_model": "TC25",
"software_brand": "pretixSCAN",
"software_version": "1.5.1"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
: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)/devices/(device_id)/
Returns information on one device, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/devices/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"device_id": 1,
"unique_serial": "UOS3GNZ27O39V3QS",
"initialization_token": "frkso3m2w58zuw70",
"all_events": false,
"limit_events": [
"museum"
],
"revoked": false,
"name": "Scanner",
"created": "2020-09-18T14:17:40.971519Z",
"initialized": "2020-09-18T14:17:44.190021Z",
"security_profile": "full",
"hardware_brand": "Zebra",
"hardware_model": "TC25",
"software_brand": "pretixSCAN",
"software_version": "1.5.1"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param device_id: The ``device_id`` field of the device to fetch
: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)/devices/
Creates a new device
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/devices/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "Scanner",
"all_events": true,
"limit_events": [],
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"device_id": 1,
"unique_serial": "UOS3GNZ27O39V3QS",
"initialization_token": "frkso3m2w58zuw70",
"all_events": true,
"limit_events": [],
"revoked": false,
"name": "Scanner",
"created": "2020-09-18T14:17:40.971519Z",
"security_profile": "full",
"initialized": null
"hardware_brand": null,
"hardware_model": null,
"software_brand": null,
"software_version": null
}
:param organizer: The ``slug`` field of the organizer to create a device for
:statuscode 201: no error
:statuscode 400: The device 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)/devices/(device_id)/
Update a device.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/devices/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"name": "Foo"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": "Foo",
...
}
:param organizer: The ``slug`` field of the organizer to modify
:param device_id: The ``device_id`` field of the deviec to modify
:statuscode 200: no error
:statuscode 400: The device 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.

View File

@@ -526,7 +526,7 @@ information about the properties.
Get current values of event settings.
Permission required: "Can change event settings"
Permission required: "Can change event settings" (Exception: with device auth, *some* settings can always be *read*.)
**Example request**:

View File

@@ -209,14 +209,15 @@ Endpoints
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/giftcards/1/transact/ HTTP/1.1
POST /api/v1/organizers/bigevents/giftcards/1/transact/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
Content-Length: 79
{
"value": "2.00"
"value": "2.00",
"text": "Optional value explaining the transaction"
}
**Example response**:

View File

@@ -24,6 +24,7 @@ Resources and endpoints
giftcards
carts
teams
devices
webhooks
seatingplans
billing_invoices

View File

@@ -24,6 +24,7 @@ addon_category integer Internal ID of
min_count integer The minimal number of add-ons that need to be chosen.
max_count integer The maximal number of add-ons that can be chosen.
position integer An integer, used for sorting
multi_allowed boolean Adding the same item multiple times is allowed
price_included boolean Adding this add-on to the item is free
===================================== ========================== =======================================================
@@ -65,6 +66,7 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 0,
"multi_allowed": false,
"price_included": true
},
{
@@ -73,6 +75,7 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}
]
@@ -112,6 +115,7 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}
@@ -141,6 +145,7 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}
@@ -158,6 +163,7 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}
@@ -206,6 +212,7 @@ Endpoints
"min_count": 0,
"max_count": 10,
"position": 1,
"multi_allowed": false,
"price_included": true
}

View File

@@ -104,6 +104,7 @@ addons list of objects Definition of a
├ min_count integer The minimal number of add-ons that need to be chosen.
├ max_count integer The maximal number of add-ons that can be chosen.
├ position integer An integer, used for sorting
├ multi_allowed boolean Adding the same item multiple times is allowed
└ price_included boolean Adding this add-on to the item is free
bundles list of objects Definition of bundles that are included in this item.
Only writable during creation,
@@ -159,6 +160,10 @@ meta_data object Values set for
The attribute ``meta_data`` has been added.
.. versionchanged:: 3.10
The attribute ``multi_allowed`` has been added to ``addons``.
Notes
-----

View File

@@ -155,6 +155,14 @@ last_modified datetime Last modificati
The ``reactivate`` operation has been added.
.. versionchanged:: 3.10
The ``search`` query parameter has been added.
.. versionchanged:: 3.11
The ``exclude`` and ``subevent_after`` query parameter has been added.
.. _order-position-resource:
@@ -193,6 +201,7 @@ addon_to integer Internal ID of
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
checkins list of objects List of check-ins with this ticket
├ id integer Internal ID of the check-in event
├ list integer Internal ID of the check-in list
├ datetime datetime Time of check-in
├ type string Type of scan (defaults to ``entry``)
@@ -468,6 +477,7 @@ List of all orders
``last_modified``, and ``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query string search: Only return orders matching a given search query
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
``require_approval`` will be returned.
@@ -480,6 +490,8 @@ List of all orders
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
you will not notice it using this method.
:query datetime created_since: Only return orders that have been created since the given date.
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date.
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
@@ -923,7 +935,8 @@ Creating orders
during order generation and is not respected automatically when the order changes later.)
* ``force`` (optional). If set to ``true``, quotas will be ignored.
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of
whether these emails are enabled for certain sales channels. Defaults to
``false``.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
@@ -1017,6 +1030,10 @@ Creating orders
Order state operations
----------------------
.. versionchanged:: 3.12
The ``mark_paid`` operation now takes a ``send_email`` parameter.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
Marks a pending or expired order as successfully paid.
@@ -1028,6 +1045,11 @@ Order state operations
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/mark_paid/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"send_email": true
}
**Example response**:
@@ -1710,6 +1732,10 @@ Order payment endpoints
Payments can now be created through the API.
.. versionchanged:: 3.12
The ``confirm`` operation now takes a ``send_email`` parameter.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
Returns a list of all payments for an order.
@@ -1810,7 +1836,10 @@ Order payment endpoints
Accept: application/json, text/javascript
Content-Type: application/json
{"force": false}
{
"send_email": true,
"force": false
}
**Example response**:

View File

@@ -26,6 +26,8 @@ close_when_sold_out boolean If ``true``, th
again.
closed boolean Whether the quota is currently closed (see above
field).
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
have been scanned at an exit.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -36,6 +38,10 @@ closed boolean Whether the quo
The attributes ``close_when_sold_out`` and ``closed`` have been added.
.. versionchanged:: 3.10
The attribute ``release_after_exit`` has been added.
Endpoints
---------
@@ -283,6 +289,7 @@ Endpoints
"total_size": 1000,
"pending_orders": 25,
"paid_orders": 423,
"exited_orders": 0,
"cart_positions": 7,
"blocking_vouchers": 126,
"waiting_list": 0

View File

@@ -39,10 +39,12 @@ geo_lon float Longitude of th
item_price_overrides list of objects List of items for which this sub-event overrides the
default price
├ item integer The internal item ID
├ disabled boolean If ``true``, item should not be available for this sub-event
└ price money (string) The price or ``null`` for the default price
variation_price_overrides list of objects List of variations for which this sub-event overrides
the default price
├ variation integer The internal variation ID
├ disabled boolean If ``true``, variation should not be available for this sub-event
└ price money (string) The price or ``null`` for the default price
meta_data object Values set for organizer-specific meta data parameters.
seating_plan integer If reserved seating is in use, the ID of a seating
@@ -74,6 +76,10 @@ seat_category_mapping object An object mappi
The attributes ``geo_lat`` and ``geo_lon`` have been added.
.. versionchanged:: 3.10
The ``disabled`` attribute has been added to ``item_price_overrides`` and ``variation_price_overrides``.
Endpoints
---------
@@ -125,6 +131,7 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -182,6 +189,7 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -216,6 +224,7 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -271,6 +280,7 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -307,6 +317,7 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "23.42"
}
],
@@ -339,6 +350,7 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "23.42"
}
],
@@ -427,6 +439,7 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],

View File

@@ -29,6 +29,22 @@ that we'll provide in this plugin::
from .exporter import MyExporter
return MyExporter
Some exporters might also prove to be useful, when provided on an organizer-level. In order to declare your
exporter as capable of providing exports spanning multiple events, your plugin should listen for this signal
and return the subclass of ``pretix.base.exporter.BaseExporter`` that we'll provide in this plugin::
from django.dispatch import receiver
from pretix.base.signals import register_multievent_data_exporters
@receiver(register_multievent_data_exporters, dispatch_uid="multieventexporter_myexporter")
def register_multievent_data_exporter(sender, **kwargs):
from .exporter import MyExporter
return MyExporter
If your exporter supports both event-level and multi-event level exports, you will need to listen for both
signals.
The exporter class
------------------

View File

@@ -33,7 +33,7 @@ Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
.. automodule:: pretix.presale.signals
@@ -66,19 +66,13 @@ Vouchers
""""""""
.. automodule:: pretix.control.signals
:members: item_forms
Vouchers
""""""""
.. automodule:: pretix.control.signals
:members: voucher_form_class, voucher_form_html, voucher_form_validation
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
Dashboards
""""""""""
.. automodule:: pretix.control.signals
:members: event_dashboard_widgets, user_dashboard_widgets
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
Ticket designs
""""""""""""""

View File

@@ -126,6 +126,8 @@ The provider class
.. autoattribute:: test_mode_message
.. autoattribute:: requires_invoice_immediately
Additional views
----------------

View File

@@ -136,7 +136,7 @@ in the ``installed`` method::
pass # Your code here
Note that ``installed`` will *not* be called if the plugin in indirectly activated for an event
Note that ``installed`` will *not* be called if the plugin is indirectly activated for an event
because the event is created with settings copied from another event.
Views
@@ -151,8 +151,8 @@ your Django app label.
with checking that the calling user is logged in, has appropriate permissions,
etc. We plan on providing native support for this in a later version.
.. _Django app: https://docs.djangoproject.com/en/1.7/ref/applications/
.. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/
.. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/
.. _Django app: https://docs.djangoproject.com/en/3.0/ref/applications/
.. _signal dispatcher: https://docs.djangoproject.com/en/3.0/topics/signals/
.. _namespace packages: https://legacy.python.org/dev/peps/pep-0420/
.. _entry point: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#locating-plugins
.. _cookiecutter: https://cookiecutter.readthedocs.io/en/latest/

View File

@@ -7,7 +7,7 @@ Coding style and quality
for more information. Use four spaces for indentation.
* We sort our imports by a certain schema, but you don't have to do this by hand. Again, ``setup.cfg`` contains
some definitions that allow the command ``isort -rc <directory>`` to automatically sort the imports in your source
some definitions that allow the command ``isort <directory>`` to automatically sort the imports in your source
files.
* For templates and models, please take a look at the `Django Coding Style`_. We like Django's `class-based views`_ and

View File

@@ -98,7 +98,7 @@ pull request nevertheless and ask us for help, we are happy to assist you.
Execute the following commands to check for code style errors::
flake8 .
isort -c -rc .
isort -c .
python manage.py check
Execute the following command to run pretix' test suite (might take a couple of minutes)::
@@ -121,7 +121,7 @@ for example, to check for any errors in any staged files when committing::
do
echo $file
git show ":$file" | flake8 - --stdin-display-name="$file" || exit 1 # we only want to lint the staged changes, not any un-staged changes
git show ":$file" | isort -df --check-only - | grep ERROR && exit 1 || true
git show ":$file" | isort -c - | grep ERROR && exit 1 || true
done

View File

@@ -1,12 +1,86 @@
Digital content
===============
URL interpolation and JWT authentication
----------------------------------------
In the simplest case, you can use the digital content module to point users to a specific piece of content on some
platform after their ticket purchase, or show them an embedded video or live stream. However, the full power of the
module can be utilized by passing additional information to the target system to automatically authenticate the user
or pre-fill some fields with their data. For example, you could use an URL like this::
https://webinars.example.com/join?as={attendee_name}&userid={order_code}-{positionid}
While this is already useful, it does not provide much security anyone could guess a valid combination for that URL.
Therefore, the module allows you to pass information as a `JSON Web Token`_, which isn't encrypted, but signed with a
shared secret such that nobody can create their own tokens or modify the contents. To use a token, set up a URL like this::
https://webinars.example.com/join?with_token={token}
Additionally, you will need to set a JWT secret and a token template, either through the pretix interface or through the
API (see below). pretix currently only supports tokens signed with ``HMAC-SHA256`` (``HS256``). Your token template can contain
whatever JSON you'd like to pass on based on the same variables, for example::
{
"iss": "pretix.eu",
"aud": "webinars.example.com",
"user": {
"id": "{order_code}-{positionid}",
"product": "{product_id}",
"variation": "{variation_id}",
"name": "{attendee_name}"
}
}
Variables can only be used in strings inside the JSON structure.
pretix will automatically add an ``iat`` claim with the current timestamp and an ``exp`` claim with an expiration timestamp
based on your configuration.
List of variables
"""""""""""""""""
The following variables are currently supported:
.. rst-class:: rest-resource-table
=================================== ====================================================================
Variable Description
=================================== ====================================================================
``order_code`` Order code (alphanumerical, unique per order, not per ticket)
``positionid`` ID of the ticket within the order (integer, starting at 1)
``order_email`` E-mail address of the ticket purchaser
``product_id`` Internal ID of the purchased product
``product_variation`` Internal ID of the purchased product variation (or empty)
``attendee_name`` Full name of the ticket holder (or empty)
``attendee_name_*`` Name parts of the ticket holder, depending on configuration, e.g. ``attendee_name_given_name`` or ``attendee_name_family_name``
``attendee_email`` E-mail address of the ticket holder (or empty)
``attendee_company`` Company of the ticket holder (or empty)
``attendee_street`` Street of the ticket holder's address (or empty)
``attendee_zipcode`` ZIP code of the ticket holder's address (or empty)
``attendee_city`` City of the ticket holder's address (or empty)
``attendee_country`` Country code of the ticket holder's address (or empty)
``attendee_state`` State of the ticket holder's address (or empty)
``answer[XYZ]`` Answer to the custom question with identifier ``XYZ``
``invoice_name`` Full name of the invoice address (or empty)
``invoice_name_*`` Name parts of the invoice address, depending on configuration, e.g. ``invoice_name_given_name`` or ``invoice_name_family_name``
``invoice_company`` Company of the invoice address (or empty)
``invoice_street`` Street of the invoice address (or empty)
``invoice_zipcode`` ZIP code of the invoice address (or empty)
``invoice_city`` City of the invoice address (or empty)
``invoice_country`` Country code of the invoice address (or empty)
``invoice_state`` State of the invoice address (or empty)
``meta_XYZ`` Value of the event's ``XYZ`` meta property
``token`` Signed JWT (only to be used in URLs, not in tokens)
=================================== ====================================================================
API Resource description
-------------------------
The digital content plugin provides a HTTP API that allows you to create new digital content for your ticket holders,
such as live streams, videos, or material downloads.
Resource description
--------------------
The digital content resource contains the following public fields:
.. rst-class:: rest-resource-table
@@ -28,10 +102,13 @@ all_products boolean If ``true``, th
limit_products list of integers List of product/item IDs. This content is only shown to buyers of these ticket types.
position integer An integer, used for sorting
subevent integer Date in an event series this content should be shown for. Should be ``null`` if this is not an event series or if this should be shown to all customers.
jwt_template string Template for JWT token generation
jwt_secret string Secret for JWT token generation
jwt_validity integer JWT validity in days
===================================== ========================== =======================================================
Endpoints
---------
API Endpoints
-------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
@@ -275,3 +352,5 @@ Endpoints
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it
.. _JSON Web Token: https://en.wikipedia.org/wiki/JSON_Web_Token

View File

@@ -16,3 +16,4 @@ If you want to **create** a plugin, please go to the
badges
campaigns
digital
webinar

43
doc/plugins/webinar.rst Normal file
View File

@@ -0,0 +1,43 @@
pretix Webinar
==============
Fetch host URLs
---------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/webinars/
Returns a list of all currently available webinar calls configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/webinars/ 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
[
{
"name": "Webinar B Sept. 8th, 2020",
"hosturl": "http://pretix.eu/demo/museum/webinar/host/a9aded3d7bd4df60/30611a34f9fee5d3/"
},
{
"name": "Webinar A Sept. 8, 2020",
"hosturl": "http://pretix.eu/demo/museum/webinar/host/e714x7d4a4a36a04/b9cc444665xxx757/"
}
]
:query subevent: Limit the result to the webinar(s) for a specific subevent.
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.

View File

@@ -47,6 +47,7 @@ gunicorn
guid
hardcoded
hostname
ics
idempotency
iframe
incrementing
@@ -54,6 +55,8 @@ inofficial
invalidations
iterable
Jimdo
jwt
JWT
libpretixprint
libsass
linters

View File

@@ -26,6 +26,9 @@ Sender address
we strongly recommend to use the SMTP settings below as well, otherwise your e-mails might be detected as spam
due to the `Sender Policy Framework`_ and similar mechanisms.
Sender name
This is the name associated with the sender address. By default, this is your event name.
Signature
This text will be appended to all e-mails in form of a signature. This might be useful e.g. to add your contact
details or any legal information that needs to be included with the e-mails.
@@ -33,6 +36,15 @@ Signature
Bcc address
This email address will receive a copy of every event-related email.
Attach calendar files
With this option, every order confirmation mail will include an ics file with name, date and location of
your event. It can be imported into many digital calendars.
Sales Channels for Checkout Emails
When you are using multiple sales channel, you may want to decide that mails for order and payment confirmation
are only to be sent for some sales channels. For orders created through the default online shop, these emails
must always be send. A similar option is available for ticket download reminders.
E-mail design
-------------

View File

@@ -292,6 +292,8 @@ Flexible group sizes
If you want to give out discounted tickets to groups starting at a given size, but still billed per person, you can do so by creating a special **Group ticket** at the per-person price and set the **Minimum amount per order** option of the ticket to the minimal group size.
For more complex use cases, you can also use add-on products that can be chosen multiple times.
This way, your ticket can be bought an arbitrary number of times but no less than the given minimal amount per order.
Fixed group sizes

View File

@@ -3,6 +3,7 @@ include README.rst
recursive-include pretix/static *
recursive-include pretix/static.dist *
recursive-include pretix/locale *
recursive-include pretix/helpers/locale *
recursive-include pretix/base/templates *
recursive-include pretix/control/templates *
recursive-include pretix/presale/templates *

View File

@@ -1 +1 @@
__version__ = "3.9.0"
__version__ = "3.12.0.dev0"

View File

@@ -3,6 +3,9 @@ from django_scopes import scopes_disabled
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
from pretix.api.auth.devicesecurity import (
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
)
from pretix.base.models import Device
@@ -25,3 +28,11 @@ class DeviceTokenAuthentication(TokenAuthentication):
raise exceptions.AuthenticationFailed('Device access has been revoked.')
return AnonymousUser(), device
def authenticate(self, request):
r = super().authenticate(request)
if r and isinstance(r[1], Device):
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
if not profile.is_allowed(request):
raise exceptions.PermissionDenied('Request denied by device security profile.')
return r

View File

@@ -0,0 +1,112 @@
from django.utils.translation import ugettext_lazy as _
class FullAccessSecurityProfile:
identifier = 'full'
verbose_name = _('Full access')
def is_allowed(self, request):
return True
class AllowListSecurityProfile:
allowlist = tuple()
def is_allowed(self, request):
key = (request.method, f"{request.resolver_match.namespace}:{request.resolver_match.url_name}")
return key in self.allowlist
class PretixScanSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixscan'
verbose_name = _('pretixSCAN')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.update'),
('GET', 'api-v1:device.revoke'),
('GET', 'api-v1:event-list'),
('GET', 'api-v1:event-detail'),
('GET', 'api-v1:subevent-list'),
('GET', 'api-v1:subevent-detail'),
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:checkinlist-list'),
('GET', 'api-v1:checkinlist-status'),
('GET', 'api-v1:checkinlistpos-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:order-list'),
('GET', 'api-v1:event.settings'),
)
class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixscan_online_kiosk'
verbose_name = _('pretixSCAN (kiosk mode, online only)')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.update'),
('GET', 'api-v1:device.revoke'),
('GET', 'api-v1:event-list'),
('GET', 'api-v1:event-detail'),
('GET', 'api-v1:subevent-list'),
('GET', 'api-v1:subevent-detail'),
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:checkinlist-list'),
('GET', 'api-v1:checkinlist-status'),
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:event.settings'),
)
class PretixPosSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixpos'
verbose_name = _('pretixPOS')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.update'),
('GET', 'api-v1:device.revoke'),
('GET', 'api-v1:event-list'),
('GET', 'api-v1:event-detail'),
('GET', 'api-v1:subevent-list'),
('GET', 'api-v1:subevent-detail'),
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:quota-list'),
('GET', 'api-v1:taxrule-list'),
('GET', 'api-v1:ticketlayout-list'),
('GET', 'api-v1:ticketlayoutitem-list'),
('GET', 'api-v1:order-list'),
('POST', 'api-v1:order-list'),
('GET', 'api-v1:order-detail'),
('DELETE', 'api-v1:orderposition-detail'),
('POST', 'api-v1:order-mark_canceled'),
('POST', 'api-v1:orderrefund-list'),
('POST', 'api-v1:orderrefund-done'),
('POST', 'api-v1:cartposition-list'),
('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
('GET', 'api-v1:event.settings'),
)
DEVICE_SECURITY_PROFILES = {
k.identifier: k() for k in (
FullAccessSecurityProfile,
PretixScanSecurityProfile,
PretixScanNoSyncSecurityProfile,
PretixPosSecurityProfile,
)
}

View File

@@ -84,3 +84,15 @@ class EventCRUDPermission(EventPermission):
return False
return True
class ProfilePermission(BasePermission):
def has_permission(self, request, view):
if not request.user.is_authenticated:
return False
if isinstance(request.auth, OAuthAccessToken):
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
return False
return True

View File

@@ -9,7 +9,7 @@ from oauth2_provider.settings import oauth2_settings
class Validator(OAuth2Validator):
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
if not getattr(request, 'organizers', None):
if not getattr(request, 'organizers', None) and request.scopes != ['profile']:
raise FatalClientError('No organizers selected.')
expires = timezone.now() + timedelta(
@@ -18,7 +18,8 @@ class Validator(OAuth2Validator):
expires=expires, redirect_uri=request.redirect_uri,
scope=" ".join(request.scopes))
g.save()
g.organizers.add(*request.organizers.all())
if request.scopes != ['profile']:
g.organizers.add(*request.organizers.all())
def validate_code(self, client_id, code, client, request, *args, **kwargs):
try:
@@ -34,12 +35,14 @@ class Validator(OAuth2Validator):
return False
def _create_access_token(self, expires, request, token, source_refresh_token=None):
if not getattr(request, 'organizers', None) and not getattr(source_refresh_token, 'access_token'):
if not getattr(request, 'organizers', None) and not getattr(source_refresh_token, 'access_token', None) and token["scope"] != 'profile':
raise FatalClientError('No organizers selected.')
if hasattr(request, 'organizers'):
orgs = list(request.organizers.all())
else:
orgs = list(source_refresh_token.access_token.organizers.all())
if token['scope'] != 'profile':
if hasattr(request, 'organizers'):
orgs = list(request.organizers.all())
else:
orgs = list(source_refresh_token.access_token.organizers.all())
access_token = super()._create_access_token(expires, request, token, source_refresh_token=None)
access_token.organizers.add(*orgs)
if token['scope'] != 'profile':
access_token.organizers.add(*orgs)
return access_token

View File

@@ -17,6 +17,17 @@ class CheckinListSerializer(I18nAwareModelSerializer):
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
'rules')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for exclude_field in self.context['request'].query_params.getlist('exclude'):
p = exclude_field.split('.')
if p[0] in self.fields:
if len(p) == 1:
del self.fields[p[0]]
elif len(p) == 2:
self.fields[p[0]].child.fields.pop(p[1])
def validate(self, data):
data = super().validate(data)
event = self.context['event']

View File

@@ -29,6 +29,9 @@ class MetaDataField(Field):
}
def to_internal_value(self, data):
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data.keys()):
raise ValidationError('meta_data needs to be an object (str -> str).')
return {
'meta_data': data
}
@@ -42,6 +45,8 @@ class MetaPropertyField(Field):
}
def to_internal_value(self, data):
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data.keys()) or not all(isinstance(k, str) for k in data.values()):
raise ValidationError('item_meta_properties needs to be an object (str -> str).')
return {
'item_meta_properties': data
}
@@ -58,6 +63,8 @@ class SeatCategoryMappingField(Field):
}
def to_internal_value(self, data):
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data.keys()) or not all(isinstance(k, int) for k in data.values()):
raise ValidationError('seat_category_mapping needs to be an object (str -> int).')
return {
'seat_category_mapping': data or {}
}
@@ -341,13 +348,13 @@ class CloneEventSerializer(EventSerializer):
class SubEventItemSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItem
fields = ('item', 'price')
fields = ('item', 'price', 'disabled')
class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItemVariation
fields = ('variation', 'price')
fields = ('variation', 'price', 'disabled')
class SubEventSerializer(I18nAwareModelSerializer):
@@ -452,27 +459,29 @@ class SubEventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def update(self, instance, validated_data):
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
item_price_overrides_data = validated_data.pop('subeventitem_set', None)
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set', None)
meta_data = validated_data.pop('meta_data', None)
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
subevent = super().update(instance, validated_data)
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
if item_price_overrides_data is not None:
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
for item_price_override_data in item_price_overrides_data:
id = existing_item_overrides.pop(item_price_override_data['item'], None)
SubEventItem(id=id, subevent=subevent, **item_price_override_data).save()
for item_price_override_data in item_price_overrides_data:
id = existing_item_overrides.pop(item_price_override_data['item'], None)
SubEventItem(id=id, subevent=subevent, **item_price_override_data).save()
SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete()
SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete()
existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)}
if variation_price_overrides_data is not None:
existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)}
for variation_price_override_data in variation_price_overrides_data:
id = existing_variation_overrides.pop(variation_price_override_data['variation'], None)
SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save()
for variation_price_override_data in variation_price_overrides_data:
id = existing_variation_overrides.pop(variation_price_override_data['variation'], None)
SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save()
SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete()
SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete()
# Meta data
if meta_data is not None:
@@ -565,11 +574,13 @@ class EventSettingsSerializer(serializers.Serializer):
'attendee_addresses_required',
'attendee_company_asked',
'attendee_company_required',
'confirm_text',
'confirm_texts',
'order_email_asked_twice',
'payment_term_mode',
'payment_term_days',
'payment_term_last',
'payment_term_weekdays',
'payment_term_minutes',
'payment_term_last',
'payment_term_expire_automatically',
'payment_term_accept_late',
'payment_explanation',
@@ -597,6 +608,7 @@ class EventSettingsSerializer(serializers.Serializer):
'invoice_numbers_consecutive',
'invoice_numbers_prefix',
'invoice_numbers_prefix_cancellations',
'invoice_numbers_counter_length',
'invoice_attendee_name',
'invoice_include_expire_date',
'invoice_address_explanation_text',
@@ -611,6 +623,7 @@ class EventSettingsSerializer(serializers.Serializer):
'invoice_introductory_text',
'invoice_additional_text',
'invoice_footer_text',
'invoice_eu_currencies',
'cancel_allow_user',
'cancel_allow_user_until',
'cancel_allow_user_paid',
@@ -622,6 +635,9 @@ class EventSettingsSerializer(serializers.Serializer):
'cancel_allow_user_paid_adjust_fees_explanation',
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'change_allow_user_variation',
'change_allow_user_until',
'change_allow_user_price',
]
def __init__(self, *args, **kwargs):
@@ -629,9 +645,13 @@ class EventSettingsSerializer(serializers.Serializer):
super().__init__(*args, **kwargs)
for fname in self.default_fields:
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
if callable(kwargs):
kwargs = kwargs()
kwargs.setdefault('required', False)
kwargs.setdefault('allow_null', True)
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
if callable(form_kwargs):
form_kwargs = form_kwargs()
if 'serializer_class' not in DEFAULTS[fname]:
raise ValidationError('{} has no serializer class'.format(fname))
f = DEFAULTS[fname]['serializer_class'](
@@ -660,3 +680,40 @@ class EventSettingsSerializer(serializers.Serializer):
settings_dict.update(data)
validate_settings(self.event, settings_dict)
return data
class DeviceEventSettingsSerializer(EventSettingsSerializer):
default_fields = [
'locales',
'locale',
'last_order_modification_date',
'show_quota_left',
'max_items_per_order',
'attendee_names_asked',
'attendee_names_required',
'attendee_emails_asked',
'attendee_emails_required',
'attendee_addresses_asked',
'attendee_addresses_required',
'attendee_company_asked',
'attendee_company_required',
'ticket_download',
'ticket_download_addons',
'ticket_download_nonadm',
'ticket_download_pending',
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_address_from_name',
'invoice_address_from',
'invoice_address_from_zipcode',
'invoice_address_from_city',
'invoice_address_from_country',
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
]

View File

@@ -45,7 +45,7 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
fields = ('addon_category', 'min_count', 'max_count',
'position', 'price_included')
'position', 'price_included', 'multi_allowed')
class ItemBundleSerializer(serializers.ModelSerializer):
@@ -77,7 +77,7 @@ class ItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
fields = ('id', 'addon_category', 'min_count', 'max_count',
'position', 'price_included')
'position', 'price_included', 'multi_allowed')
def validate(self, data):
data = super().validate(data)
@@ -349,7 +349,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out')
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out', 'release_after_exit')
def validate(self, data):
data = super().validate(data)

View File

@@ -68,7 +68,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country')):
if not pycountry.countries.get(alpha_2=data.get('country').code):
raise ValidationError(
{'country': ['Invalid country code.']}
)
@@ -122,7 +122,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('datetime', 'list', 'auto_checked_in', 'type')
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'type')
class OrderDownloadsField(serializers.Field):
@@ -270,8 +270,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'require_attention',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
'order__status')
@@ -376,6 +377,14 @@ class OrderSerializer(I18nAwareModelSerializer):
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
self.fields['positions'].child.fields.pop('pdf_data')
for exclude_field in self.context['request'].query_params.getlist('exclude'):
p = exclude_field.split('.')
if p[0] in self.fields:
if len(p) == 1:
del self.fields[p[0]]
elif len(p) == 2:
self.fields[p[0]].child.fields.pop(p[1])
def validate_locale(self, l):
if l not in set(k for k in self.instance.event.settings.locales):
raise ValidationError('"{}" is not a supported locale for this event.'.format(l))
@@ -416,16 +425,26 @@ class OrderSerializer(I18nAwareModelSerializer):
return instance
class SimulatedOrderPositionSerializer(OrderPositionSerializer):
addon_to = serializers.SlugRelatedField(read_only=True, slug_field='positionid')
class SimulatedOrderSerializer(OrderSerializer):
positions = SimulatedOrderPositionSerializer(many=True, read_only=True)
class PriceCalcSerializer(serializers.Serializer):
item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
tax_rule = serializers.PrimaryKeyRelatedField(queryset=TaxRule.objects.none(), required=False, allow_null=True)
locale = serializers.CharField(allow_null=True, required=False)
def __init__(self, *args, **kwargs):
event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['item'].queryset = event.items.all()
self.fields['tax_rule'].queryset = event.tax_rules.all()
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=event)
if event.has_subevents:
self.fields['subevent'].queryset = event.subevents.all()
@@ -590,7 +609,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country')):
if not pycountry.countries.get(alpha_2=data.get('country').code):
raise ValidationError(
{'country': ['Invalid country code.']}
)
@@ -903,6 +922,19 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
continue
if pos_data.get('subevent'):
if pos_data.get('item').pk in pos_data['subevent'].item_overrides and pos_data['subevent'].item_overrides[pos_data['item'].pk].disabled:
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
if (
pos_data.get('variation') and pos_data['variation'].pk in pos_data['subevent'].var_overrides and
pos_data['subevent'].var_overrides[pos_data['variation'].pk].disabled
):
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
@@ -963,7 +995,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else:
pos.order = order
if addon_to:
pos.addon_to = pos_map[addon_to]
if simulate:
pos.addon_to = pos_map[addon_to]._wrapped
else:
pos.addon_to = pos_map[addon_to]
if pos.price is None:
price = get_price(

View File

@@ -9,7 +9,8 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.base.auth import get_auth_backends
from pretix.base.models import (
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail
@@ -66,9 +67,6 @@ class EventSlugField(serializers.SlugRelatedField):
class TeamSerializer(serializers.ModelSerializer):
limit_events = EventSlugField(slug_field='slug', many=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Meta:
model = Team
fields = (
@@ -86,6 +84,28 @@ class TeamSerializer(serializers.ModelSerializer):
return data
class DeviceSerializer(serializers.ModelSerializer):
limit_events = EventSlugField(slug_field='slug', many=True)
device_id = serializers.IntegerField(read_only=True)
unique_serial = serializers.CharField(read_only=True)
hardware_brand = serializers.CharField(read_only=True)
hardware_model = 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)
revoked = serializers.BooleanField(read_only=True)
initialized = serializers.DateTimeField(read_only=True)
initialization_token = serializers.DateTimeField(read_only=True)
class Meta:
model = Device
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'
)
class TeamInviteSerializer(serializers.ModelSerializer):
class Meta:
model = TeamInvite

View File

@@ -6,6 +6,7 @@ from django_scopes import scopes_disabled
from pretix.api.models import ApiCall, WebHookCall
from pretix.base.signals import periodic_task
from pretix.helpers.periodic import minimum_interval
register_webhook_events = Signal(
providing_args=[]
@@ -19,11 +20,13 @@ instances.
@receiver(periodic_task)
@scopes_disabled()
@minimum_interval(minutes_after_success=12 * 60)
def cleanup_webhook_logs(sender, **kwargs):
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
@receiver(periodic_task)
@scopes_disabled()
@minimum_interval(minutes_after_success=12 * 60)
def cleanup_api_logs(sender, **kwargs):
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()

View File

@@ -21,6 +21,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
orga_router.register(r'teams', organizer.TeamViewSet)
orga_router.register(r'devices', organizer.DeviceViewSet)
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
@@ -44,7 +45,7 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
event_router.register(r'cartpositions', cart.CartPositionViewSet)
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
question_router = routers.DefaultRouter()
question_router.register(r'options', item.QuestionOptionViewSet)

View File

@@ -1,3 +1,4 @@
import django_filters
from django.core.exceptions import ValidationError
from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery
from django.db.models.functions import Coalesce
@@ -17,6 +18,7 @@ from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import CheckinListOrderPositionSerializer
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, CheckinList, Event, Order, OrderPosition,
)
@@ -27,10 +29,17 @@ from pretix.helpers.database import FixedOrderBy
with scopes_disabled():
class CheckinListFilter(FilterSet):
subevent_match = django_filters.NumberFilter(method='subevent_match_qs')
class Meta:
model = CheckinList
fields = ['subevent']
def subevent_match_qs(self, qs, name, value):
return qs.filter(
Q(subevent_id=value) | Q(subevent_id__isnull=True)
)
class CheckinListViewSet(viewsets.ModelViewSet):
serializer_class = CheckinListSerializer
@@ -79,73 +88,74 @@ class CheckinListViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['GET'])
def status(self, *args, **kwargs):
clist = self.get_object()
cqs = Checkin.objects.filter(
position__order__event=clist.event,
position__order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
list=clist
)
pqs = OrderPosition.objects.filter(
order__event=clist.event,
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
)
if clist.subevent:
pqs = pqs.filter(subevent=clist.subevent)
if not clist.all_products:
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
with language(self.request.event.settings.locale):
clist = self.get_object()
cqs = Checkin.objects.filter(
position__order__event=clist.event,
position__order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
list=clist
)
pqs = OrderPosition.objects.filter(
order__event=clist.event,
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
)
if clist.subevent:
pqs = pqs.filter(subevent=clist.subevent)
if not clist.all_products:
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
ev = clist.subevent or clist.event
response = {
'event': {
'name': str(ev.name),
},
'checkin_count': cqs.count(),
'position_count': pqs.count()
}
op_by_item = {
p['item']: p['cnt']
for p in pqs.order_by().values('item').annotate(cnt=Count('id'))
}
op_by_variation = {
p['variation']: p['cnt']
for p in pqs.order_by().values('variation').annotate(cnt=Count('id'))
}
c_by_item = {
p['position__item']: p['cnt']
for p in cqs.order_by().values('position__item').annotate(cnt=Count('id'))
}
c_by_variation = {
p['position__variation']: p['cnt']
for p in cqs.order_by().values('position__variation').annotate(cnt=Count('id'))
}
if not clist.all_products:
items = clist.limit_products
else:
items = clist.event.items
response['items'] = []
for item in items.order_by('category__position', 'position', 'pk').prefetch_related('variations'):
i = {
'id': item.pk,
'name': str(item),
'admission': item.admission,
'checkin_count': c_by_item.get(item.pk, 0),
'position_count': op_by_item.get(item.pk, 0),
'variations': []
ev = clist.subevent or clist.event
response = {
'event': {
'name': str(ev.name),
},
'checkin_count': cqs.count(),
'position_count': pqs.count()
}
for var in item.variations.all():
i['variations'].append({
'id': var.pk,
'value': str(var),
'checkin_count': c_by_variation.get(var.pk, 0),
'position_count': op_by_variation.get(var.pk, 0),
})
response['items'].append(i)
return Response(response)
op_by_item = {
p['item']: p['cnt']
for p in pqs.order_by().values('item').annotate(cnt=Count('id'))
}
op_by_variation = {
p['variation']: p['cnt']
for p in pqs.order_by().values('variation').annotate(cnt=Count('id'))
}
c_by_item = {
p['position__item']: p['cnt']
for p in cqs.order_by().values('position__item').annotate(cnt=Count('id'))
}
c_by_variation = {
p['position__variation']: p['cnt']
for p in cqs.order_by().values('position__variation').annotate(cnt=Count('id'))
}
if not clist.all_products:
items = clist.limit_products
else:
items = clist.event.items
response['items'] = []
for item in items.order_by('category__position', 'position', 'pk').prefetch_related('variations'):
i = {
'id': item.pk,
'name': str(item),
'admission': item.admission,
'checkin_count': c_by_item.get(item.pk, 0),
'position_count': op_by_item.get(item.pk, 0),
'variations': []
}
for var in item.variations.all():
i['variations'].append({
'id': var.pk,
'value': str(var),
'checkin_count': c_by_variation.get(var.pk, 0),
'position_count': op_by_variation.get(var.pk, 0),
})
response['items'].append(i)
return Response(response)
with scopes_disabled():
@@ -192,7 +202,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
except ValueError:
raise Http404()
def get_queryset(self, ignore_status=False):
def get_queryset(self, ignore_status=False, ignore_products=False):
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.checkinlist.pk
@@ -247,12 +257,12 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
if not self.checkinlist.all_products:
if not self.checkinlist.all_products and not ignore_products:
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
return qs
@action(detail=True, methods=['POST'])
@action(detail=False, methods=['POST'], url_name='redeem', url_path='(?P<pk>[^/]+)/redeem')
def redeem(self, *args, **kwargs):
force = bool(self.request.data.get('force', False))
type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
@@ -260,13 +270,27 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
raise ValidationError("Invalid check-in type.")
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
nonce = self.request.data.get('nonce')
op = self.get_object(ignore_status=True)
if 'datetime' in self.request.data:
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
else:
dt = now()
try:
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
if self.kwargs['pk'].isnumeric():
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:
op = queryset.get(secret=self.kwargs['pk'])
except OrderPosition.DoesNotExist:
self.request.event.log_action('pretix.event.checkin.unknown', data={
'datetime': dt,
'type': type,
'list': self.checkinlist.pk,
'barcode': self.kwargs['pk']
}, user=self.request.user, auth=self.request.auth)
raise Http404()
given_answers = {}
if 'answers' in self.request.data:
aws = self.request.data.get('answers')
@@ -302,6 +326,14 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
]
}, status=400)
except CheckInError as e:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': e.code,
'datetime': dt,
'type': type,
'list': self.checkinlist.pk
}, user=self.request.user, auth=self.request.auth)
return Response({
'status': 'error',
'reason': e.code,
@@ -314,11 +346,3 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def get_object(self, ignore_status=False):
queryset = self.filter_queryset(self.get_queryset(ignore_status=ignore_status))
if self.kwargs['pk'].isnumeric():
obj = get_object_or_404(queryset, Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:
obj = get_object_or_404(queryset, secret=self.kwargs['pk'])
return obj

View File

@@ -35,7 +35,7 @@ class DeviceSerializer(serializers.ModelSerializer):
model = Device
fields = [
'organizer', 'device_id', 'unique_serial', 'api_token',
'name'
'name', 'security_profile'
]

View File

@@ -10,12 +10,12 @@ from rest_framework.response import Response
from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.serializers.event import (
CloneEventSerializer, EventSerializer, EventSettingsSerializer,
SubEventSerializer, TaxRuleSerializer,
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
CartPosition, Device, Event, ItemCategory, TaxRule, TeamAPIToken,
CartPosition, Device, Event, TaxRule, TeamAPIToken,
)
from pretix.base.models.event import SubEvent
from pretix.helpers.dicts import merge_dicts
@@ -73,6 +73,7 @@ class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.none()
lookup_field = 'slug'
lookup_url_kwarg = 'event'
lookup_value_regex = '[^/]+'
permission_classes = (EventCRUDPermission,)
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
ordering = ('slug',)
@@ -228,7 +229,7 @@ with scopes_disabled():
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SubEventSerializer
queryset = ItemCategory.objects.none()
queryset = SubEvent.objects.none()
write_permission = 'can_change_event_settings'
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filterset_class = SubEventFilter
@@ -337,10 +338,16 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
class EventSettingsView(views.APIView):
permission = 'can_change_event_settings'
permission = None
write_permission = 'can_change_event_settings'
def get(self, request, *args, **kwargs):
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
if isinstance(request.auth, Device):
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event)
elif 'can_change_event_settings' in request.eventpermset:
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
else:
raise PermissionDenied()
if 'explain' in request.GET:
return Response({
fname: {

View File

@@ -542,6 +542,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
data = {
'paid_orders': qa.count_paid_orders[quota],
'pending_orders': qa.count_pending_orders[quota],
'exited_orders': qa.count_exited_orders[quota],
'blocking_vouchers': qa.count_vouchers[quota],
'cart_positions': qa.count_cart[quota],
'waiting_list': qa.count_pending_orders[quota],

View File

@@ -3,8 +3,9 @@ import logging
from django import forms
from django.conf import settings
from django.utils.translation import gettext as _
from oauth2_provider.exceptions import OAuthToolkitError
from oauth2_provider.exceptions import FatalClientError, OAuthToolkitError
from oauth2_provider.forms import AllowForm
from oauth2_provider.settings import oauth2_settings
from oauth2_provider.views import (
AuthorizationView as BaseAuthorizationView,
RevokeTokenView as BaseRevokeTokenView, TokenView as BaseTokenView,
@@ -24,9 +25,12 @@ class OAuthAllowForm(AllowForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
scope = kwargs.pop('scope')
super().__init__(*args, **kwargs)
self.fields['organizers'].queryset = Organizer.objects.filter(
pk__in=user.teams.values_list('organizer', flat=True))
if scope == 'profile':
del self.fields['organizers']
class AuthorizationView(BaseAuthorizationView):
@@ -36,6 +40,7 @@ class AuthorizationView(BaseAuthorizationView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
kwargs['scope'] = self.request.GET.get('scope')
return kwargs
def get_context_data(self, **kwargs):
@@ -43,8 +48,14 @@ class AuthorizationView(BaseAuthorizationView):
ctx['settings'] = settings
return ctx
def create_authorization_response(self, request, scopes, credentials, allow, organizers):
credentials["organizers"] = organizers
def validate_authorization_request(self, request):
require_approval = request.GET.get("approval_prompt", oauth2_settings.REQUEST_APPROVAL_PROMPT)
if require_approval != 'force' and request.GET.get('scope') != 'profile':
raise FatalClientError('Combnination of require_approval and scope values not allowed.')
return super().validate_authorization_request(request)
def create_authorization_response(self, request, scopes, credentials, allow, organizers=None):
credentials["organizers"] = organizers or []
return super().create_authorization_response(request, scopes, credentials, allow)
def form_valid(self, form):

View File

@@ -4,7 +4,7 @@ from decimal import Decimal
import django_filters
import pytz
from django.db import transaction
from django.db.models import F, Prefetch, Q
from django.db.models import Exists, F, OuterRef, Prefetch, Q
from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404
@@ -26,12 +26,12 @@ from pretix.api.serializers.order import (
InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer,
OrderPaymentSerializer, OrderPositionSerializer,
OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer,
PriceCalcSerializer,
PriceCalcSerializer, SimulatedOrderSerializer,
)
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota,
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
TeamAPIToken, generate_position_secret, generate_secret,
)
from pretix.base.payment import PaymentException
@@ -52,6 +52,7 @@ from pretix.base.signals import (
order_modified, order_paid, order_placed, register_ticket_outputs,
)
from pretix.base.templatetags.money import money_filter
from pretix.control.signals import order_search_filter_q
with scopes_disabled():
class OrderFilter(FilterSet):
@@ -60,11 +61,62 @@ with scopes_disabled():
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
search = django_filters.CharFilter(method='search_qs')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
def subevent_after_qs(self, qs, name, value):
qs = qs.annotate(
has_se_after=Exists(
OrderPosition.all.filter(
subevent_id__in=SubEvent.objects.filter(
Q(date_to__gt=value) | Q(date_from__gt=value, date_to__isnull=True), event=OuterRef(OuterRef('event_id'))
).values_list('id'),
order_id=OuterRef('pk'),
)
)
).filter(has_se_after=True)
return qs
def search_qs(self, qs, name, value):
u = value
if "-" in value:
code = (Q(event__slug__icontains=u.rsplit("-", 1)[0])
& Q(code__icontains=Order.normalize_code(u.rsplit("-", 1)[1])))
else:
code = Q(code__icontains=Order.normalize_code(u))
matching_invoices = Invoice.objects.filter(
Q(invoice_no__iexact=u)
| Q(invoice_no__iexact=u.zfill(5))
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
matching_positions = OrderPosition.objects.filter(
Q(order=OuterRef('pk')) & Q(
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
| Q(secret__istartswith=u) | Q(voucher__code__icontains=u)
)
).values('id')
mainq = (
code
| Q(email__icontains=u)
| Q(invoice_address__name_cached__icontains=u)
| Q(invoice_address__company__icontains=u)
| Q(pk__in=matching_invoices)
| Q(comment__icontains=u)
| Q(has_pos=True)
)
for recv, q in order_search_filter_q.send(sender=getattr(self, 'event', None), query=u):
mainq = mainq | q
return qs.annotate(has_pos=Exists(matching_positions)).filter(
mainq
)
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
@@ -83,16 +135,19 @@ class OrderViewSet(viewsets.ModelViewSet):
return ctx
def get_queryset(self):
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
fqs = OrderFee.all
else:
fqs = OrderFee.objects
qs = self.request.event.orders.prefetch_related(
Prefetch('fees', queryset=fqs.all()),
'payments', 'refunds', 'refunds__payment'
).select_related(
'invoice_address'
)
qs = self.request.event.orders
if 'fees' not in self.request.GET.getlist('exclude'):
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
fqs = OrderFee.all
else:
fqs = OrderFee.objects
qs = qs.prefetch_related(Prefetch('fees', queryset=fqs.all()))
if 'payments' not in self.request.GET.getlist('exclude'):
qs = qs.prefetch_related('payments')
if 'refunds' not in self.request.GET.getlist('exclude'):
qs = qs.prefetch_related('refunds', 'refunds__payment')
if 'invoice_address' not in self.request.GET.getlist('exclude'):
qs = qs.select_related('invoice_address')
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
opq = OrderPosition.all
@@ -129,6 +184,7 @@ class OrderViewSet(viewsets.ModelViewSet):
return prov
raise NotFound('Unknown output provider.')
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
def list(self, request, **kwargs):
date = serializers.DateTimeField().to_representation(now())
queryset = self.filter_queryset(self.get_queryset())
@@ -172,6 +228,7 @@ class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
order = self.get_object()
send_mail = request.data.get('send_email', True)
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
@@ -213,6 +270,7 @@ class OrderViewSet(viewsets.ModelViewSet):
try:
p.confirm(auth=self.request.auth,
user=self.request.user if request.user.is_authenticated else None,
send_mail=send_mail,
count_waitinglist=False)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -488,10 +546,12 @@ class OrderViewSet(viewsets.ModelViewSet):
self.perform_create(serializer)
send_mail = serializer._send_mail
order = serializer.instance
serializer = OrderSerializer(order, context=serializer.context)
if not order.pk:
# Simulation
serializer = SimulatedOrderSerializer(order, context=serializer.context)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
serializer = OrderSerializer(order, context=serializer.context)
order.log_action(
'pretix.event.order.placed',
@@ -743,7 +803,8 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
{
"item": 2,
"variation": null,
"subevent": 3
"subevent": 3,
"tax_rule": 4,
}
Sample output:
@@ -797,7 +858,11 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
if data.get('subevent'):
kwargs['subevent'] = data.get('subevent')
if data.get('tax_rule'):
kwargs['tax_rule'] = data.get('tax_rule')
price = get_price(**kwargs)
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
with language(data.get('locale') or self.request.event.settings.locale):
return Response({
'gross': price.gross,
@@ -806,6 +871,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
'rate': price.rate,
'name': str(price.name),
'tax': price.tax,
'tax_rule': tr.pk if tr else None,
})
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
@@ -912,6 +978,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
def confirm(self, request, **kwargs):
payment = self.get_object()
force = request.data.get('force', False)
send_mail = request.data.get('send_email', True)
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
@@ -920,6 +987,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
payment.confirm(user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth,
count_waitinglist=False,
send_mail=send_mail,
force=force)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -6,20 +6,22 @@ from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import filters, serializers, status, viewsets
from rest_framework import filters, mixins, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import (
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
TeamAPITokenSerializer, TeamInviteSerializer, TeamMemberSerializer,
TeamSerializer,
DeviceSerializer, GiftCardSerializer, OrganizerSerializer,
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
TeamMemberSerializer, TeamSerializer,
)
from pretix.base.models import (
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User,
)
from pretix.helpers.dicts import merge_dicts
@@ -29,6 +31,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Organizer.objects.none()
lookup_field = 'slug'
lookup_url_kwarg = 'organizer'
lookup_value_regex = '[^/]+'
filter_backends = (filters.OrderingFilter,)
ordering = ('slug',)
ordering_fields = ('name', 'slug')
@@ -352,3 +355,44 @@ class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
serializer = self.get_serializer_class()(instance)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)
class DeviceViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
GenericViewSet):
serializer_class = DeviceSerializer
queryset = Device.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
lookup_field = 'device_id'
def get_queryset(self):
return self.request.organizer.devices.order_by('pk')
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
inst.log_action(
'pretix.device.created',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save()
inst.log_action(
'pretix.device.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
return inst

View File

@@ -3,14 +3,18 @@ from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from rest_framework.views import APIView
from pretix.api.auth.permission import ProfilePermission
class MeView(APIView):
authentication_classes = (SessionAuthentication, OAuth2Authentication)
permission_classes = (ProfilePermission,)
def get(self, request, format=None):
return Response({
'email': request.user.email,
'fullname': request.user.fullname,
'locale': request.user.locale,
'is_staff': request.user.is_staff,
'timezone': request.user.timezone
})

View File

@@ -85,6 +85,8 @@ class ParametrizedOrderWebhookEvent(WebhookEvent):
def build_payload(self, logentry: LogEntry):
order = logentry.content_object
if not order:
return None
return {
'notification_id': logentry.pk,
@@ -99,6 +101,8 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry):
d = super().build_payload(logentry)
if d is None:
return None
d['orderposition_id'] = logentry.parsed_data.get('position')
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
d['checkin_list'] = logentry.parsed_data.get('list')
@@ -218,6 +222,10 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
return # Ignore, e.g. plugin not installed
payload = event_type.build_payload(logentry)
if payload is None:
# Content object deleted?
return
t = time.time()
try:

View File

@@ -98,7 +98,10 @@ class BaseAuthBackend:
class NativeAuthBackend(BaseAuthBackend):
identifier = 'native'
verbose_name = _('pretix User')
@property
def verbose_name(self):
return _('{system} User').format(system=settings.PRETIX_INSTANCE_NAME)
@property
def login_form_fields(self) -> dict:

View File

@@ -12,7 +12,9 @@ from django.utils.timezone import now
from django.utils.translation import get_language, gettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.models import Event
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
@@ -112,7 +114,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
'site_url': settings.SITE_URL,
'body': body_md,
'subject': str(subject),
'color': '#8E44B3',
'color': settings.PRETIX_PRIMARY_COLOR,
'rtl': get_language() in settings.LANGUAGES_RTL
}
if self.event:
@@ -220,6 +222,7 @@ class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
base_parameters.append('position_or_address')
params = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
@@ -238,7 +241,9 @@ def get_email_context(**kwargs):
try:
kwargs['invoice_address'] = kwargs['order'].invoice_address
except InvoiceAddress.DoesNotExist:
kwargs['invoice_address'] = InvoiceAddress()
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
finally:
kwargs.setdefault("position_or_address", kwargs['invoice_address'])
ctx = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
@@ -258,9 +263,31 @@ def _placeholder_payment(order, payment):
return str(payment.payment_provider.order_pending_mail_render(order))
def get_best_name(position_or_address, parts=False):
"""
Return the best name we got for either an invoice address or an order position, falling back to the respective other
"""
from pretix.base.models import InvoiceAddress, OrderPosition
if isinstance(position_or_address, InvoiceAddress):
if position_or_address.name:
return position_or_address.name_parts if parts else position_or_address.name
elif position_or_address.order:
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
if isinstance(position_or_address, OrderPosition):
if position_or_address.attendee_name:
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
elif position_or_address.order:
try:
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return {} if parts else ""
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.base.models import InvoiceAddress
from pretix.multidomain.urlreverse import build_absolute_uri
ph = [
@@ -294,9 +321,8 @@ def base_placeholders(sender, **kwargs):
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyDate(order.expires.astimezone(event.timezone)),
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
# TODO: This used to be "date" in some placeholders, add a migration!
),
SimpleFunctionalMailTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
@@ -315,6 +341,51 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
@@ -429,11 +500,7 @@ def base_placeholders(sender, **kwargs):
),
SimpleFunctionalMailTextPlaceholder(
'name', ['position_or_address'],
lambda position_or_address: (
position_or_address.name
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name
),
get_best_name,
_('John Doe'),
),
]
@@ -448,11 +515,7 @@ def base_placeholders(sender, **kwargs):
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: (
position_or_address.name_parts.get(f, '')
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name_parts.get(f, '')
),
lambda position_or_address, f=f: get_best_name(position_or_address, parts=True).get(f, ''),
name_scheme['sample'][f]
))

View File

@@ -1,24 +1,34 @@
import io
import tempfile
from collections import OrderedDict
from collections import OrderedDict, namedtuple
from decimal import Decimal
from typing import Tuple
from defusedcsv import csv
from django import forms
from django.db.models import QuerySet
from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES
from pretix.base.models import Event
class BaseExporter:
"""
This is the base class for all data exporters
"""
def __init__(self, event):
def __init__(self, event, progress_callback=lambda v: None):
self.event = event
self.progress_callback = progress_callback
self.is_multievent = isinstance(event, QuerySet)
if isinstance(event, QuerySet):
self.events = event
self.event = None
else:
self.events = Event.objects.filter(pk=event.pk)
def __str__(self):
return self.identifier
@@ -85,6 +95,7 @@ class BaseExporter:
class ListExporter(BaseExporter):
ProgressSetTotal = namedtuple('ProgressSetTotal', 'total')
@property
def export_form_fields(self) -> dict:
@@ -117,35 +128,66 @@ class ListExporter(BaseExporter):
def _render_csv(self, form_data, output_file=None, **kwargs):
if output_file:
if 'b' in output_file.mode:
output_file = io.TextIOWrapper(output_file, encoding='utf-8', newline='')
writer = csv.writer(output_file, **kwargs)
total = 0
counter = 0
for line in self.iterate_list(form_data):
if isinstance(line, self.ProgressSetTotal):
total = line.total
continue
line = [
localize(f) if isinstance(f, Decimal) else f
for f in line
]
if total:
counter += 1
if counter % max(10, total // 100) == 0:
self.progress_callback(counter / total * 100)
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', None
else:
output = io.StringIO()
writer = csv.writer(output, **kwargs)
total = 0
counter = 0
for line in self.iterate_list(form_data):
if isinstance(line, self.ProgressSetTotal):
total = line.total
continue
line = [
localize(f) if isinstance(f, Decimal) else f
for f in line
]
if total:
counter += 1
if counter % max(10, total // 100) == 0:
self.progress_callback(counter / total * 100)
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data, output_file=None):
wb = Workbook()
ws = wb.active
wb = Workbook(write_only=True)
ws = wb.create_sheet()
try:
ws.title = str(self.verbose_name)
except:
pass
total = 0
counter = 0
for i, line in enumerate(self.iterate_list(form_data)):
for j, val in enumerate(line):
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
if isinstance(line, self.ProgressSetTotal):
total = line.total
continue
ws.append([
str(val) if not isinstance(val, KNOWN_TYPES) else val
for val in line
])
if total:
counter += 1
if counter % max(10, total // 100) == 0:
self.progress_callback(counter / total * 100)
if output_file:
wb.save(output_file)
@@ -203,35 +245,63 @@ class MultiSheetListExporter(ListExporter):
raise NotImplementedError() # noqa
def _render_sheet_csv(self, form_data, sheet, output_file=None, **kwargs):
total = 0
counter = 0
if output_file:
if 'b' in output_file.mode:
output_file = io.TextIOWrapper(output_file, encoding='utf-8', newline='')
writer = csv.writer(output_file, **kwargs)
for line in self.iterate_sheet(form_data, sheet):
if isinstance(line, self.ProgressSetTotal):
total = line.total
continue
line = [
localize(f) if isinstance(f, Decimal) else f
for f in line
]
writer.writerow(line)
if total:
counter += 1
if counter % max(10, total // 100) == 0:
self.progress_callback(counter / total * 100)
return self.get_filename() + '.csv', 'text/csv', None
else:
output = io.StringIO()
writer = csv.writer(output, **kwargs)
for line in self.iterate_sheet(form_data, sheet):
if isinstance(line, self.ProgressSetTotal):
total = line.total
continue
line = [
localize(f) if isinstance(f, Decimal) else f
for f in line
]
writer.writerow(line)
if total:
counter += 1
if counter % max(10, total // 100) == 0:
self.progress_callback(counter / total * 100)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data, output_file=None):
wb = Workbook()
ws = wb.active
wb.remove(ws)
for s, l in self.sheets:
wb = Workbook(write_only=True)
n_sheets = len(self.sheets)
for i_sheet, (s, l) in enumerate(self.sheets):
ws = wb.create_sheet(str(l))
total = 0
counter = 0
for i, line in enumerate(self.iterate_sheet(form_data, sheet=s)):
for j, val in enumerate(line):
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
if isinstance(line, self.ProgressSetTotal):
total = line.total
continue
ws.append([
str(val) if not isinstance(val, KNOWN_TYPES) else val
for val in line
])
if total:
counter += 1
if counter % max(10, total // 100) == 0:
self.progress_callback(counter / total * 100 / n_sheets + 100 / n_sheets * i_sheet)
if output_file:
wb.save(output_file)

View File

@@ -1,81 +1,33 @@
import os
import tempfile
from collections import OrderedDict
from decimal import Decimal
from zipfile import ZipFile
import dateutil.parser
from django import forms
from django.db.models import Exists, OuterRef, Q
from django.db.models import CharField, Exists, F, OuterRef, Q, Subquery, Sum
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _, pgettext
from pretix.base.models import OrderPayment
from pretix.base.models import Invoice, InvoiceLine, OrderPayment
from ..exporter import BaseExporter
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ..exporter import BaseExporter, MultiSheetListExporter
from ..services.invoices import invoice_pdf_task
from ..signals import register_data_exporters
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
class InvoiceExporter(BaseExporter):
identifier = 'invoices'
verbose_name = _('All invoices')
def render(self, form_data: dict, output_file=None):
qs = self.event.invoices.filter(shredded=False)
if form_data.get('payment_provider'):
qs = qs.annotate(
has_payment_with_provider=Exists(
OrderPayment.objects.filter(
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider'))
)
)
)
qs = qs.filter(has_payment_with_provider=1)
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__gte=date_value)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
with tempfile.TemporaryDirectory() as d:
any = False
with ZipFile(output_file or os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
try:
if not i.file:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
any = True
i.file.close()
except FileNotFoundError:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
any = True
i.file.close()
if not any:
return None
if output_file:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', None
else:
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
class InvoiceExporterMixin:
@property
def export_form_fields(self):
def invoice_exporter_form_fields(self):
return OrderedDict(
[
('date_from',
@@ -99,6 +51,8 @@ class InvoiceExporter(BaseExporter):
label=_('Payment provider'),
choices=[
('', _('All payment providers')),
] + get_all_payment_providers() if self.is_multievent else [
('', _('All payment providers')),
] + [
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
],
@@ -111,7 +65,339 @@ class InvoiceExporter(BaseExporter):
]
)
def invoices_queryset(self, form_data: dict):
qs = Invoice.objects.filter(event__in=self.events)
if form_data.get('payment_provider'):
qs = qs.annotate(
has_payment_with_provider=Exists(
OrderPayment.objects.filter(
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider'))
)
)
)
qs = qs.filter(has_payment_with_provider=1)
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__gte=date_value)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
return qs
class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
identifier = 'invoices'
verbose_name = _('All invoices')
def render(self, form_data: dict, output_file=None):
qs = self.invoices_queryset(form_data).filter(shredded=False)
with tempfile.TemporaryDirectory() as d:
total = qs.count()
if not total:
return None
counter = 0
with ZipFile(output_file or os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs.iterator():
try:
if not i.file:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
i.file.close()
except FileNotFoundError:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
i.file.close()
counter += 1
if total and counter % max(10, total // 100) == 0:
self.progress_callback(counter / total * 100)
if self.is_multievent:
filename = '{}_invoices.zip'.format(self.events.first().organizer.slug)
else:
filename = '{}_invoices.zip'.format(self.event.slug)
if output_file:
return filename, 'application/zip', None
else:
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return filename, 'application/zip', zipf.read()
@property
def export_form_fields(self):
return self.invoice_exporter_form_fields
class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
identifier = 'invoicedata'
verbose_name = _('Invoice data')
@property
def additional_form_fields(self):
return self.invoice_exporter_form_fields
@property
def sheets(self):
return (
('invoices', _('Invoices')),
('lines', _('Invoice lines')),
)
def iterate_sheet(self, form_data, sheet):
_ = gettext
if sheet == 'invoices':
yield [
_('Invoice number'),
_('Date'),
_('Order code'),
_('E-mail address'),
_('Invoice type'),
_('Cancellation of'),
_('Language'),
_('Invoice sender:') + ' ' + _('Name'),
_('Invoice sender:') + ' ' + _('Address'),
_('Invoice sender:') + ' ' + _('ZIP code'),
_('Invoice sender:') + ' ' + _('City'),
_('Invoice sender:') + ' ' + _('Country'),
_('Invoice sender:') + ' ' + _('Tax ID'),
_('Invoice sender:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Company'),
_('Invoice recipient:') + ' ' + _('Name'),
_('Invoice recipient:') + ' ' + _('Street address'),
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
_('Reverse charge'),
_('Shown foreign currency'),
_('Foreign currency rate'),
_('Total value (with taxes)'),
_('Total value (without taxes)'),
_('Payment matching IDs'),
_('Payment providers'),
]
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
base_qs = self.invoices_queryset(form_data)\
qs = base_qs.select_related(
'order', 'refers'
).prefetch_related('order__payments').annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
total_gross=Subquery(
InvoiceLine.objects.filter(
invoice=OuterRef('pk')
).order_by().values('invoice').annotate(
s=Sum('gross_value')
).values('s')
),
total_net=Subquery(
InvoiceLine.objects.filter(
invoice=OuterRef('pk')
).order_by().values('invoice').annotate(
s=Sum(F('gross_value') - F('tax_value'))
).values('s')
)
)
all_ids = base_qs.order_by('full_invoice_no').values_list('pk', flat=True)
yield self.ProgressSetTotal(total=len(all_ids))
for ids in chunked_iterable(all_ids, 1000):
invs = sorted(qs.filter(id__in=ids), key=lambda k: ids.index(k.pk))
for i in invs:
pmis = []
for p in i.order.payments.all():
if p.state in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_CREATED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_REFUNDED):
pprov = p.payment_provider
if pprov:
mid = pprov.matching_id(p)
if mid:
pmis.append(mid)
pmi = '\n'.join(pmis)
yield [
i.full_invoice_no,
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
i.order.email,
_('Cancellation') if i.is_cancellation else _('Invoice'),
i.refers.full_invoice_no if i.refers else '',
i.locale,
i.invoice_from_name,
i.invoice_from,
i.invoice_from_zipcode,
i.invoice_from_city,
i.invoice_from_country,
i.invoice_from_tax_id,
i.invoice_from_vat_id,
i.invoice_to_company,
i.invoice_to_name,
i.invoice_to_street or i.invoice_to,
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
_('Yes') if i.reverse_charge else _('No'),
i.foreign_currency_display,
i.foreign_currency_rate,
i.total_gross if i.total_gross else Decimal('0.00'),
Decimal(i.total_net if i.total_net else '0.00').quantize(Decimal('0.01')),
pmi,
', '.join([
str(self.providers.get(p, p)) for p in sorted(set((i.payment_providers or '').split(',')))
if p and p != 'free'
])
]
elif sheet == 'lines':
yield [
_('Invoice number'),
_('Line number'),
_('Description'),
_('Gross price'),
_('Net price'),
_('Tax value'),
_('Tax rate'),
_('Tax name'),
_('Event start date'),
_('Date'),
_('Order code'),
_('E-mail address'),
_('Invoice type'),
_('Cancellation of'),
_('Invoice sender:') + ' ' + _('Name'),
_('Invoice sender:') + ' ' + _('Address'),
_('Invoice sender:') + ' ' + _('ZIP code'),
_('Invoice sender:') + ' ' + _('City'),
_('Invoice sender:') + ' ' + _('Country'),
_('Invoice sender:') + ' ' + _('Tax ID'),
_('Invoice sender:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Company'),
_('Invoice recipient:') + ' ' + _('Name'),
_('Invoice recipient:') + ' ' + _('Street address'),
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
_('Payment providers'),
]
p_providers = OrderPayment.objects.filter(
order=OuterRef('invoice__order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
qs = InvoiceLine.objects.annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
).filter(
invoice__in=self.invoices_queryset(form_data)
).order_by('invoice__full_invoice_no', 'position').select_related(
'invoice', 'invoice__order', 'invoice__refers'
)
yield self.ProgressSetTotal(total=qs.count())
for l in qs.iterator():
i = l.invoice
yield [
i.full_invoice_no,
l.position + 1,
l.description.replace("<br />", " - "),
l.gross_value,
l.net_value,
l.tax_value,
l.tax_rate,
l.tax_name,
date_format(l.event_date_from, "SHORT_DATE_FORMAT") if l.event_date_from else "",
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
i.order.email,
_('Cancellation') if i.is_cancellation else _('Invoice'),
i.refers.full_invoice_no if i.refers else '',
i.invoice_from_name,
i.invoice_from,
i.invoice_from_zipcode,
i.invoice_from_city,
i.invoice_from_country,
i.invoice_from_tax_id,
i.invoice_from_vat_id,
i.invoice_to_company,
i.invoice_to_name,
i.invoice_to_street or i.invoice_to,
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
', '.join([
str(self.providers.get(p, p)) for p in sorted(set((l.payment_providers or '').split(',')))
if p and p != 'free'
])
]
@cached_property
def providers(self):
return dict(get_all_payment_providers())
def get_filename(self):
if self.is_multievent:
return '{}_invoices'.format(self.events.first().organizer.slug)
else:
return '{}_invoices'.format(self.event.slug)
@receiver(register_data_exporters, dispatch_uid="exporter_invoices")
def register_invoice_export(sender, **kwargs):
return InvoiceExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_invoices")
def register_multievent_invoice_export(sender, **kwargs):
return InvoiceExporter
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
def register_invoicedata_exporter(sender, **kwargs):
return InvoiceDataExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_invoicedata")
def register_multievent_invoicedatae_xporter(sender, **kwargs):
return InvoiceDataExporter

View File

@@ -8,7 +8,9 @@ from pretix.base.models import OrderPosition
from ..exporter import BaseExporter
from ..models import Order
from ..signals import register_data_exporters
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
class MailExporter(BaseExporter):
@@ -16,14 +18,18 @@ class MailExporter(BaseExporter):
verbose_name = _('Email addresses (text file)')
def render(self, form_data: dict):
qs = self.event.orders.filter(status__in=form_data['status'])
qs = Order.objects.filter(event__in=self.events, status__in=form_data['status']).prefetch_related('event')
addrs = qs.values('email')
pos = OrderPosition.objects.filter(
order__event=self.event, order__status__in=form_data['status']
order__event__in=self.events, order__status__in=form_data['status']
).values('attendee_email')
data = "\r\n".join(set(a['email'] for a in addrs if a['email'])
| set(a['attendee_email'] for a in pos if a['attendee_email']))
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")
if self.is_multievent:
return '{}_pretixemails.txt'.format(self.events.first().organizer.slug), 'text/plain', data.encode("utf-8")
else:
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")
@property
def export_form_fields(self):
@@ -44,3 +50,8 @@ class MailExporter(BaseExporter):
@receiver(register_data_exporters, dispatch_uid="exporter_mail")
def register_mail_export(sender, **kwargs):
return MailExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_mail")
def register_multievent_mail_export(sender, **kwargs):
return MailExporter

View File

@@ -4,27 +4,37 @@ from decimal import Decimal
import pytz
from django import forms
from django.db.models import (
Count, DateTimeField, F, IntegerField, Max, OuterRef, Subquery, Sum,
CharField, Count, DateTimeField, IntegerField, Max, OuterRef, Subquery,
Sum,
)
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.models import (
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
GiftCard, Invoice, InvoiceAddress, Order, OrderPosition, Question,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import PERSON_NAME_SCHEMES
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ..exporter import ListExporter, MultiSheetListExporter
from ..signals import register_data_exporters
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
class OrderListExporter(MultiSheetListExporter):
identifier = 'orderlist'
verbose_name = gettext_lazy('Order data')
@cached_property
def providers(self):
return dict(get_all_payment_providers())
@property
def sheets(self):
return (
@@ -50,13 +60,13 @@ class OrderListExporter(MultiSheetListExporter):
tax_rates = set(
a for a
in OrderFee.objects.filter(
order__event=self.event
order__event__in=self.events
).values_list('tax_rate', flat=True).distinct().order_by()
)
tax_rates |= set(
a for a
in OrderPosition.objects.filter(
order__event=self.event
order__event__in=self.events
).values_list('tax_rate', flat=True).distinct().order_by()
)
tax_rates = sorted(tax_rates)
@@ -70,9 +80,11 @@ class OrderListExporter(MultiSheetListExporter):
elif sheet == 'fees':
return self.iterate_fees(form_data)
def iterate_orders(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
@cached_property
def event_object_cache(self):
return {e.pk: e for e in self.events}
def iterate_orders(self, form_data: dict):
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
@@ -82,24 +94,42 @@ class OrderListExporter(MultiSheetListExporter):
).values(
'm'
).order_by()
p_providers = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
i_numbers = Invoice.objects.filter(
order=OuterRef('pk'),
).values('order').annotate(
m=GroupConcat('full_invoice_no', delimiter=', ')
).values(
'm'
).order_by()
s = OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(k=Count('id')).values('k')
qs = self.event.orders.annotate(
qs = Order.objects.filter(event__in=self.events).annotate(
payment_date=Subquery(p_date, output_field=DateTimeField()),
payment_providers=Subquery(p_providers, output_field=CharField()),
invoice_numbers=Subquery(i_numbers, output_field=CharField()),
pcnt=Subquery(s, output_field=IntegerField())
).select_related('invoice_address').prefetch_related('invoices')
).select_related('invoice_address')
if form_data['paid_only']:
qs = qs.filter(status=Order.STATUS_PAID)
tax_rates = self._get_all_tax_rates(qs)
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'),
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Order time'), _('Company'), _('Name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(label)
headers += [
@@ -119,6 +149,7 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Requires special attention'))
headers.append(_('Comment'))
headers.append(_('Positions'))
headers.append(_('Payment providers'))
yield headers
@@ -139,20 +170,25 @@ class OrderListExporter(MultiSheetListExporter):
)
}
for order in qs.order_by('datetime'):
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)
row = [
self.event_object_cache[order.event_id].slug,
order.code,
order.total,
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
order.datetime.astimezone(tz).strftime('%H:%M:%S'),
]
try:
row += [
order.invoice_address.company,
order.invoice_address.name,
]
if len(name_scheme['fields']) > 1:
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
@@ -167,7 +203,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
@@ -189,27 +225,42 @@ class OrderListExporter(MultiSheetListExporter):
taxrate_values['taxsum'] + fee_taxrate_values['taxsum'],
]
row.append(', '.join([i.number for i in order.invoices.all()]))
row.append(order.invoice_numbers)
row.append(order.sales_channel)
row.append(_('Yes') if order.checkin_attention else _('No'))
row.append(order.comment or "")
row.append(order.pcnt)
row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((order.payment_providers or '').split(',')))
if p and p != 'free'
]))
yield row
def iterate_fees(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
qs = OrderFee.objects.filter(
order__event=self.event,
order__event__in=self.events,
).annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
).select_related('order', 'order__invoice_address', 'tax_rule')
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID)
headers = [
_('Event slug'),
_('Order code'),
_('Status'),
_('Email'),
_('Order date'),
_('Order time'),
_('Fee type'),
_('Description'),
_('Price'),
@@ -219,23 +270,28 @@ class OrderListExporter(MultiSheetListExporter):
_('Company'),
_('Invoice address name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
headers.append(_('Payment providers'))
yield headers
for op in qs.order_by('order__datetime'):
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)
row = [
self.event_object_cache[order.event_id].slug,
order.code,
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
order.datetime.astimezone(tz).strftime('%H:%M:%S'),
op.get_fee_type_display(),
op.description,
op.value,
@@ -248,7 +304,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.company,
order.invoice_address.name,
]
if len(name_scheme['fields']) > 1:
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
@@ -263,14 +319,28 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
if p and p != 'free'
]))
yield row
def iterate_positions(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
qs = OrderPosition.objects.filter(
order__event=self.event,
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
base_qs = OrderPosition.objects.filter(
order__event__in=self.events,
)
qs = base_qs.annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
).select_related(
'order', 'order__invoice_address', 'item', 'variation',
'voucher', 'tax_rule'
@@ -280,14 +350,18 @@ class OrderListExporter(MultiSheetListExporter):
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID)
has_subevents = self.events.filter(has_subevents=True).exists()
headers = [
_('Event slug'),
_('Order code'),
_('Position ID'),
_('Status'),
_('Email'),
_('Order date'),
_('Order time'),
]
if self.event.has_subevents:
if has_subevents:
headers.append(pgettext('subevent', 'Date'))
headers.append(_('Start date'))
headers.append(_('End date'))
@@ -300,8 +374,8 @@ class OrderListExporter(MultiSheetListExporter):
_('Tax value'),
_('Attendee name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Attendee name') + ': ' + str(label))
headers += [
@@ -314,8 +388,14 @@ class OrderListExporter(MultiSheetListExporter):
pgettext('address', 'State'),
_('Voucher'),
_('Pseudonymization ID'),
_('Seat ID'),
_('Seat name'),
_('Seat zone'),
_('Seat row'),
_('Seat number'),
]
questions = list(self.event.questions.all())
questions = list(Question.objects.filter(event__in=self.events))
options = {}
for q in questions:
if q.type == Question.TYPE_CHOICE_MULTIPLE:
@@ -329,7 +409,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Company'),
_('Invoice address name'),
]
if len(name_scheme['fields']) > 1:
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
@@ -337,97 +417,130 @@ class OrderListExporter(MultiSheetListExporter):
]
headers += [
_('Sales channel'), _('Order locale'),
_('Payment providers'),
]
yield headers
for op in qs.order_by('order__datetime', 'positionid'):
order = op.order
row = [
order.code,
op.positionid,
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
]
if self.event.has_subevents:
row.append(op.subevent.name)
row.append(op.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
if op.subevent.date_to:
row.append(op.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
else:
row.append('')
row += [
str(op.item),
str(op.variation) if op.variation else '',
op.price,
op.tax_rate,
str(op.tax_rule) if op.tax_rule else '',
op.tax_value,
op.attendee_name,
]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
op.attendee_name_parts.get(k, '')
)
row += [
op.attendee_email,
op.company or '',
op.street or '',
op.zipcode or '',
op.city or '',
op.country if op.country else '',
op.state or '',
op.voucher.code if op.voucher else '',
op.pseudonymization_id,
]
acache = {}
for a in op.answers.all():
# We do not want to localize Date, Time and Datetime question answers, as those can lead
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
if a.question.type == Question.TYPE_CHOICE_MULTIPLE:
acache[a.question_id] = set(o.pk for o in a.options.all())
elif a.question.type in Question.UNLOCALIZED_TYPES:
acache[a.question_id] = a.answer
else:
acache[a.question_id] = str(a)
for q in questions:
if q.type == Question.TYPE_CHOICE_MULTIPLE:
for o in options[q.pk]:
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
else:
row.append(acache.get(q.pk, ''))
all_ids = list(base_qs.order_by('order__datetime', 'positionid').values_list('pk', flat=True))
yield self.ProgressSetTotal(total=len(all_ids))
for ids in chunked_iterable(all_ids, 1000):
ops = sorted(qs.filter(id__in=ids), key=lambda k: ids.index(k.pk))
try:
row += [
order.invoice_address.company,
order.invoice_address.name,
for op in ops:
order = op.order
tz = pytz.timezone(self.event_object_cache[order.event_id].settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
order.code,
op.positionid,
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
order.datetime.astimezone(tz).strftime('%H:%M:%S'),
]
if len(name_scheme['fields']) > 1:
if has_subevents:
if op.subevent:
row.append(op.subevent.name)
row.append(op.subevent.date_from.astimezone(self.event_object_cache[order.event_id].timezone).strftime('%Y-%m-%d %H:%M:%S'))
if op.subevent.date_to:
row.append(op.subevent.date_to.astimezone(self.event_object_cache[order.event_id].timezone).strftime('%Y-%m-%d %H:%M:%S'))
else:
row.append('')
else:
row.append('')
row.append('')
row.append('')
row += [
str(op.item),
str(op.variation) if op.variation else '',
op.price,
op.tax_rate,
str(op.tax_rule) if op.tax_rule else '',
op.tax_value,
op.attendee_name,
]
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
op.attendee_name_parts.get(k, '')
)
row += [
order.invoice_address.street,
order.invoice_address.zipcode,
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
op.attendee_email,
op.company or '',
op.street or '',
op.zipcode or '',
op.city or '',
op.country if op.country else '',
op.state or '',
op.voucher.code if op.voucher else '',
op.pseudonymization_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [
order.sales_channel,
order.locale
]
yield row
if op.seat:
row += [
op.seat.seat_guid,
str(op.seat),
op.seat.zone_name,
op.seat.row_name,
op.seat.seat_number,
]
else:
row += ['', '', '', '', '']
acache = {}
for a in op.answers.all():
# We do not want to localize Date, Time and Datetime question answers, as those can lead
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
if a.question.type == Question.TYPE_CHOICE_MULTIPLE:
acache[a.question_id] = set(o.pk for o in a.options.all())
elif a.question.type in Question.UNLOCALIZED_TYPES:
acache[a.question_id] = a.answer
else:
acache[a.question_id] = str(a)
for q in questions:
if q.type == Question.TYPE_CHOICE_MULTIPLE:
for o in options[q.pk]:
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
else:
row.append(acache.get(q.pk, ''))
try:
row += [
order.invoice_address.company,
order.invoice_address.name,
]
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
)
row += [
order.invoice_address.street,
order.invoice_address.zipcode,
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
row += [
order.sales_channel,
order.locale
]
row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
if p and p != 'free'
]))
yield row
def get_filename(self):
return '{}_orders'.format(self.event.slug)
if self.is_multievent:
return '{}_orders'.format(self.events.first().organizer.slug)
else:
return '{}_orders'.format(self.event.slug)
class PaymentListExporter(ListExporter):
@@ -459,31 +572,28 @@ class PaymentListExporter(ListExporter):
)
def iterate_list(self, form_data):
tz = pytz.timezone(self.event.settings.timezone)
provider_names = {
k: v.verbose_name
for k, v in self.event.get_payment_providers().items()
}
provider_names = dict(get_all_payment_providers())
payments = OrderPayment.objects.filter(
order__event=self.event,
order__event__in=self.events,
state__in=form_data.get('payment_states', [])
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event=self.event,
order__event__in=self.events,
state__in=form_data.get('refund_states', [])
).order_by('created')
objs = sorted(list(payments) + list(refunds), key=lambda o: o.created)
headers = [
_('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Status code'), _('Amount'), _('Payment method')
]
yield headers
yield self.ProgressSetTotal(total=len(objs))
for obj in objs:
tz = pytz.timezone(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:
@@ -491,6 +601,7 @@ class PaymentListExporter(ListExporter):
else:
d2 = ''
row = [
obj.order.event.slug,
obj.order.code,
obj.full_id,
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
@@ -503,7 +614,10 @@ class PaymentListExporter(ListExporter):
yield row
def get_filename(self):
return '{}_payments'.format(self.event.slug)
if self.is_multievent:
return '{}_payments'.format(self.events.first().organizer.slug)
else:
return '{}_payments'.format(self.event.slug)
class QuotaListExporter(ListExporter):
@@ -513,7 +627,7 @@ class QuotaListExporter(ListExporter):
def iterate_list(self, form_data):
headers = [
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
_('Current user\'s carts'), _('Waiting list'), _('Current availability')
_('Current user\'s carts'), _('Waiting list'), _('Exited orders'), _('Current availability')
]
yield headers
@@ -532,6 +646,7 @@ class QuotaListExporter(ListExporter):
qa.count_vouchers[quota],
qa.count_cart[quota],
qa.count_waitinglist[quota],
qa.count_exited_orders[quota],
_('Infinite') if avail[1] is None else avail[1]
]
yield row
@@ -540,218 +655,34 @@ class QuotaListExporter(ListExporter):
return '{}_quotas'.format(self.event.slug)
class InvoiceDataExporter(MultiSheetListExporter):
identifier = 'invoicedata'
verbose_name = gettext_lazy('Invoice data')
@property
def sheets(self):
return (
('invoices', _('Invoices')),
('lines', _('Invoice lines')),
)
def iterate_sheet(self, form_data, sheet):
if sheet == 'invoices':
yield [
_('Invoice number'),
_('Date'),
_('Order code'),
_('E-mail address'),
_('Invoice type'),
_('Cancellation of'),
_('Language'),
_('Invoice sender:') + ' ' + _('Name'),
_('Invoice sender:') + ' ' + _('Address'),
_('Invoice sender:') + ' ' + _('ZIP code'),
_('Invoice sender:') + ' ' + _('City'),
_('Invoice sender:') + ' ' + _('Country'),
_('Invoice sender:') + ' ' + _('Tax ID'),
_('Invoice sender:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Company'),
_('Invoice recipient:') + ' ' + _('Name'),
_('Invoice recipient:') + ' ' + _('Street address'),
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
_('Reverse charge'),
_('Shown foreign currency'),
_('Foreign currency rate'),
_('Total value (with taxes)'),
_('Total value (without taxes)'),
_('Payment matching IDs'),
]
qs = self.event.invoices.order_by('full_invoice_no').select_related(
'order', 'refers'
).prefetch_related('order__payments').annotate(
total_gross=Subquery(
InvoiceLine.objects.filter(
invoice=OuterRef('pk')
).order_by().values('invoice').annotate(
s=Sum('gross_value')
).values('s')
),
total_net=Subquery(
InvoiceLine.objects.filter(
invoice=OuterRef('pk')
).order_by().values('invoice').annotate(
s=Sum(F('gross_value') - F('tax_value'))
).values('s')
)
)
for i in qs:
pmis = []
for p in i.order.payments.all():
if p.state in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_CREATED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_REFUNDED):
pprov = p.payment_provider
if pprov:
mid = pprov.matching_id(p)
if mid:
pmis.append(mid)
pmi = '\n'.join(pmis)
yield [
i.full_invoice_no,
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
i.order.email,
_('Cancellation') if i.is_cancellation else _('Invoice'),
i.refers.full_invoice_no if i.refers else '',
i.locale,
i.invoice_from_name,
i.invoice_from,
i.invoice_from_zipcode,
i.invoice_from_city,
i.invoice_from_country,
i.invoice_from_tax_id,
i.invoice_from_vat_id,
i.invoice_to_company,
i.invoice_to_name,
i.invoice_to_street or i.invoice_to,
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
_('Yes') if i.reverse_charge else _('No'),
i.foreign_currency_display,
i.foreign_currency_rate,
i.total_gross if i.total_gross else Decimal('0.00'),
Decimal(i.total_net if i.total_net else '0.00').quantize(Decimal('0.01')),
pmi
]
elif sheet == 'lines':
yield [
_('Invoice number'),
_('Line number'),
_('Description'),
_('Gross price'),
_('Net price'),
_('Tax value'),
_('Tax rate'),
_('Tax name'),
_('Event start date'),
_('Date'),
_('Order code'),
_('E-mail address'),
_('Invoice type'),
_('Cancellation of'),
_('Invoice sender:') + ' ' + _('Name'),
_('Invoice sender:') + ' ' + _('Address'),
_('Invoice sender:') + ' ' + _('ZIP code'),
_('Invoice sender:') + ' ' + _('City'),
_('Invoice sender:') + ' ' + _('Country'),
_('Invoice sender:') + ' ' + _('Tax ID'),
_('Invoice sender:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Company'),
_('Invoice recipient:') + ' ' + _('Name'),
_('Invoice recipient:') + ' ' + _('Street address'),
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
]
qs = InvoiceLine.objects.filter(
invoice__event=self.event
).order_by('invoice__full_invoice_no', 'position').select_related(
'invoice', 'invoice__order', 'invoice__refers'
)
for l in qs:
i = l.invoice
yield [
i.full_invoice_no,
l.position + 1,
l.description.replace("<br />", " - "),
l.gross_value,
l.net_value,
l.tax_value,
l.tax_rate,
l.tax_name,
date_format(l.event_date_from, "SHORT_DATE_FORMAT") if l.event_date_from else "",
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
i.order.email,
_('Cancellation') if i.is_cancellation else _('Invoice'),
i.refers.full_invoice_no if i.refers else '',
i.invoice_from_name,
i.invoice_from,
i.invoice_from_zipcode,
i.invoice_from_city,
i.invoice_from_country,
i.invoice_from_tax_id,
i.invoice_from_vat_id,
i.invoice_to_company,
i.invoice_to_name,
i.invoice_to_street or i.invoice_to,
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
]
def get_filename(self):
return '{}_invoices'.format(self.event.slug)
class GiftcardRedemptionListExporter(ListExporter):
identifier = 'giftcardredemptionlist'
verbose_name = gettext_lazy('Gift card redemptions')
def iterate_list(self, form_data):
tz = pytz.timezone(self.event.settings.timezone)
payments = OrderPayment.objects.filter(
order__event=self.event,
provider='giftcard'
order__event__in=self.events,
provider='giftcard',
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event=self.event,
provider='giftcard'
order__event__in=self.events,
provider='giftcard',
state=OrderRefund.REFUND_STATE_DONE
).order_by('created')
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
headers = [
_('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
_('Event slug'), _('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
]
yield headers
for obj in objs:
tz = pytz.timezone(obj.order.event.settings.timezone)
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
row = [
obj.order.event.slug,
obj.order.code,
obj.full_id,
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
@@ -762,7 +693,10 @@ class GiftcardRedemptionListExporter(ListExporter):
yield row
def get_filename(self):
return '{}_giftcardredemptions'.format(self.event.slug)
if self.is_multievent:
return '{}_giftcardredemptions'.format(self.events.first().organizer.slug)
else:
return '{}_giftcardredemptions'.format(self.event.slug)
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
@@ -770,21 +704,31 @@ def register_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_orderlist")
def register_multievent_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
def register_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_paymentlist")
def register_multievent_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_quotalist")
def register_quotalist_exporter(sender, **kwargs):
return QuotaListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
def register_invoicedata_exporter(sender, **kwargs):
return InvoiceDataExporter
@receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist")
def register_giftcardredemptionlist_exporter(sender, **kwargs):
return GiftcardRedemptionListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardredemptionlist")
def register_multievent_i_giftcardredemptionlist_exporter(sender, **kwargs):
return GiftcardRedemptionListExporter

View File

@@ -65,6 +65,8 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
super().__init__(*args, **kwargs)
for fname in self.auto_fields:
kwargs = DEFAULTS[fname].get('form_kwargs', {})
if callable(kwargs):
kwargs = kwargs()
kwargs.setdefault('required', False)
field = DEFAULTS[fname]['form_class'](
**kwargs

View File

@@ -36,8 +36,8 @@ from pretix.base.i18n import language
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS,
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS,
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import ExtFileField, SplitDateTimeField
@@ -49,7 +49,7 @@ from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
REQUIRED_NAME_PARTS = ['given_name', 'family_name', 'full_name']
REQUIRED_NAME_PARTS = ['salutation', 'given_name', 'family_name', 'full_name']
class NamePartsWidget(forms.MultiWidget):
@@ -73,6 +73,8 @@ class NamePartsWidget(forms.MultiWidget):
a['data-fname'] = fname
if fname == 'title' and self.titles:
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
elif fname == 'salutation':
widgets.append(Select(attrs=a, choices=[('', '---')] + [(s, s) for s in PERSON_NAME_SALUTATIONS]))
else:
widgets.append(self.widget(attrs=a))
super().__init__(widgets, attrs)
@@ -162,12 +164,18 @@ class NamePartsFormField(forms.MultiValueField):
**d,
choices=[('', '')] + [(d, d) for d in self.scheme_titles[1]]
)
field.part_name = fname
fields.append(field)
elif fname == 'salutation':
d = dict(defaults)
d.pop('max_length', None)
field = forms.ChoiceField(
**d,
choices=[('', '---')] + [(s, s) for s in PERSON_NAME_SALUTATIONS]
)
else:
field = forms.CharField(**defaults)
field.part_name = fname
fields.append(field)
field.part_name = fname
fields.append(field)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
@@ -184,6 +192,10 @@ class NamePartsFormField(forms.MultiValueField):
raise forms.ValidationError(self.error_messages['required'], code='required')
if self.require_all_fields and not all(v for v in value):
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
if sum(len(v) for v in value if v) > 250:
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
return value
@@ -244,8 +256,10 @@ class BaseQuestionsForm(forms.Form):
super().__init__(*args, **kwargs)
add_fields = {}
if item.admission and event.settings.attendee_names_asked:
self.fields['attendee_name_parts'] = NamePartsFormField(
add_fields['attendee_name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.attendee_names_required and not self.all_optional,
scheme=event.settings.name_scheme,
@@ -254,7 +268,7 @@ class BaseQuestionsForm(forms.Form):
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
)
if item.admission and event.settings.attendee_emails_asked:
self.fields['attendee_email'] = forms.EmailField(
add_fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required and not self.all_optional,
label=_('Attendee email'),
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
@@ -265,14 +279,15 @@ class BaseQuestionsForm(forms.Form):
)
)
if item.admission and event.settings.attendee_company_asked:
self.fields['company'] = forms.CharField(
add_fields['company'] = forms.CharField(
required=event.settings.attendee_company_required and not self.all_optional,
label=_('Company'),
max_length=255,
initial=(cartpos.company if cartpos else orderpos.company),
)
if item.admission and event.settings.attendee_addresses_asked:
self.fields['street'] = forms.CharField(
add_fields['street'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Address'),
widget=forms.Textarea(attrs={
@@ -282,24 +297,26 @@ class BaseQuestionsForm(forms.Form):
}),
initial=(cartpos.street if cartpos else orderpos.street),
)
self.fields['zipcode'] = forms.CharField(
add_fields['zipcode'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
max_length=30,
label=_('ZIP code'),
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
widget=forms.TextInput(attrs={
'autocomplete': 'postal-code',
}),
)
self.fields['city'] = forms.CharField(
add_fields['city'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('City'),
max_length=255,
initial=(cartpos.city if cartpos else orderpos.city),
widget=forms.TextInput(attrs={
'autocomplete': 'address-level2',
}),
)
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
self.fields['country'] = CountryField(
add_fields['country'] = CountryField(
countries=CachedCountries
).formfield(
required=event.settings.attendee_addresses_required and not self.all_optional,
@@ -324,7 +341,7 @@ class BaseQuestionsForm(forms.Form):
self.data = self.data.copy()
del self.data[fprefix + 'state']
self.fields['state'] = forms.ChoiceField(
add_fields['state'] = forms.ChoiceField(
label=pgettext_lazy('address', 'State'),
required=False,
choices=c,
@@ -332,7 +349,14 @@ class BaseQuestionsForm(forms.Form):
'autocomplete': 'address-level1',
}),
)
self.fields['state'].widget.is_required = True
add_fields['state'].widget.is_required = True
field_positions = list(
[
(n, event.settings.system_question_order.get(n if n != 'state' else 'country', 0))
for n in add_fields.keys()
]
)
for q in questions:
# Do we already have an answer? Provide it as the initial value
@@ -385,13 +409,14 @@ class BaseQuestionsForm(forms.Form):
)
elif q.type == Question.TYPE_COUNTRYCODE:
field = CountryField(
countries=CachedCountries
countries=CachedCountries,
blank=True, null=True, blank_label=' ',
).formfield(
label=label, required=required,
help_text=help_text,
widget=forms.Select,
empty_label='',
initial=initial.answer if initial else guess_country(event),
empty_label=' ',
initial=initial.answer if initial else (guess_country(event) if required else None),
)
elif q.type == Question.TYPE_CHOICE:
field = forms.ModelChoiceField(
@@ -485,7 +510,12 @@ class BaseQuestionsForm(forms.Form):
field._required = q.required and not self.all_optional
field.required = False
self.fields['question_%s' % q.id] = field
add_fields['question_%s' % q.id] = field
field_positions.append(('question_%s' % q.id, q.position))
field_positions.sort(key=lambda e: e[1])
for fname, p in field_positions:
self.fields[fname] = add_fields[fname]
responses = question_form_fields.send(sender=event, position=pos)
data = pos.meta_info_data
@@ -532,7 +562,8 @@ class BaseQuestionsForm(forms.Form):
if not self.all_optional:
for q in question_cache.values():
if question_is_required(q) and not d.get('question_%d' % q.pk):
answer = d.get('question_%d' % q.pk)
if question_is_required(q) and not answer and answer != 0:
raise ValidationError({'question_%d' % q.pk: [_('This field is required')]})
return d
@@ -566,7 +597,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-in-eu': ','.join(EU_COUNTRIES)}),
'internal_reference': forms.TextInput,
}
labels = {
@@ -616,6 +647,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
)
self.fields['state'].widget.is_required = True
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
if cc and cc not in EU_COUNTRIES and fprefix + 'vat_id' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'vat_id']
if not event.settings.invoice_address_required or self.all_optional:
for k, f in self.fields.items():
f.required = False
@@ -630,8 +666,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['company'].widget.is_required = True
self.fields['company'].widget.attrs['required'] = 'required'
del self.fields['company'].widget.attrs['data-display-dependency']
if 'vat_id' in self.fields:
del self.fields['vat_id'].widget.attrs['data-display-dependency']
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
@@ -663,6 +697,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
data = self.cleaned_data
if not data.get('is_business'):
data['company'] = ''
data['vat_id'] = ''
if data.get('is_business') and not data.get('country') in EU_COUNTRIES:
data['vat_id'] = ''
if self.event.settings.invoice_address_required:
if data.get('is_business') and not data.get('company'):
raise ValidationError(_('You need to provide a company name.'))
@@ -683,7 +720,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
) and len(data.get('name_parts', {})) == 1:
# Do not save the country if it is the only field set -- we don't know the user even checked it!
self.cleaned_data['country'] = ''
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):

View File

@@ -15,6 +15,7 @@ class DatePickerWidget(forms.DateInput):
date_attrs = dict(attrs)
date_attrs.setdefault('class', 'form-control')
date_attrs['class'] += ' datepickerfield'
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
date_attrs['placeholder'] = now().replace(
@@ -32,6 +33,7 @@ class TimePickerWidget(forms.TimeInput):
time_attrs = dict(attrs)
time_attrs.setdefault('class', 'form-control')
time_attrs['class'] += ' timepickerfield'
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
time_attrs['placeholder'] = now().replace(
@@ -102,6 +104,8 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs.setdefault('autocomplete', 'off')
date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield'
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
def date_placeholder():
df = date_format or get_format('DATE_INPUT_FORMATS')[0]

View File

@@ -4,6 +4,9 @@ from django.conf import settings
from django.utils import translation
from django.utils.formats import date_format, number_format
from django.utils.translation import gettext
from pretix.base.templatetags.money import money_filter
from i18nfield.fields import ( # noqa
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
)
@@ -12,8 +15,6 @@ from i18nfield.forms import I18nFormField # noqa
from i18nfield.strings import LazyI18nString # noqa
from i18nfield.utils import I18nJSONEncoder # noqa
from pretix.base.templatetags.money import money_filter
class LazyDate:
def __init__(self, value):
@@ -26,6 +27,21 @@ class LazyDate:
return date_format(self.value, "SHORT_DATE_FORMAT")
class LazyExpiresDate:
def __init__(self, expires):
self.value = expires
def __format__(self, format_spec):
return self.__str__()
def __str__(self):
at_end_of_day = self.value.hour == 23 and self.value.minute == 59 and self.value.second >= 59
if at_end_of_day:
return date_format(self.value, "SHORT_DATE_FORMAT")
else:
return date_format(self.value, "SHORT_DATETIME_FORMAT")
class LazyCurrencyNumber:
def __init__(self, value, currency):
self.value = value

View File

@@ -391,18 +391,18 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
p_size = p.wrap(self.event_width, self.event_height)
return txt
if not self.invoice.event.has_subevents:
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
p_str = (
shorten(self.invoice.event.name) + '\n' +
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
from_date=self.invoice.event.get_date_from_display(),
to_date=self.invoice.event.get_date_to_display()
from_date=self.invoice.event.get_date_from_display(show_times=False),
to_date=self.invoice.event.get_date_to_display(show_times=False)
)
)
else:
p_str = (
shorten(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
shorten(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display(show_times=False)
)
else:
p_str = shorten(self.invoice.event.name)

View File

@@ -1,46 +1,91 @@
import json
import sys
import pytz
from django.core.management.base import BaseCommand
from django.utils.timezone import override
from django_scopes import scope
from tqdm import tqdm
from pretix.base.i18n import language
from pretix.base.models import Event, Organizer
from pretix.base.signals import register_data_exporters
from pretix.base.signals import (
register_data_exporters, register_multievent_data_exporters,
)
class Command(BaseCommand):
help = "Run an exporter to get data out of pretix"
def add_arguments(self, parser):
parser.add_argument('organizer_slug', nargs=1, type=str)
parser.add_argument('event_slug', nargs=1, type=str)
parser.add_argument('export_provider', nargs=1, type=str)
parser.add_argument('output_file', nargs=1, type=str)
parser.add_argument('organizer_slug', type=str)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('event_slug', nargs="?", type=str)
group.add_argument('--all-events', action='store_true')
group.add_argument('--event_slugs', nargs="+", type=str)
parser.add_argument('export_provider', type=str)
parser.add_argument('output_file', type=str)
parser.add_argument('--parameters', action='store', type=str, help='JSON-formatted parameters')
parser.add_argument('--locale', action='store', type=str, help='...')
parser.add_argument('--timezone', action='store', type=str, help='...')
def handle(self, *args, **options):
try:
o = Organizer.objects.get(slug=options['organizer_slug'][0])
o = Organizer.objects.get(slug=options['organizer_slug'])
except Organizer.DoesNotExist:
self.stderr.write(self.style.ERROR('Organizer not found.'))
sys.exit(1)
with scope(organizer=o):
try:
e = o.events.get(slug=options['event_slug'][0])
except Event.DoesNotExist:
self.stderr.write(self.style.ERROR('Event not found.'))
sys.exit(1)
locale = options.get("locale", None)
timezone = pytz.timezone(options['timezone']) if options.get('timezone') else None
with language(e.settings.locale), override(e.settings.timezone):
responses = register_data_exporters.send(e)
for receiver, response in responses:
ex = response(e)
if ex.identifier == options['export_provider'][0]:
with scope(organizer=o):
if options['event_slug']:
try:
e = o.events.get(slug=options['event_slug'])
except Event.DoesNotExist:
self.stderr.write(self.style.ERROR('Event not found.'))
sys.exit(1)
if not locale:
locale = e.settings.locale
if not timezone:
timezone = e.settings.timezone
signal_result = register_data_exporters.send(e)
else:
e = o.events.all()
if options['event_slugs']:
e = e.filter(slug__in=options['event_slugs'])
not_found = set(options['event_slugs']).difference(event.slug for event in e)
if not_found:
self.stderr.write(self.style.ERROR('The following events were not found: {}'.format(", ".join(not_found))))
sys.exit(1)
if not e.exists():
self.stderr.write(self.style.ERROR('No events found.'))
sys.exit(1)
if not locale:
locale = e.first().settings.locale
self.stderr.write(self.style.WARNING(
"Guessing locale '{}' based on event '{}'.".format(locale, e.first().slug)))
if not timezone:
timezone = e.first().settings.timezone
self.stderr.write(self.style.WARNING(
"Guessing timezone '{}' based on event '{}'.".format(timezone, e.first().slug)))
signal_result = register_multievent_data_exporters.send(o)
pbar = tqdm(total=100)
def report_status(val):
pbar.update(round(val, 2) - pbar.n)
with language(locale), override(timezone):
for receiver, response in signal_result:
ex = response(e, report_status)
if ex.identifier == options['export_provider']:
params = json.loads(options.get('parameters') or '{}')
with open(options['output_file'][0], 'wb') as f:
with open(options['output_file'], 'wb') as f:
try:
ex.render(form_data=params, output_file=f)
except TypeError:
@@ -53,6 +98,7 @@ class Command(BaseCommand):
f.write(d[2])
sys.exit(0)
pbar.close()
self.stderr.write(self.style.ERROR('Export provider not found.'))
sys.exit(1)

View File

@@ -1,6 +1,11 @@
import time
import traceback
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.dispatch.dispatcher import NO_RECEIVERS
from pretix.helpers.periodic import SKIPPED
from ...signals import periodic_task
@@ -9,12 +14,30 @@ class Command(BaseCommand):
help = "Run periodic tasks"
def handle(self, *args, **options):
for recv, resp in periodic_task.send_robust(self):
if isinstance(resp, Exception):
verbosity = int(options['verbosity'])
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
return
for receiver in periodic_task._live_receivers(self):
if verbosity > 1:
self.stdout.write(f'Running {receiver.__module__}.{receiver.__name__}')
t0 = time.time()
try:
r = receiver(signal=periodic_task, sender=self)
except Exception as err:
if isinstance(Exception, KeyboardInterrupt):
raise err
if settings.SENTRY_ENABLED:
from sentry_sdk import capture_exception
capture_exception(resp)
capture_exception(err)
self.stdout.write(self.style.ERROR(f'FAIL: {str(err)}\n'))
else:
raise resp
call_command('clearsessions')
self.stdout.write(self.style.ERROR(f'FAIL: {str(err)}\n'))
traceback.print_exc()
else:
if options.get('verbosity') > 1:
if r is SKIPPED:
self.stdout.write(self.style.SUCCESS(f'Skipped {receiver.__module__}.{receiver.__name__}'))
else:
self.stdout.write(self.style.SUCCESS(f'Completed {receiver.__module__}.{receiver.__name__} in {round(time.time() - t0, 3)}s'))

View File

@@ -18,6 +18,7 @@ class Command(BaseCommand):
cmd = 'shell_plus'
except ImportError:
cmd = 'shell'
del options['skip_checks']
parser = self.create_parser(sys.argv[0], sys.argv[1])
flags = parser.parse_known_args(sys.argv[2:])[1]

View File

@@ -3,6 +3,9 @@ from collections import defaultdict
from django.apps import apps
from django.conf import settings
from django.db import connection
from pretix.base.models import Event, Invoice, Order, OrderPosition, Organizer
if settings.HAS_REDIS:
import django_redis
@@ -201,6 +204,19 @@ class Histogram(Metric):
self._execute_redis_pipeline(pipe)
def estimate_count_fast(type):
"""
See https://wiki.postgresql.org/wiki/Count_estimate
"""
if 'postgres' in settings.DATABASES['default']['ENGINE']:
cursor = connection.cursor()
cursor.execute("select reltuples from pg_class where relname='%s';" % type._meta.db_table)
row = cursor.fetchone()
return int(row[0])
else:
return type.objects.count()
def metric_values():
"""
Produces the the values to be presented to the monitoring system
@@ -223,8 +239,14 @@ def metric_values():
metrics[a] = metrics[atarget]
# Throwaway metrics
exact_tables = [
Order, OrderPosition, Invoice, Event, Organizer
]
for m in apps.get_models(): # Count all models
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
if any(issubclass(m, p) for p in exact_tables):
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
else:
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
return metrics

View File

@@ -199,9 +199,7 @@ class SecurityMiddleware(MiddlewareMixin):
'default-src': ["{static}"],
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'object-src': ["'none'"],
# frame-src is deprecated but kept for compatibility with CSP 1.0 browsers, e.g. Safari 9
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src,
@@ -212,8 +210,9 @@ class SecurityMiddleware(MiddlewareMixin):
# single-sign-on this can be nearly anything so we cannot really restrict
# this. However, we'll restrict it to HTTPS.
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
'report-uri': ["/csp_report/"],
}
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.0.6 on 2020-06-20 16:33
import django_countries.fields
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0153_auto_20200528_1953'),
]
operations = [
migrations.AddField(
model_name='subeventitem',
name='disabled',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='subeventitemvariation',
name='disabled',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='invoiceaddress',
name='country',
field=django_countries.fields.CountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.6 on 2020-06-26 14:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0154_auto_20200620_1633'),
]
operations = [
migrations.AddField(
model_name='quota',
name='release_after_exit',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.6 on 2020-06-28 19:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0155_quota_release_after_exit'),
]
operations = [
migrations.AddField(
model_name='cartposition',
name='override_tax_rate',
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.0.6 on 2020-07-12 09:32
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0156_cartposition_override_tax_rate'),
]
operations = [
migrations.AddField(
model_name='itemaddon',
name='multi_allowed',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 3.0.8 on 2020-07-24 07:54
from django.db import migrations, models
import pretix.base.models.base
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0157_auto_20200712_0932'),
]
operations = [
migrations.AlterField(
model_name='cachedfile',
name='file',
field=models.FileField(max_length=255, null=True, upload_to=pretix.base.models.base.cachedfile_name),
),
migrations.AlterField(
model_name='cartposition',
name='country',
field=pretix.helpers.countries.FastCountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2, null=True),
),
migrations.AlterField(
model_name='invoice',
name='invoice_from_country',
field=pretix.helpers.countries.FastCountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2, null=True),
),
migrations.AlterField(
model_name='invoice',
name='invoice_to_country',
field=pretix.helpers.countries.FastCountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2, null=True),
),
migrations.AlterField(
model_name='invoiceaddress',
name='country',
field=pretix.helpers.countries.FastCountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2),
),
migrations.AlterField(
model_name='orderposition',
name='country',
field=pretix.helpers.countries.FastCountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2, null=True),
),
migrations.AlterField(
model_name='taxrule',
name='home_country',
field=pretix.helpers.countries.FastCountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2),
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 3.0.8 on 2020-07-24 09:05
from django.db import migrations
from pretix.base.channels import get_all_sales_channels
def set_sales_channels(apps, schema_editor):
# We now allow restricting some mails to certain sales channels
# The default is changing from all channels to "web" only
# Therefore, for existing events, we enable all sales channels
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
Event = apps.get_model('pretixbase', 'Event')
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channels()) + "]"
batch_size = 1000
Event_SettingsStore.objects.bulk_create([
Event_SettingsStore(
object=event,
key="mail_sales_channel_placed_paid",
value=all_sales_channels)
for event in Event.objects.all()
], batch_size=batch_size)
Event_SettingsStore.objects.bulk_create([
Event_SettingsStore(
object=event,
key="mail_sales_channel_download_reminder",
value=all_sales_channels)
for event in Event.objects.all()
], batch_size=batch_size)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0158_auto_20200724_0754'),
]
operations = [
migrations.RunPython(set_sales_channels, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.0.8 on 2020-07-31 10:05
import json
from django.db import migrations
def migrate_confirm_text(apps, schema_editor):
# We now allow creating multiple confirm texts so we migrate the setting for that
# from `confirm_text` to `confirm_texts`
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
for store in Event_SettingsStore.objects.filter(key="confirm_text"):
if store.value:
values = json.dumps([json.loads(store.value)]) # convert single value to one-element list
store.key = "confirm_texts"
store.value = values
store.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0159_mails_by_sales_channel'),
]
operations = [
migrations.RunPython(migrate_confirm_text, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,20 @@
from django.db import migrations
def migrate_change_allow_user_price(apps, schema_editor):
# Previously, the "gt" value was meant to represent "greater or equal", which became an issue the moment
# we introduced a "greater" and "greater or equal" option. This migrates any previous "greater or equal"
# selection to the new "gte".
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
Event_SettingsStore.objects.filter(key="change_allow_user_price", value="gt").update(value="gte")
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0160_multiple_confirm_texts'),
]
operations = [
migrations.RunPython(migrate_change_allow_user_price, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.0.9 on 2020-08-24 07:29
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0161_order_changes_retain_old_default'),
]
operations = [
migrations.RemoveField(
model_name='seat',
name='name',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.9 on 2020-10-13 08:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0162_remove_seat_name'),
]
operations = [
migrations.AddField(
model_name='device',
name='security_profile',
field=models.CharField(default='full', max_length=190, null=True),
),
]

View File

@@ -173,7 +173,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return self.email
def send_security_notice(self, messages, email=None):
from pretix.base.services.mail import mail, SendMailException
from pretix.base.services.mail import SendMailException, mail
try:
with language(self.locale):

View File

@@ -27,7 +27,7 @@ class CachedFile(models.Model):
date = models.DateTimeField(null=True, blank=True)
filename = models.CharField(max_length=255)
type = models.CharField(max_length=255)
file = models.FileField(null=True, blank=True, upload_to=cachedfile_name)
file = models.FileField(null=True, blank=True, upload_to=cachedfile_name, max_length=255)
@receiver(post_delete, sender=CachedFile)
@@ -48,14 +48,15 @@ class LoggingMixin:
:param data: Any JSON-serializable object
:param user: The user performing the action (optional)
"""
from .log import LogEntry
from .event import Event
from .devices import Device
from pretix.api.models import OAuthAccessToken, OAuthApplication
from .organizer import TeamAPIToken
from pretix.api.webhooks import get_all_webhook_events, notify_webhooks
from ..notifications import get_all_notification_types
from ..services.notifications import notify
from pretix.api.webhooks import get_all_webhook_events, notify_webhooks
from .devices import Device
from .event import Event
from .log import LogEntry
from .organizer import TeamAPIToken
event = None
if isinstance(self, Event):

View File

@@ -1,8 +1,9 @@
from django.conf import settings
from django.db import models
from django.db.models import Exists, F, Max, OuterRef, Q, Subquery
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from django_scopes import ScopedManager, scopes_disabled
from jsonfallback.fields import FallbackJSONField
from pretix.base.models import LoggedModel
@@ -47,7 +48,7 @@ class CheckinList(LoggedModel):
@property
def positions(self):
from . import OrderPosition, Order
from . import Order, OrderPosition
qs = OrderPosition.objects.filter(
order__event=self.event,
@@ -89,11 +90,14 @@ class CheckinList(LoggedModel):
).count()
@property
@scopes_disabled()
# Disable scopes, because this query is safe and the additional organizer filter in the EXISTS() subquery tricks PostgreSQL into a bad
# subplan that sequentially scans all events
def checkin_count(self):
return self.event.cache.get_or_set(
'checkin_list_{}_checkin_count'.format(self.pk),
lambda: self.positions.annotate(
checkedin=Exists(Checkin.objects.filter(list_id=self.pk, position=OuterRef('pk')))
lambda: self.positions.using(settings.DATABASE_REPLICA).annotate(
checkedin=Exists(Checkin.objects.filter(list_id=self.pk, position=OuterRef('pk'), type=Checkin.TYPE_ENTRY,))
).filter(
checkedin=True
).count(),

View File

@@ -6,6 +6,7 @@ from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES
from pretix.base.models import LoggedModel
@@ -74,6 +75,13 @@ class Device(LoggedModel):
max_length=190,
null=True, blank=True
)
security_profile = models.CharField(
max_length=190,
choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()],
default='full',
null=True,
blank=False
)
objects = ScopedManager(organizer='organizer')

View File

@@ -12,7 +12,7 @@ from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery, Value
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
@@ -89,7 +89,7 @@ class EventMixin:
self.date_from.astimezone(tz), "TIME_FORMAT"
)
def get_date_to_display(self, tz=None, short=False) -> str:
def get_date_to_display(self, tz=None, show_times=True, short=False) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting. Returns an empty string
@@ -100,14 +100,14 @@ class EventMixin:
return ""
return _date(
self.date_to.astimezone(tz),
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
)
def get_date_range_display(self, tz=None, force_show_end=False) -> str:
"""
Returns a formatted string containing the start date and the end date
of the event with respect to the current locale and to the ``show_times`` and
``show_date_to`` settings.
of the event with respect to the current locale and to the ``show_date_to``
setting. Times are not shown.
"""
tz = tz or self.timezone
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
@@ -189,7 +189,9 @@ class EventMixin:
).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
return qs.prefetch_related(
return qs.annotate(
has_paid_item=Exists(Item.objects.filter(event_id=OuterRef(cls._event_id), default_price__gt=0))
).prefetch_related(
Prefetch(
'quotas',
to_attr='active_quotas',
@@ -280,6 +282,7 @@ class Event(EventMixin, LoggedModel):
"""
settings_namespace = 'event'
_event_id = 'pk'
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
testmode = models.BooleanField(default=False)
@@ -375,6 +378,7 @@ class Event(EventMixin, LoggedModel):
self.settings.invoice_include_expire_date = True
self.settings.ticketoutput_pdf__enabled = True
self.settings.ticketoutput_passbook__enabled = True
self.settings.event_list_type = 'calendar'
@property
def social_image(self):
@@ -414,7 +418,7 @@ class Event(EventMixin, LoggedModel):
return super().presale_has_ended
def delete_all_orders(self, really=False):
from .orders import OrderRefund, OrderPayment, OrderPosition, OrderFee
from .orders import OrderFee, OrderPayment, OrderPosition, OrderRefund
if not really:
raise TypeError("Pass really=True as a parameter.")
@@ -501,8 +505,10 @@ class Event(EventMixin, LoggedModel):
), tz)
def copy_data_from(self, other):
from . import ItemAddOn, ItemCategory, Item, Question, Quota, ItemMetaValue
from ..signals import event_copy_data
from . import (
Item, ItemAddOn, ItemCategory, ItemMetaValue, Question, Quota,
)
self.plugins = other.plugins
self.is_public = other.is_public
@@ -783,7 +789,12 @@ class Event(EventMixin, LoggedModel):
'name_ascending': ('name', 'date_from'),
'name_descending': ('-name', 'date_from'),
}[ordering]
subevs = queryset.filter(
subevs = queryset.annotate(
has_paid_item=Value(
self.cache.get_or_set('has_paid_item', lambda: self.items.filter(default_price__gt=0).exists(), 3600),
output_field=models.BooleanField()
)
).filter(
Q(active=True) & Q(is_public=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
| Q(date_to__gte=now() - timedelta(hours=24))
@@ -882,6 +893,7 @@ class Event(EventMixin, LoggedModel):
def delete_sub_objects(self):
self.cartposition_set.filter(addon_to__isnull=False).delete()
self.cartposition_set.all().delete()
self.vouchers.all().delete()
self.items.all().delete()
self.subevents.all().delete()
@@ -976,6 +988,7 @@ class SubEvent(EventMixin, LoggedModel):
:type location: str
"""
_event_id = 'event_id'
event = models.ForeignKey(Event, related_name="subevents", on_delete=models.PROTECT)
active = models.BooleanField(default=False, verbose_name=_("Active"),
help_text=_("Only with this checkbox enabled, this date is visible in the "
@@ -1060,21 +1073,35 @@ class SubEvent(EventMixin, LoggedModel):
return self.event.settings
@cached_property
def item_price_overrides(self):
def item_overrides(self):
from .items import SubEventItem
return {
si.item_id: si.price
for si in SubEventItem.objects.filter(subevent=self, price__isnull=False)
si.item_id: si
for si in SubEventItem.objects.filter(subevent=self)
}
@cached_property
def var_price_overrides(self):
def var_overrides(self):
from .items import SubEventItemVariation
return {
si.variation_id: si
for si in SubEventItemVariation.objects.filter(subevent=self)
}
@property
def item_price_overrides(self):
return {
si.item_id: si.price
for si in self.item_overrides.values() if si.price is not None
}
@property
def var_price_overrides(self):
return {
si.variation_id: si.price
for si in SubEventItemVariation.objects.filter(subevent=self, price__isnull=False)
for si in self.var_overrides.values() if si.price is not None
}
@property
@@ -1096,12 +1123,28 @@ class SubEvent(EventMixin, LoggedModel):
if self.event and clear_cache:
self.event.cache.clear()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__original_dates = (self.date_from, self.date_to)
def save(self, *args, **kwargs):
from .orders import Order
clear_cache = kwargs.pop('clear_cache', False)
super().save(*args, **kwargs)
if self.event and clear_cache:
self.event.cache.clear()
if (self.date_from, self.date_to) != self.__original_dates:
"""
This is required to guarantee a synchronization invariant of our scanning apps.
Our syncing apps throw away order records of subevents more than X days ago, since
they are not interesting for ticket scanning and pose a performance hazard. However,
the app needs to know when a subevent is moved to a date in the future, since that
might require it to re-download and re-store the orders.
"""
Order.objects.filter(all_positions__subevent=self).update(last_modified=now())
@staticmethod
def clean_items(event, items):
for item in items:
@@ -1168,8 +1211,8 @@ class RequiredAction(models.Model):
created = not self.pk
super().save(*args, **kwargs)
if created:
from .log import LogEntry
from ..services.notifications import notify
from .log import LogEntry
logentry = LogEntry.objects.create(
content_object=self,

View File

@@ -9,10 +9,10 @@ from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import pgettext
from django_countries.fields import CountryField
from django_scopes import ScopedManager
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.helpers.countries import FastCountryField
def invoice_filename(instance, filename: str) -> str:
@@ -84,7 +84,7 @@ class Invoice(models.Model):
invoice_from_name = models.CharField(max_length=190, null=True)
invoice_from_zipcode = models.CharField(max_length=190, null=True)
invoice_from_city = models.CharField(max_length=190, null=True)
invoice_from_country = CountryField(null=True)
invoice_from_country = FastCountryField(null=True)
invoice_from_tax_id = models.CharField(max_length=190, null=True)
invoice_from_vat_id = models.CharField(max_length=190, null=True)
invoice_to = models.TextField()
@@ -94,7 +94,7 @@ class Invoice(models.Model):
invoice_to_zipcode = models.CharField(max_length=190, null=True)
invoice_to_city = models.TextField(null=True)
invoice_to_state = models.CharField(max_length=190, null=True)
invoice_to_country = CountryField(null=True)
invoice_to_country = FastCountryField(null=True)
invoice_to_vat_id = models.TextField(null=True)
invoice_to_beneficiary = models.TextField(null=True)
date = models.DateField(default=today)
@@ -116,8 +116,8 @@ class Invoice(models.Model):
objects = ScopedManager(organizer='event__organizer')
@staticmethod
def _to_numeric_invoice_number(number):
return '{:05d}'.format(int(number))
def _to_numeric_invoice_number(number, places):
return ('{:0%dd}' % places).format(int(number))
@property
def full_invoice_from(self):
@@ -173,7 +173,7 @@ class Invoice(models.Model):
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])
def _get_numeric_invoice_number(self):
def _get_numeric_invoice_number(self, c_length):
numeric_invoices = Invoice.objects.filter(
event__organizer=self.event.organizer,
prefix=self.prefix,
@@ -182,7 +182,7 @@ class Invoice(models.Model):
).aggregate(
max=Max('numeric_number')
)['max'] or 0
return self._to_numeric_invoice_number(numeric_invoices + 1)
return self._to_numeric_invoice_number(numeric_invoices + 1, c_length)
def _get_invoice_number_from_order(self):
return '{order}-{count}'.format(
@@ -209,7 +209,7 @@ class Invoice(models.Model):
self.prefix += 'TEST-'
for i in range(10):
if self.event.settings.get('invoice_numbers_consecutive'):
self.invoice_no = self._get_numeric_invoice_number()
self.invoice_no = self._get_numeric_invoice_number(self.event.settings.invoice_numbers_counter_length)
else:
self.invoice_no = self._get_invoice_number_from_order()
try:

View File

@@ -118,6 +118,7 @@ class SubEventItem(models.Model):
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
item = models.ForeignKey('Item', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
disabled = models.BooleanField(default=False, verbose_name=_('Disable product for this date'))
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -145,6 +146,7 @@ class SubEventItemVariation(models.Model):
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
disabled = models.BooleanField(default=False)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -376,9 +378,9 @@ class Item(LoggedModel):
'but only for fixed bundles!')
)
allow_cancel = models.BooleanField(
verbose_name=_('Allow product to be canceled'),
verbose_name=_('Allow product to be canceled or changed'),
default=True,
help_text=_('If this is checked, the usual cancellation settings of this event apply. If this is unchecked, '
help_text=_('If this is checked, the usual cancellation and order change settings of this event apply. If this is unchecked, '
'orders containing this product can not be canceled by users but only by you.')
)
min_per_order = models.IntegerField(
@@ -410,7 +412,8 @@ class Item(LoggedModel):
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=['web']
default=['web'],
blank=True,
)
issue_giftcard = models.BooleanField(
verbose_name=_('This product is a gift card'),
@@ -446,24 +449,32 @@ class Item(LoggedModel):
return self.event.settings.show_quota_left
return self.show_quota_left
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False):
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False):
price = price if price is not None else self.default_price
if not self.tax_rule:
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
rate=Decimal('0.00'), name='')
else:
t = self.tax_rule.tax(price, base_price_is=base_price_is,
currency=currency or self.event.currency)
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
override_tax_rate=override_tax_rate, currency=currency or self.event.currency)
if include_bundled:
for b in self.bundles.all():
if b.designated_price and b.bundled_item.tax_rule_id != self.tax_rule_id:
if b.bundled_variation:
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross',
invoice_address=invoice_address,
currency=currency)
else:
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
compare_price = self.tax_rule.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
bprice = b.bundled_item.tax(b.designated_price * b.count,
invoice_address=invoice_address,
base_price_is='gross',
currency=currency)
compare_price = self.tax_rule.tax(b.designated_price * b.count,
override_tax_rate=override_tax_rate,
invoice_address=invoice_address,
currency=currency)
t.net += bprice.net - compare_price.net
t.tax += bprice.tax - compare_price.tax
t.name = "MIXED!"
@@ -671,23 +682,31 @@ class ItemVariation(models.Model):
def price(self):
return self.default_price if self.default_price is not None else self.item.default_price
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False):
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False, override_tax_rate=None,
invoice_address=None):
price = price if price is not None else self.price
if not self.item.tax_rule:
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
rate=Decimal('0.00'), name='')
else:
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency)
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency,
override_tax_rate=override_tax_rate,
invoice_address=invoice_address)
if include_bundled:
for b in self.item.bundles.all():
if b.designated_price and b.bundled_item.tax_rule_id != self.item.tax_rule_id:
if b.bundled_variation:
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross',
currency=currency,
invoice_address=invoice_address)
else:
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
compare_price = self.item.tax_rule.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross',
currency=currency,
invoice_address=invoice_address)
compare_price = self.item.tax_rule.tax(b.designated_price * b.count, base_price_is='gross',
currency=currency, invoice_address=invoice_address)
t.net += bprice.net - compare_price.net
t.tax += bprice.tax - compare_price.tax
t.name = "MIXED!"
@@ -822,6 +841,10 @@ class ItemAddOn(models.Model):
help_text=_('If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost '
'money individually.')
)
multi_allowed = models.BooleanField(
default=False,
verbose_name=_('Allow the same product to be selected multiple times'),
)
position = models.PositiveIntegerField(
default=0,
verbose_name=_("Position")
@@ -1117,6 +1140,8 @@ class Question(LoggedModel):
return None
if self.type == Question.TYPE_CHOICE:
if isinstance(answer, QuestionOption):
return answer
q = Q(identifier=answer)
if isinstance(answer, int) or answer.isdigit():
q |= Q(pk=answer)
@@ -1131,6 +1156,8 @@ class Question(LoggedModel):
Q(identifier__in=answer.split(","))
))
llen = len(answer.split(','))
elif all(isinstance(o, QuestionOption) for o in answer):
return o
else:
l_ = list(self.options.filter(
Q(pk__in=[a for a in answer if isinstance(a, int) or a.isdigit()]) |
@@ -1333,6 +1360,16 @@ class Quota(LoggedModel):
)
closed = models.BooleanField(default=False)
release_after_exit = models.BooleanField(
verbose_name=_('Allow to sell more tickets once people have checked out'),
help_text=_('With this option, quota will be released as soon as people are scanned at an exit of your event. '
'This will only happen if they have been scanned both at an entry and at an exit and the exit '
'is the more recent scan. It does not matter which check-in list either of the scans was on, '
'but check-in lists are ignored if they are set to "Allow re-entering after an exit scan" to '
'prevent accidental overbooking.'),
default=False,
)
objects = ScopedManager(organizer='event__organizer')
class Meta:

View File

@@ -75,7 +75,10 @@ class LogEntry(models.Model):
@cached_property
def display_object(self):
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event, TaxRule, SubEvent
from . import (
Event, Item, ItemCategory, Order, Question, Quota, SubEvent,
TaxRule, Voucher,
)
try:
if self.content_type.model_class() is Event:

View File

@@ -27,7 +27,7 @@ from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country, CountryField
from django_countries.fields import Country
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
@@ -44,7 +44,7 @@ from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from ...helpers.countries import CachedCountries
from ...helpers.countries import CachedCountries, FastCountryField
from .base import LockModel, LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation, Question, QuestionOption, Quota
@@ -209,7 +209,7 @@ class Order(LockModel, LoggedModel):
return self.full_code
def gracefully_delete(self, user=None, auth=None):
from . import Voucher, GiftCard, GiftCardTransaction
from . import GiftCard, GiftCardTransaction, Voucher
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
@@ -392,13 +392,19 @@ 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)
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if self.event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
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))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if self.event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
elif mode == 'minutes':
exp_by_date = now_dt.astimezone(tz) + timedelta(minutes=self.event.settings.get('payment_term_minutes', as_type=int))
else:
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
self.expires = exp_by_date
@@ -434,6 +440,19 @@ class Order(LockModel, LoggedModel):
self.status in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED) and self.count_positions
)
@cached_property
def user_change_deadline(self):
until = self.event.settings.get('change_allow_user_until', as_type=RelativeDateWrapper)
if until:
if self.event.has_subevents:
terms = [
until.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
]
return min(terms) if terms else None
else:
return until.datetime(self.event)
@cached_property
def user_cancel_deadline(self):
if self.status == Order.STATUS_PAID and self.total != Decimal('0.00'):
@@ -466,6 +485,40 @@ class Order(LockModel, LoggedModel):
fee += self.event.settings.cancel_allow_user_paid_keep
return round_decimal(fee, self.event.currency)
@property
@scopes_disabled()
def user_change_allowed(self) -> bool:
"""
Returns whether or not this order can be canceled by the user.
"""
from .checkin import Checkin
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID) or not self.count_positions:
return False
if self.cancellation_requests.exists():
return False
if self.require_approval:
return False
positions = list(
self.positions.all().annotate(
has_variations=Exists(ItemVariation.objects.filter(item_id=OuterRef('item_id'))),
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
).select_related('item').prefetch_related('issued_gift_cards')
)
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
if not cancelable or not positions:
return False
for op in positions:
if op.issued_gift_cards.all():
return False
if self.user_change_deadline and now() > self.user_change_deadline:
return False
return self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])
@property
@scopes_disabled()
def user_cancel_allowed(self) -> bool:
@@ -474,7 +527,7 @@ class Order(LockModel, LoggedModel):
"""
from .checkin import Checkin
if self.cancellation_requests.exists():
if self.cancellation_requests.exists() or not self.cancel_allowed():
return False
positions = list(
self.positions.all().annotate(
@@ -502,10 +555,16 @@ class Order(LockModel, LoggedModel):
# Algorithm to choose which payments are to be refunded to create the least hassle
payments = payments or self.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED)
for p in payments:
p.full_refund_possible = p.payment_provider.payment_refund_supported(p)
p.partial_refund_possible = p.payment_provider.payment_partial_refund_supported(p)
p.propose_refund = Decimal('0.00')
p.available_amount = p.amount - p.refunded_amount
if p.payment_provider:
p.full_refund_possible = p.payment_provider.payment_refund_supported(p)
p.partial_refund_possible = p.payment_provider.payment_partial_refund_supported(p)
p.propose_refund = Decimal('0.00')
p.available_amount = p.amount - p.refunded_amount
else:
p.full_refund_possible = False
p.partial_refund_possible = False
p.propose_refund = Decimal('0.00')
p.available_amount = Decimal('0.00')
unused_payments = set(p for p in payments if p.full_refund_possible or p.partial_refund_possible)
to_refund = amount
@@ -789,7 +848,9 @@ class Order(LockModel, LoggedModel):
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
from pretix.base.services.mail import (
SendMailException, TolerantDict, mail, render_mail,
)
if not self.email:
return
@@ -804,6 +865,7 @@ class Order(LockModel, LoggedModel):
try:
email_content = render_mail(template, context)
subject = subject.format_map(TolerantDict(context))
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
@@ -1070,7 +1132,7 @@ class AbstractPosition(models.Model):
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
country = CountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
country = FastCountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
class Meta:
@@ -1186,7 +1248,7 @@ class AbstractPosition(models.Model):
self.attendee_name,
self.company,
self.street,
self.zipcode + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name
]
lines = [r.strip() for r in lines if r]
@@ -1371,7 +1433,9 @@ class OrderPayment(models.Model):
:type mail_text: str
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
from pretix.base.services.invoices import generate_invoice, invoice_qualified
from pretix.base.services.invoices import (
generate_invoice, invoice_qualified,
)
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
@@ -1441,7 +1505,7 @@ class OrderPayment(models.Model):
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
)
if send_mail:
if send_mail and self.order.sales_channel in self.order.event.settings.mail_sales_channel_placed_paid:
self._send_paid_mail(invoice, user, mail_text)
if self.order.event.settings.mail_send_order_paid_attendee:
for p in self.order.positions.all():
@@ -1794,7 +1858,10 @@ class OrderFee(models.Model):
self.fee_type, self.value
)
def _calculate_tax(self):
def _calculate_tax(self, tax_rule=None):
if tax_rule:
self.tax_rule = tax_rule
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
@@ -1804,18 +1871,17 @@ class OrderFee(models.Model):
self.tax_rule = self.order.event.settings.tax_rate_default
if self.tax_rule:
if self.tax_rule.tax_applicable(ia):
tax = self.tax_rule.tax(self.value, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia)
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
def save(self, *args, **kwargs):
if self.tax_rule and not self.tax_rule.rate and not self.tax_rule.pk:
self.tax_rule = None
if self.tax_rate is None:
self._calculate_tax()
self.order.touch()
@@ -1950,20 +2016,16 @@ class OrderPosition(AbstractPosition):
self.item.id, self.variation.id if self.variation else 0, self.order_id
)
def _calculate_tax(self):
self.tax_rule = self.item.tax_rule
def _calculate_tax(self, tax_rule=None):
self.tax_rule = tax_rule or self.item.tax_rule
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
if self.tax_rule:
if self.tax_rule.tax_applicable(ia):
tax = self.tax_rule.tax(self.price, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
@@ -2021,7 +2083,9 @@ class OrderPosition(AbstractPosition):
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
from pretix.base.services.mail import (
SendMailException, mail, render_mail,
)
if not self.attendee_email:
return
@@ -2102,6 +2166,10 @@ class CartPosition(AbstractPosition):
includes_tax = models.BooleanField(
default=True
)
override_tax_rate = models.DecimalField(
max_digits=10, decimal_places=2,
null=True, blank=True
)
is_bundled = models.BooleanField(default=False)
objects = ScopedManager(organizer='event__organizer')
@@ -2118,6 +2186,8 @@ class CartPosition(AbstractPosition):
@property
def tax_rate(self):
if self.includes_tax:
if self.override_tax_rate is not None:
return self.override_tax_rate
return self.item.tax(self.price, base_price_is='gross').rate
else:
return Decimal('0.00')
@@ -2125,7 +2195,7 @@ class CartPosition(AbstractPosition):
@property
def tax_value(self):
if self.includes_tax:
return self.item.tax(self.price, base_price_is='gross').tax
return self.item.tax(self.price, override_tax_rate=self.override_tax_rate, base_price_is='gross').tax
else:
return Decimal('0.00')
@@ -2141,8 +2211,8 @@ class InvoiceAddress(models.Model):
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))

View File

@@ -113,7 +113,7 @@ class Organizer(LoggedModel):
), tz)
def allow_delete(self):
from . import Order, Invoice
from . import Invoice, Order
return (
not Order.objects.filter(event__organizer=self).exists() and
not Invoice.objects.filter(event__organizer=self).exists() and

View File

@@ -42,7 +42,7 @@ class SeatingPlan(LoggedModel):
layout = models.TextField(validators=[SeatingPlanLayoutValidator()])
Category = namedtuple('Categrory', 'name')
RawSeat = namedtuple('Seat', 'name guid number row category zone sorting_rank row_label seat_label x y')
RawSeat = namedtuple('Seat', 'guid number row category zone sorting_rank row_label seat_label x y')
def __str__(self):
return self.name
@@ -95,7 +95,6 @@ class SeatingPlan(LoggedModel):
yield self.RawSeat(
number=s['seat_number'],
guid=s['seat_guid'],
name='{} {}'.format(r['row_number'], s['seat_number']), # TODO: Zone? Variable scheme?
row=r['row_number'],
row_label=row_label,
seat_label=seat_label,
@@ -125,7 +124,6 @@ class Seat(models.Model):
"""
event = models.ForeignKey(Event, related_name='seats', on_delete=models.CASCADE)
subevent = models.ForeignKey(SubEvent, null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
name = models.CharField(max_length=190)
zone_name = models.CharField(max_length=190, blank=True, default="")
row_name = models.CharField(max_length=190, blank=True, default="")
row_label = models.CharField(max_length=190, null=True)
@@ -141,6 +139,10 @@ class Seat(models.Model):
class Meta:
ordering = ['sorting_rank', 'seat_guid']
@property
def name(self):
return str(self)
def __str__(self):
parts = []
if self.zone_name:
@@ -157,13 +159,13 @@ class Seat(models.Model):
parts.append(gettext('Seat {number}').format(number=self.seat_number))
if not parts:
return self.name
return self.seat_guid
return ', '.join(parts)
@classmethod
def annotated(cls, qs, event_id, subevent, ignore_voucher_id=None, minimal_distance=0,
ignore_order_id=None, ignore_cart_id=None, distance_only_within_row=False):
from . import Order, OrderPosition, Voucher, CartPosition
from . import CartPosition, Order, OrderPosition, Voucher
vqs = Voucher.objects.filter(
event_id=event_id,
@@ -253,15 +255,18 @@ class Seat(models.Model):
ignore_order_id=ignore_orderpos.order_id if ignore_orderpos else None,
ignore_cart_id=(
distance_ignore_cart_id or
(ignore_cart.cart_id if ignore_cart else None)
(ignore_cart.cart_id if ignore_cart and ignore_cart is not True else None)
))
q = Q(has_order=True) | Q(has_voucher=True)
if ignore_cart is not True:
q |= Q(has_cart=True)
qs_closeby_taken = qs_annotated.annotate(
distance=(
Power(F('x') - Value(self.x), Value(2), output_field=models.FloatField()) +
Power(F('y') - Value(self.y), Value(2), output_field=models.FloatField())
)
).exclude(pk=self.pk).filter(
Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True),
q,
distance__lt=self.event.settings.seating_minimal_distance ** 2
)
if self.event.settings.seating_distance_within_row:

View File

@@ -5,12 +5,12 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
from i18nfield.fields import I18nCharField
from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
from pretix.base.templatetags.money import money_filter
from pretix.helpers.countries import FastCountryField
class TaxedPrice:
@@ -116,7 +116,7 @@ class TaxRule(LoggedModel):
"ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax "
"calculation. USE AT YOUR OWN RISK.")
)
home_country = CountryField(
home_country = FastCountryField(
verbose_name=_('Merchant country'),
blank=True,
help_text=_('Your country of residence. This is the country the EU reverse charge rule will not apply in, '
@@ -164,16 +164,41 @@ class TaxRule(LoggedModel):
def has_custom_rules(self):
return self.custom_rules and self.custom_rules != '[]'
def tax(self, base_price, base_price_is='auto', currency=None):
def tax_rate_for(self, invoice_address):
if not self._tax_applicable(invoice_address):
return Decimal('0.00')
if self.has_custom_rules:
rule = self.get_matching_rule(invoice_address)
if rule.get('action', 'vat') == 'vat' and rule.get('rate') is not None:
return Decimal(rule.get('rate'))
return Decimal(self.rate)
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None):
from .event import Event
try:
currency = currency or self.event.currency
except Event.DoesNotExist:
pass
if self.rate == Decimal('0.00'):
rate = Decimal(self.rate)
if override_tax_rate is not None:
rate = override_tax_rate
elif invoice_address:
adjust_rate = self.tax_rate_for(invoice_address)
if adjust_rate == gross_price_is_tax_rate and base_price_is == 'gross':
rate = adjust_rate
elif adjust_rate != rate:
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
base_price = normal_price.net
base_price_is = 'net'
subtract_from_gross = Decimal('0.00')
rate = adjust_rate
if rate == Decimal('0.00'):
return TaxedPrice(
net=base_price, gross=base_price, tax=Decimal('0.00'),
rate=self.rate, name=self.name
net=base_price - subtract_from_gross, gross=base_price - subtract_from_gross, tax=Decimal('0.00'),
rate=rate, name=self.name
)
if base_price_is == 'auto':
@@ -183,19 +208,27 @@ class TaxRule(LoggedModel):
base_price_is = 'net'
if base_price_is == 'gross':
gross = base_price
net = round_decimal(gross - (base_price * (1 - 100 / (100 + self.rate))),
if base_price >= Decimal('0.00'):
# For positive prices, make sure they don't go negative because of bundles
gross = max(Decimal('0.00'), base_price - subtract_from_gross)
else:
# If the price is already negative, we don't really care any more
gross = base_price - subtract_from_gross
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
elif base_price_is == 'net':
net = base_price
gross = round_decimal((net * (1 + self.rate / 100)),
currency)
gross = round_decimal((net * (1 + rate / 100)), currency)
if subtract_from_gross:
gross -= subtract_from_gross
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
else:
raise ValueError('Unknown base price type: {}'.format(base_price_is))
return TaxedPrice(
net=net, gross=gross, tax=gross - net,
rate=self.rate, name=self.name
rate=rate, name=self.name
)
@property
@@ -243,7 +276,7 @@ class TaxRule(LoggedModel):
return False
def tax_applicable(self, invoice_address):
def _tax_applicable(self, invoice_address):
if self._custom_rules:
rule = self.get_matching_rule(invoice_address)
return rule.get('action', 'vat') == 'vat'

View File

@@ -1,4 +1,5 @@
import re
from collections import defaultdict
from decimal import Decimal, DecimalException
import pycountry
@@ -12,11 +13,13 @@ from django.utils.translation import (
)
from django_countries import countries
from django_countries.fields import Country
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.questions import guess_country
from pretix.base.models import (
ItemVariation, OrderPosition, QuestionAnswer, QuestionOption, Seat,
ItemVariation, OrderPosition, Question, QuestionAnswer, QuestionOption,
Seat,
)
from pretix.base.services.pricing import get_price
from pretix.base.settings import (
@@ -417,7 +420,7 @@ class AttendeeStreet(ImportColumn):
return _('Attendee address') + ': ' + _('Address')
def assign(self, value, order, position, invoice_address, **kwargs):
position.address = value or ''
position.street = value or ''
class AttendeeZip(ImportColumn):
@@ -528,7 +531,7 @@ class Secret(ImportColumn):
super().__init__(*args)
def clean(self, value, previous_values):
if value and (value in self._cached or OrderPosition.all.filter(order__event=self.event, secret=value).exists()):
if value and (value in self._cached or OrderPosition.all.filter(order__event__organizer=self.event.organizer, secret=value).exists()):
raise ValidationError(
_('You cannot assign a position secret that already exists.')
)
@@ -626,6 +629,22 @@ class Comment(ImportColumn):
class QuestionColumn(ImportColumn):
def __init__(self, event, q):
self.q = q
self.option_resolve_cache = defaultdict(set)
for opt in q.options.all():
self.option_resolve_cache[str(opt.id)].add(opt)
self.option_resolve_cache[opt.identifier].add(opt)
if isinstance(opt.answer, LazyI18nString):
if isinstance(opt.answer.data, dict):
for v in opt.answer.data.values():
self.option_resolve_cache[v.strip()].add(opt)
else:
self.option_resolve_cache[opt.answer.strip()].add(opt)
else:
self.option_resolve_cache[opt.answer.strip()].add(opt)
super().__init__(event)
@property
@@ -638,7 +657,23 @@ class QuestionColumn(ImportColumn):
def clean(self, value, previous_values):
if value:
return self.q.clean_answer(value)
if self.q.type == Question.TYPE_CHOICE:
if value not in self.option_resolve_cache:
raise ValidationError(_('Invalid option selected.'))
if len(self.option_resolve_cache[value]) > 1:
raise ValidationError(_('Ambigous option selected.'))
return list(self.option_resolve_cache[value])[0]
elif self.q.type == Question.TYPE_CHOICE_MULTIPLE:
values = value.split(',')
if any(v.strip() not in self.option_resolve_cache for v in values):
raise ValidationError(_('Invalid option selected.'))
if any(len(self.option_resolve_cache[v.strip()]) > 1 for v in values):
raise ValidationError(_('Ambigous option selected.'))
return [list(self.option_resolve_cache[v.strip()])[0] for v in values]
else:
return self.q.clean_answer(value)
def assign(self, value, order, position, invoice_address, **kwargs):
if value:
@@ -702,7 +737,7 @@ def get_all_columns(event):
SeatColumn(event),
Comment(event)
]
for q in event.questions.exclude(type='F'):
for q in event.questions.prefetch_related('options').exclude(type=Question.TYPE_FILE):
default.append(QuestionColumn(event, q))
for recv, resp in order_import_columns.send(sender=event):

View File

@@ -49,6 +49,7 @@ class PaymentProviderForm(Form):
val = cleaned_data.get(k)
if v._required and not val:
self.add_error(k, _('This field is required.'))
return cleaned_data
class BasePaymentProvider:
@@ -167,13 +168,23 @@ class BasePaymentProvider:
@property
def abort_pending_allowed(self) -> bool:
"""
Whether or not a user can abort a payment in pending start to switch to another
Whether or not a user can abort a payment in pending state to switch to another
payment method. This returns ``False`` by default which is no guarantee that
aborting a pending payment can never happen, it just hides the frontend button
to avoid users accidentally committing double payments.
"""
return False
@property
def requires_invoice_immediately(self):
"""
Return whether this payment method requires an invoice to exist for an order, even though the event
is configured to only create invoices for paid orders.
By default this is False, but it might be overwritten for e.g. bank transfer.
`execute_payment` is called after the invoice is created.
"""
return False
@property
def settings_form_fields(self) -> dict:
"""
@@ -306,7 +317,7 @@ class BasePaymentProvider:
initial=['web'],
widget=forms.CheckboxSelectMultiple,
help_text=_(
'Only allow the usage of this payment provider in the following sales channels'),
'Only allow the usage of this payment provider in the selected sales channels.'),
)),
('_hidden',
forms.BooleanField(
@@ -374,6 +385,8 @@ class BasePaymentProvider:
"""
return {}
payment_form_class = PaymentProviderForm
def payment_form(self, request: HttpRequest) -> Form:
"""
This is called by the default implementation of :py:meth:`payment_form_render`
@@ -386,7 +399,7 @@ class BasePaymentProvider:
``PaymentProviderForm`` (from this module) that handles some nasty issues about
required fields for you.
"""
form = PaymentProviderForm(
form = self.payment_form_class(
data=(request.POST if request.method == 'POST' and request.POST.get("payment") == self.identifier else None),
prefix='payment_%s' % self.identifier,
initial={
@@ -767,7 +780,7 @@ class BasePaymentProvider:
def matching_id(self, payment: OrderPayment):
"""
Will be called to get an ID for a matching this payment when comparing pretix records with records of an external
Will be called to get an ID for matching this payment when comparing pretix records with records of an external
source. This should return the main transaction ID for your API.
:param payment: The payment in question.
@@ -902,7 +915,7 @@ class ManualPayment(BasePaymentProvider):
label=_('Payment process description in order confirmation emails'),
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
'mails. It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
'the placeholders {order}, {total}, {currency} and {total_with_currency}.'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
)),
@@ -910,7 +923,7 @@ class ManualPayment(BasePaymentProvider):
label=_('Payment process description for pending orders'),
help_text=_('This text will be shown on the order confirmation page for pending orders. '
'It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
'the placeholders {order}, {total}, {currency} and {total_with_currency}.'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
)),
@@ -1062,7 +1075,10 @@ class GiftCardPayment(BasePaymentProvider):
def api_payment_details(self, payment: OrderPayment):
from .models import GiftCard
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
try:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
except GiftCard.DoesNotExist:
return {}
return {
'gift_card': {
'id': gc.pk,
@@ -1118,7 +1134,7 @@ class GiftCardPayment(BasePaymentProvider):
cart['raw']
)
total += sum([f.value for f in fees])
remainder = total - gc.value
remainder = total
if remainder > Decimal('0.00'):
del cs['payment']
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
@@ -1199,7 +1215,7 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.accepted_by(self.event.organizer): # noqa - just a safeguard
raise PaymentException(_("This gift card is not accepted by this event organizer."))
if payment.amount > gc.value: # noqa - just a safeguard
raise PaymentException(_("This gift card was used in the meantime. Please try again"))
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
if gc.expires and gc.expires < now(): # noqa - just a safeguard
messages.error(request, _("This gift card is no longer valid."))
return

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