Compare commits

...

224 Commits

Author SHA1 Message Date
Raphael Michel
31d1fc31cd Bump version to 3.8.0 2020-04-17 17:19:02 +02:00
Raphael Michel
597211d83a Merge pull request #1646 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-04-17 17:11:24 +02:00
Raphael Michel
17679d4304 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3609 of 3609 strings)

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

powered by weblate
2020-04-17 17:11:08 +02:00
Raphael Michel
0fb70c78a9 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3609 of 3609 strings)

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

powered by weblate
2020-04-17 17:11:07 +02:00
Raphael Michel
e254e90e49 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-04-17 16:26:52 +02:00
Raphael Michel
9c6e5f025d Merge pull request #1642 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-04-17 16:26:21 +02:00
Raphael Michel
3c86532218 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate
2020-04-17 16:25:29 +02:00
Raphael Michel
3834ae566f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate
2020-04-17 16:25:29 +02:00
Raphael Michel
6766b2b19e Fix #1645 -- Allow one-letter event slugs 2020-04-17 16:25:04 +02:00
Raphael Michel
b6d2f67c7c Cache sorting of countries 2020-04-17 13:21:13 +02:00
Raphael Michel
e70f593a94 Minor SQL performance improvements 2020-04-17 12:14:37 +02:00
Raphael Michel
ed5726fc0c Merge branch 'master' of github.com:pretix/pretix 2020-04-17 12:04:41 +02:00
Martin Gross
5400d26c60 Log the reason for failed PayPal refunds 2020-04-17 12:02:43 +02:00
Raphael Michel
0bb6104532 Flip order of invoices and tickets in email attachments 2020-04-16 13:14:06 +02:00
Raphael Michel
16aa403735 Fix incorrect checkin list tests 2020-04-15 13:06:24 +02:00
Martin Gross
1c279a92a7 Merge pull request #1643 from pretix/event_cancellation_giftcard_refund
Allow to issue gift card refunds when cancelling whole events
2020-04-15 13:00:41 +02:00
Martin Gross
35985dcb11 Remove legal warning, make refund_as_giftcard-option clearer. 2020-04-15 12:58:43 +02:00
Raphael Michel
b0dcbe31fa Fix incorrect quota error when changing subevent and item of a position 2020-04-15 12:54:57 +02:00
Martin Gross
b3c3ee3b22 Allow to issue gift card refunds when cancelling whole events 2020-04-15 10:08:12 +02:00
Raphael Michel
aab340fd87 Fix missing translation context 2020-04-15 09:43:58 +02:00
Raphael Michel
1871324ef4 Restrict length of item name in quick setup
PRETIXEU-21V
2020-04-15 09:20:55 +02:00
Raphael Michel
d799d560b7 Fix typo in settings definition 2020-04-14 09:47:47 +02:00
Raphael Michel
01e2851a76 Invoice model: Do not crash on invalid states 2020-04-14 09:47:34 +02:00
Raphael Michel
ef2a4244ed Merge branch 'master' of github.com:pretix/pretix 2020-04-09 09:29:58 +02:00
Raphael Michel
55539dc8e5 Fix confusing background color in subevents 2020-04-09 09:29:40 +02:00
Martin Gross
ef303bfcc4 Add cancel_allow_user_paid_adjust_fees_explanation (#1639)
* Add cancel_allow_user_paid_adjust_fees_explanation

* Cleanup
2020-04-08 17:49:36 +02:00
Martin Gross
fff9ac04a9 Fix test (Caused by 31fdf8721b) 2020-04-08 17:49:16 +02:00
Martin Gross
76d27fbfaa Cleanup 2020-04-08 17:08:58 +02:00
Martin Gross
2b1123b487 Add cancel_allow_user_paid_adjust_fees_explanation 2020-04-08 16:43:29 +02:00
pretix translation bot
3607d8706d Translations update from Weblate (#1638)
* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (German (informal))

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch (informal))

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch (informal))

Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate

Co-authored-by: Martin Gross <martin@pc-coholic.de>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2020-04-08 15:48:55 +02:00
Raphael Michel
31fdf8721b API: Fix selecting checkin question answers by option identifier 2020-04-07 15:16:58 +02:00
Raphael Michel
128a1f349a Revert accidentally commited change 2020-04-03 14:47:44 +02:00
Raphael Michel
7d432f0639 Improve display of attendee addresses 2020-04-03 14:35:38 +02:00
Raphael Michel
1ffc799c4d Split banner text into top and bottom 2020-04-03 13:02:23 +02:00
Raphael Michel
25dd8f2e2f Introduce banner text 2020-04-02 18:35:28 +02:00
Raphael Michel
b121596e4b Merge pull request #1635 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-04-02 18:28:50 +02:00
Raphael Michel
cf835df62e Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3601 of 3601 strings)

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

powered by weblate
2020-04-02 18:28:27 +02:00
Raphael Michel
7a3b7d4f02 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3601 of 3601 strings)

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

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

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

powered by weblate
2020-04-02 18:17:28 +02:00
Raphael Michel
06de74d877 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-04-02 18:17:18 +02:00
Raphael Michel
2ae9e3e0d9 Directly load editor on supported browsers 2020-04-02 18:16:33 +02:00
Raphael Michel
0c0fe58bbf Improve UX of ticket download settings 2020-04-02 18:16:22 +02:00
Raphael Michel
7b1e1a48ef Enable PDF and passbook outputs for new events by default 2020-04-02 18:03:26 +02:00
Raphael Michel
c7dd50de0d CartMixin: Prevent None values in sorting function 2020-04-02 16:53:17 +02:00
Raphael Michel
a1caa65776 Revert "Upgrade jQuery version (but keep old one around for now for plugins)"
We've got to many instances of this around:
https://stackoverflow.com/questions/38871753/uncaught-typeerror-a-indexof-is-not-a-function-error-when-opening-new-foundat

This reverts commit cc46d55f5e.
2020-04-02 16:42:54 +02:00
Raphael Michel
260973345d Merge pull request #1634 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-04-02 14:41:19 +02:00
Raphael Michel
2c9b2620ea Add company and address fields to attendees (#1633)
* Add company and address fields to attendees

* Update src/pretix/control/templates/pretixcontrol/event/settings.html

Co-Authored-By: Martin Gross <gross@rami.io>

Co-authored-by: Martin Gross <gross@rami.io>
2020-04-02 14:41:09 +02:00
Maarten van den Berg
909c80e710 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate
2020-04-02 09:51:33 +02:00
Maarten van den Berg
5a218ae6a9 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3590 of 3590 strings)

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

powered by weblate
2020-04-02 09:51:33 +02:00
Raphael Michel
b498d45621 Pass gift_cards to order_fee_calculation 2020-04-02 09:50:44 +02:00
Raphael Michel
b02196434b Re-compute fees after applying a giftcard 2020-04-01 17:09:58 +02:00
Raphael Michel
c0edce7760 AJAX: Do not throw error on "abort" in Safari 2020-04-01 16:06:46 +02:00
Raphael Michel
cc46d55f5e Upgrade jQuery version (but keep old one around for now for plugins) 2020-04-01 16:06:46 +02:00
Martin Gross
ea8abb8dab Add Giftcard Redemption Export 2020-04-01 15:46:40 +02:00
Raphael Michel
f765d094b4 Fix crash in UCBrowser (PRETIXEU-20M) 2020-04-01 11:14:30 +02:00
Raphael Michel
86f222870d Cancelling orders: Do not send email to organizer if the refund is in transit 2020-03-31 17:56:51 +02:00
Martin Gross
19b5270d76 Fix tests (Caused by 61a1368ed2) 2020-03-31 14:34:10 +02:00
Martin Gross
db76b9b0ef Tiny fix to make Edge happy and display cancellation-slider 2020-03-31 14:04:57 +02:00
pretix translation bot
d23e53873f Translations update from Weblate (#1632)
* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (German (informal))

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

Co-authored-by: Martin Gross <martin@pc-coholic.de>
2020-03-31 13:35:14 +02:00
pretix translation bot
c116a4b998 Translations update from Weblate (#1631)
* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (German (informal))

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch (informal))

Currently translated at 100.0% (106 of 106 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 99.4% (3569 of 3589 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (3589 of 3589 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch (informal))

Currently translated at 100.0% (3589 of 3589 strings)

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

powered by weblate

Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2020-03-31 13:28:34 +02:00
Martin Gross
2471d4bca5 Update po files
[CI skip]

Signed-off-by: Martin Gross <gross@rami.io>
2020-03-31 12:47:09 +02:00
Martin Gross
8e04dbdcca Change "we keep" to "The organizer keeps" to avoid confusion 2020-03-31 12:45:32 +02:00
Raphael Michel
0928358396 Fix > vs >= in gift card message 2020-03-29 13:53:58 +02:00
Raphael Michel
23f783c15c Force django-libsass version to be upgraded 2020-03-26 20:51:54 +01:00
Raphael Michel
edae96c84f Fix TypeError during cancellation 2020-03-26 20:49:53 +01:00
Martin Gross
242ebdfae9 Show Subevent start time in select2-pickers (#1630)
* Add Subevent time to __str__

* Show subevent-dates in select2 picker

* Show event-dateblock (if enabled) on Widget Voucher redemption page

* Update src/pretix/base/models/event.py

Co-Authored-By: Raphael Michel <michel@rami.io>

* Update src/pretix/control/templates/pretixcontrol/vouchers/index.html

Co-Authored-By: Raphael Michel <michel@rami.io>

* Update src/pretix/control/views/typeahead.py

Co-Authored-By: Raphael Michel <michel@rami.io>

* Remove date-block on non-subevent voucher redemption pages

Co-authored-by: Raphael Michel <michel@rami.io>
2020-03-26 13:10:45 +01:00
Raphael Michel
0ee502abec Improve performance by not re-evaluating main context processors on
every template rendering in a signal receiver
2020-03-26 09:50:58 +01:00
Raphael Michel
29cb1e93d8 Reduce number of queries on the order change form 2020-03-26 09:50:14 +01:00
Raphael Michel
c89242855c Reduce number of SQL queries on order detail page 2020-03-26 09:38:56 +01:00
Raphael Michel
61a1368ed2 Widget: Show date and time of subevent after calendar selection 2020-03-25 17:48:24 +01:00
Raphael Michel
ac3e00fa03 Color buttons green if they otherwise look like a headline 2020-03-25 17:48:09 +01:00
Raphael Michel
d9d0f7b6f3 Add Order.cancellation_date (#1629)
* Add Order.cancellation_date

* Add tests
2020-03-25 16:37:34 +01:00
Raphael Michel
ad5e2df3be Merge pull request #1628 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-25 14:59:07 +01:00
Raphael Michel
ec34561815 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3589 of 3589 strings)

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

powered by weblate
2020-03-25 14:58:32 +01:00
Raphael Michel
e1540b1648 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3589 of 3589 strings)

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

powered by weblate
2020-03-25 14:58:31 +01:00
Raphael Michel
a6b265455d Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-03-25 14:14:28 +01:00
Raphael Michel
8a6334bd86 Introduce cancellation requests (#1627)
* Allow to adjust the cancellation fee without JS

* Introduce cancellation requests

* ignore→delete

* Change a few things after Martin's review

* Add a few tests
2020-03-25 14:13:55 +01:00
Raphael Michel
173a23722a Merge pull request #1625 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-25 14:12:26 +01:00
Raphael Michel
ab8eb2a34d Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3546 of 3546 strings)

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

powered by weblate
2020-03-25 13:03:51 +01:00
pajowu
30dcda616b Send checkin list mapping in event_copy_data signal (#1624)
* Send checkin list map in event_copy_data signal

* Add checkin_list_map to documentation and definition of event_copy_data
2020-03-25 13:03:45 +01:00
Raphael Michel
3eafec9d6e Allow customers to choose to receive their refund as a gift card (#1626)
* Minor text adjustments

* Allow users to receive their cancellation as a gift card
2020-03-25 11:41:40 +01:00
Raphael Michel
a5910016fd Allow users to increase cancellation fees (#1622)
* Allow users to increase cancellation fees

* Fix typo
2020-03-25 10:11:29 +01:00
pajowu
0a49b93b26 Render remaining badges onto new page even if it doesn't fill t… (#1621) 2020-03-24 17:43:58 +01:00
Raphael Michel
7449bea836 Add request argument to order_info/position_info signals 2020-03-24 15:57:00 +01:00
Raphael Michel
0fc4478332 Add docs on pretix-digital 2020-03-24 15:56:51 +01:00
Raphael Michel
0df4a6e7ed Add signals order_info_top and position_info_top 2020-03-24 14:22:02 +01:00
Benjamin Hättasch
a37cd380c8 Fix sample service config (#1620) 2020-03-24 09:10:56 +01:00
Raphael Michel
11b2bd8887 Allow markdown in multiple-choice question options 2020-03-23 17:55:52 +01:00
Raphael Michel
8986db0975 Merge pull request #1619 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-23 17:55:50 +01:00
Raphael Michel
2921611cb1 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3546 of 3546 strings)

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

powered by weblate
2020-03-23 17:55:20 +01:00
Raphael Michel
785fb29513 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3546 of 3546 strings)

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

powered by weblate
2020-03-23 17:55:19 +01:00
Raphael Michel
81c3d7fa17 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-03-23 17:02:11 +01:00
Raphael Michel
8ff963698d Text change "Modify" → "Add or remove tickets" on review order page 2020-03-23 16:47:25 +01:00
Raphael Michel
6da63e0169 Question form: Guess default country 2020-03-23 16:44:37 +01:00
Raphael Michel
f84903ae27 Drop removed context attribute from from_db_value 2020-03-23 16:44:37 +01:00
Raphael Michel
a0a7859b33 Update redis 2020-03-23 16:03:19 +01:00
Raphael Michel
af23d6e4bf Upgrade to Django 3.0 and other dependencies (#1568)
* Upgrade Django to 3.0 and other dependencies to recent versions

* Fix otp version contsraint

* Remove six dependency

* Resolve some warnings

* Fix failing tests

* Update django-countries

* Resolve all RemovedInDjango31Warnings in test suite

* Run isort

* Fix import

* Update PostgreSQL version on travis
2020-03-23 15:02:20 +01:00
Raphael Michel
7e9c9beace Allow to use a custom domain per event (#1617)
* Drop support for maindomain_urls/subdomain_urls in plugins

* Allow to use a custom domain per event

* Fix bug when manually saving domains

* Fix custom domains in debugging

* Fix middleware

* Fix middleware again, update docs
2020-03-23 13:03:14 +01:00
Raphael Michel
ac2fc2de5c Cancelling events: Allow to cancel all dates in a series 2020-03-23 10:37:10 +01:00
Raphael Michel
45e548873e Do not crash event page on social image generation 2020-03-22 11:04:51 +01:00
Raphael Michel
f484eb65df PayPal: Do not crash on failed refund 2020-03-22 11:04:51 +01:00
Raphael Michel
027a785ab5 Log out other sessions after email or 2FA changes 2020-03-22 11:04:51 +01:00
Raphael Michel
25b80cbb57 Fix invalid requirement spec 2020-03-20 12:46:15 +01:00
Raphael Michel
589fa0f9de Cancelling events: Send email even if refund failed 2020-03-20 12:38:57 +01:00
Raphael Michel
6d2989d15a Run a forked version of vat_moss 2020-03-20 11:06:19 +01:00
Raphael Michel
5bb27b29ae Seat statistics: Only use active variations 2020-03-16 16:32:34 +01:00
Raphael Michel
d17f8a71e6 Fix crashes in new statistical feature 2020-03-16 16:12:48 +01:00
Raphael Michel
b664cc712a Cancelling events: Allow to create manual and partial refunds 2020-03-16 16:00:44 +01:00
Raphael Michel
d61e8a9204 Cancelling events: Allow to select fee types to keep 2020-03-16 15:44:37 +01:00
Martin Gross
f00012a63e Statistics on sold and unsold seats, as well as potential profi… (#1613)
* Statistics on sold and unsold seats, as well as potential profits

* Rework of seats-stats

* Fix crash when all seats are assigned

* Update src/pretix/plugins/statistics/views.py

Co-Authored-By: Raphael Michel <michel@rami.io>

* Update src/pretix/plugins/statistics/views.py

Co-Authored-By: Raphael Michel <michel@rami.io>

* Update src/pretix/plugins/statistics/views.py

Co-Authored-By: Raphael Michel <michel@rami.io>

* Fix count of sold seats

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: Raphael Michel <michel@rami.io>
2020-03-16 15:20:30 +01:00
Raphael Michel
bd238f76ce Fix broken test 2020-03-16 13:29:12 +01:00
Raphael Michel
703ae97820 Fix typo in German translation 2020-03-16 13:27:33 +01:00
Raphael Michel
1a60c5ea64 Fix missing bleach call in invoice renderer 2020-03-16 11:44:31 +01:00
Raphael Michel
1d3ac5f02f Fix StaticDevice.MultipleObjectsReturned
PRETIXEU-1YX
2020-03-16 11:41:33 +01:00
Raphael Michel
8d23d75dfd Only send download reminders if there's actually a download 2020-03-16 09:26:11 +01:00
Maico Timmerman
9a32668ee1 Make next url authentication backend dependent (#1609)
* Make next url authentication backend dependent

* Rename authentication next_url to get_next_url.

* Add test for custom authentication backend get_next_url.

* Fix typo in docstring of authentication backend get_next_url.
2020-03-15 11:05:57 +01:00
pajowu
ca0407a133 Subclass MultipleChoiceField to serialize to list (#1605)
* Subclass MultipleChoiceField to serialize to list

* Rename pretix.api.serializers.MultipleChoiceField to ListMultipleChoiceField

* Keep order in ListMultipleChoiceField
2020-03-14 22:04:12 +01:00
Raphael Michel
1de77b0784 Translations update from Weblate (#1612)
Translations update from Weblate
2020-03-14 22:02:09 +01:00
Maarten van den Berg
d0907d3dcf Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-13 08:00:11 +01:00
pajowu
81cc4bd768 Do not validate event creation form on back button (#1611) 2020-03-12 19:17:49 +01:00
pajowu
262639e063 Show checkbox if items per order limit is 1 (#1610) 2020-03-12 18:47:14 +01:00
Martin Gross
dedd93fb89 Add Support for Square POS payment_data 2020-03-12 14:33:37 +01:00
Raphael Michel
45f94aee03 Merge pull request #1602 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-12 13:31:40 +01:00
Maarten van den Berg
d36e7d033f Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-12 13:31:17 +01:00
Maarten van den Berg
b94bd277bf Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-12 13:31:16 +01:00
Maico Timmerman
e5095185d9 Send widget_data in voucher redeem request. (#1606) 2020-03-12 13:31:12 +01:00
pajowu
d76ce47597 Allow creating question with dependency_question=None (#1607) 2020-03-12 13:30:18 +01:00
Martin Gross
58717850c2 Add date-on-frontpage check to secondary template 2020-03-12 12:06:26 +01:00
Martin Gross
29d52d4fe5 Second attempt at hiding date on frontpage in title 2020-03-12 12:00:18 +01:00
Martin Gross
34c9c40ddc Option to hide date-block on frontpage (#1603)
* Option to hide date-block on frontpage

* Also hide date in headline
2020-03-12 10:28:47 +01:00
Martin Gross
39d05a6c40 Duplicate generate_ticket for items on duplication 2020-03-11 18:44:29 +01:00
Martin Gross
b664222c62 Calculation fix for unlimited Quota w/ Waitinglist 2020-03-11 16:48:57 +01:00
Raphael Michel
1ee48a10b5 Allow to reactivate canceled orders (#1601) 2020-03-11 11:40:56 +01:00
Raphael Michel
2431a8b767 Fix typo 2020-03-09 17:37:13 +01:00
Raphael Michel
af84354e51 Resolve some warnings in the test suite 2020-03-09 14:57:19 +01:00
Raphael Michel
b04de880fc Bump to 3.8.0.dev0 2020-03-09 14:34:00 +01:00
Raphael Michel
11f3057f76 Bump version to 3.7.0 2020-03-09 14:33:17 +01:00
Raphael Michel
ba164c16f6 Fix test failure that only happens in the 20 days before daylight saving time changes 2020-03-09 12:56:58 +01:00
Raphael Michel
7ef766ddfa Cancelling events: Only show email fields conditionally 2020-03-09 11:44:25 +01:00
Raphael Michel
bcafcc7dd8 Fix invalid translation file 2020-03-09 11:36:59 +01:00
Raphael Michel
e5b7102abc Merge pull request #1600 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-09 11:21:33 +01:00
Raphael Michel
3601dd6bee Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-09 11:20:45 +01:00
Raphael Michel
a1d5854fbf Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3516 of 3516 strings)

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

powered by weblate
2020-03-09 11:20:44 +01:00
Raphael Michel
09544a688d Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-03-09 11:11:54 +01:00
Raphael Michel
58a5892cc0 Merge pull request #1598 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-09 11:11:12 +01:00
Maarten van den Berg
c9af76b46e Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 97.9% (3436 of 3510 strings)

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

powered by weblate
2020-03-09 01:00:13 +01:00
Maarten van den Berg
91753935cf Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3510 of 3510 strings)

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

powered by weblate
2020-03-09 01:00:12 +01:00
Raphael Michel
23a52eb12a Add lang attribute to <html> tag in presale 2020-03-08 15:23:47 +01:00
Raphael Michel
79ecb231f2 Fix placement of back link when a header is used 2020-03-08 15:07:59 +01:00
Raphael Michel
08de7f59a3 Move iframe.js into main JS file 2020-03-08 15:07:59 +01:00
Raphael Michel
0de3c33bab Remove unused bootstrap components from CSS 2020-03-08 15:07:59 +01:00
Raphael Michel
a4ae8b0e66 Include lightbox in main.scss file 2020-03-08 15:07:59 +01:00
Raphael Michel
be1bf81298 Add font-display: swap to custom shop fonts 2020-03-08 15:07:59 +01:00
Raphael Michel
b7528ae1cf Cancelling events: Fix incorrect refund amount in emails 2020-03-06 18:09:38 +01:00
Raphael Michel
4f6712ccbe OrderChangeManager: Prevent creation of 0 € invoices 2020-03-06 17:05:15 +01:00
Raphael Michel
939335f94b Widget: Fix incorrect computation of cache key 2020-03-06 17:00:47 +01:00
Raphael Michel
c849276a35 Widget: Allow to filter by product 2020-03-06 17:00:26 +01:00
Raphael Michel
8e9f0f07a1 Optimize performance of waiting list dashboard tile 2020-03-06 14:03:28 +01:00
Raphael Michel
389884d191 shell_scoped: Fall back to shell if shell_plus is not installed 2020-03-06 14:03:06 +01:00
Raphael Michel
d8c2c82da7 Update invoice language in build_invoice 2020-03-06 09:36:20 +01:00
Raphael Michel
c3ed3d4899 Adjust tests for last commit 2020-03-05 18:12:40 +01:00
Raphael Michel
e9235cd433 Bank transfer: ignore order status when manually assigning amount 2020-03-05 17:37:54 +01:00
Raphael Michel
975b6d800a Fix hardcoded PK in test 2020-03-05 17:09:33 +01:00
Raphael Michel
ee260c8231 API: Allow to simulate orders 2020-03-05 16:37:55 +01:00
Raphael Michel
f7fddc05dd Improve caching of widget.js 2020-03-05 13:09:42 +01:00
Raphael Michel
eaa61c7795 Add @gzip_page to widget views 2020-03-05 12:53:17 +01:00
Raphael Michel
d4994258e6 Avoid issues with duplicate ItemBundles 2020-03-05 12:53:02 +01:00
Raphael Michel
9b50ec2d74 Cancelling events: Allow to inform waiting list 2020-03-05 10:22:59 +01:00
Raphael Michel
447b6b7fee Waiting list: Do not send notifications for disabled items 2020-03-05 09:37:35 +01:00
Raphael Michel
40f763c999 Cancelling events: Create special log entry 2020-03-05 09:37:17 +01:00
Raphael Michel
6a3d05be9e Cancelling events: Fix invalid email logging 2020-03-05 09:23:07 +01:00
Raphael Michel
766447f021 Fix typo in form field name 2020-03-04 14:45:54 +01:00
Raphael Michel
5fbeb90f00 Order import: Fix crash on unknown delimiter
PRETIXEU-1Y0
2020-03-04 14:44:55 +01:00
Raphael Michel
c01cc85eda Merge pull request #1597 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-03-03 17:13:10 +01:00
Raphael Michel
4a054da6ee Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3510 of 3510 strings)

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

powered by weblate
2020-03-03 17:11:12 +01:00
Raphael Michel
583a2b6572 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3510 of 3510 strings)

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

powered by weblate
2020-03-03 17:11:11 +01:00
Raphael Michel
fbe025afb2 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3488 of 3488 strings)

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

powered by weblate
2020-03-03 16:56:06 +01:00
Raphael Michel
66e6191122 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3488 of 3488 strings)

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

powered by weblate
2020-03-03 16:56:05 +01:00
Raphael Michel
0f26f0787c Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-03-03 16:55:56 +01:00
Raphael Michel
62a86c9b4a Allow to cancel all orders in an event (#1596)
* Allow to cancel all orders in an event

* Add tests

* Actually add tests
2020-03-03 16:55:05 +01:00
Raphael Michel
07318be4c9 Fix incorrect total in invoice preview 2020-03-03 16:45:18 +01:00
Raphael Michel
3c8ef2c620 Fix incorrect result message 2020-03-03 16:43:36 +01:00
Raphael Michel
a858f47220 API: Fix crash when cancelling a redeemed gift card 2020-03-02 13:18:16 +01:00
Raphael Michel
381fa5e1cd Allow to add a text to gift card transactions 2020-03-02 12:47:39 +01:00
Raphael Michel
1539eea664 API: Allow devices to access gift cards 2020-03-02 12:19:19 +01:00
Raphael Michel
3d41d1331a API: Support for cross-organizer gift cards 2020-03-02 12:19:02 +01:00
Raphael Michel
00848b3339 Remove "default" flag from cloned layouts 2020-03-02 09:35:53 +01:00
Raphael Michel
d174b11c6a Reduce retention time of sass compiler cache 2020-02-28 16:21:34 +01:00
Raphael Michel
a501ce496a Fix broken sass compiler cache 2020-02-28 16:21:18 +01:00
Raphael Michel
de277cc959 Badges, PDF tickets: Allow to copy layouts 2020-02-28 12:50:31 +01:00
Raphael Michel
12b3ae81d6 Mark payment_term_days settings as required in the backend 2020-02-28 12:24:00 +01:00
Raphael Michel
fcdb40dda0 Refs #1593 -- Fix subsequent issue when clearing the cart 2020-02-27 16:28:07 +01:00
Raphael Michel
f65cf8e86a Fix checks for min_per_order and max_per_order in combination with variations 2020-02-27 15:08:35 +01:00
Raphael Michel
12540238b7 Ignore replica database in tests 2020-02-27 14:44:20 +01:00
Raphael Michel
398a30e33a Merge pull request #1592 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-02-27 11:05:27 +01:00
Raphael Michel
3410640618 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3488 of 3488 strings)

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

powered by weblate
2020-02-27 11:05:05 +01:00
Raphael Michel
7b45cfccc2 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3488 of 3488 strings)

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

powered by weblate
2020-02-27 11:05:04 +01:00
Raphael Michel
33f503aea1 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-02-27 10:55:11 +01:00
Raphael Michel
3fd650081b Allowing more options to style pretix shops (#1585)
* Fix caching issues in SASS compilation

* Allow to set a custom page background color

* Allow to disable round corners

* Support larger header pictures

* Allow to show title despite header

* Move language picker

* FIx widget styles
2020-02-27 10:54:00 +01:00
Martin Gross
b622854be6 Fix versionchanged for item_meta_data 2020-02-27 09:25:44 +01:00
Raphael Michel
6d00daa9ee Fix crash when sending mails with empty mail_from 2020-02-26 17:34:37 +01:00
Raphael Michel
f27148998a Support for itemmeta: objects in the API 2020-02-26 15:59:42 +01:00
Raphael Michel
4a0369cc37 REbase migration 2020-02-26 15:22:41 +01:00
Martin Gross
76aaf61e19 Add meta_data for items (#1576)
* PoC for ItemMetaProperties/Values

* Missing is_valid

* ItemMetaProperties/Values in editable via API, cloneable

* Tests

* Add Docs

* Fix import order

* Fix another import sorting...

* Typeahead for ItemMetaValues

* Test for editing event-objects

* Fix typeahead permission checks

* Further access restriction

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2020-02-26 15:06:24 +01:00
Raphael Michel
dd1e5fa929 Test suite: Allow to cancel expired orders 2020-02-26 11:56:23 +01:00
Raphael Michel
4a2516e303 Allow to cancel expired orders 2020-02-26 11:23:52 +01:00
Raphael Michel
cf06712eca Merge pull request #1590 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-02-26 08:51:30 +01:00
Maarten van den Berg
6185d675f0 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 97.5% (3388 of 3474 strings)

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

powered by weblate
2020-02-26 04:00:13 +01:00
David100mark
a53cd3abce Translated on translate.pretix.eu (French)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2020-02-26 04:00:12 +01:00
David100mark
ebe86a17fb Translated on translate.pretix.eu (French)
Currently translated at 66.9% (2325 of 3474 strings)

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

powered by weblate
2020-02-26 04:00:11 +01:00
David100mark
d189b16ee7 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3474 of 3474 strings)

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

powered by weblate
2020-02-26 04:00:11 +01:00
Raphael Michel
d70ce4491a Merge pull request #1584 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-02-20 16:12:22 +01:00
Maarten van den Berg
607ff48d70 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 97.4% (3382 of 3474 strings)

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

powered by weblate
2020-02-19 18:25:18 +01:00
Maarten van den Berg
4bab44ca85 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3474 of 3474 strings)

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

powered by weblate
2020-02-19 18:25:18 +01:00
Raphael Michel
a5cdb485d0 Fix faulty test cases 2020-02-19 18:25:05 +01:00
Raphael Michel
282ef792c4 Do not show "create new event" if there are no permissions to dos o 2020-02-19 18:24:47 +01:00
Raphael Michel
6cd888a1dc Fix date parsing issue in Danish locale 2020-02-19 18:00:02 +01:00
Raphael Michel
2e5b80c83c Fix missing fees in order overview 2020-02-19 16:09:40 +01:00
Raphael Michel
4511110069 Fix timezone of notifications 2020-02-19 14:28:06 +01:00
Raphael Michel
1af1d8c658 Add rich_text_snippet 2020-02-18 09:45:20 +01:00
Raphael Michel
9f6a3f9a6a Add custom field for invoice addresses 2020-02-18 09:21:00 +01:00
Raphael Michel
1c03d5d305 Invoices: Label tax ID as ABN in Australia 2020-02-18 09:05:09 +01:00
Raphael Michel
69a1fccd20 Fix #1582 - Add explicit requirement to packaging library 2020-02-17 21:00:44 +01:00
Raphael Michel
2a54aa2d83 Bump to 3.7.0.dev0 2020-02-17 16:39:27 +01:00
360 changed files with 94343 additions and 61869 deletions

View File

@@ -13,24 +13,24 @@ services:
- postgresql - postgresql
matrix: matrix:
include: include:
- python: 3.7 - python: 3.8
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.7 - python: 3.8
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.7 - python: 3.8
env: JOB=style env: JOB=style
- python: 3.7 - python: 3.8
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.7 - python: 3.8
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.7 - python: 3.7
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.8
env: JOB=doc-spelling env: JOB=doc-spelling
- python: 3.7 - python: 3.8
env: JOB=translation-spelling env: JOB=translation-spelling
addons: addons:
postgresql: "9.4" postgresql: "10"
mariadb: '10.3' mariadb: '10.3'
apt: apt:
packages: packages:

View File

@@ -182,7 +182,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
-v /var/pretix-data:/data \ -v /var/pretix-data:/data \
-v /etc/pretix:/etc/pretix \ -v /etc/pretix:/etc/pretix \
-v /var/run/redis:/var/run/redis \ -v /var/run/redis:/var/run/redis \
--sysctl net.core.somaxconn=4096 --sysctl net.core.somaxconn=4096 \
pretix/standalone:stable all pretix/standalone:stable all
ExecStop=/usr/bin/docker stop %n ExecStop=/usr/bin/docker stop %n

View File

@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
offers at `pretix.eu`_. offers at `pretix.eu`_.
We tested this guide on the Linux distribution **Debian 8.0** but it should work very similar on other We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
modern distributions, especially on all systemd-based ones. modern distributions, especially on all systemd-based ones.
Requirements Requirements
@@ -133,7 +133,7 @@ command if you're running MySQL::
(venv)$ pip3 install "pretix[postgres]" gunicorn (venv)$ pip3 install "pretix[postgres]" gunicorn
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``. Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
We also need to create a data directory:: We also need to create a data directory::

View File

@@ -43,6 +43,7 @@ seating_plan integer If reserved sea
seat_category_mapping object An object mapping categories of the seating plan seat_category_mapping object An object mapping categories of the seating plan
(strings) to items in the event (integers or ``null``). (strings) to items in the event (integers or ``null``).
timezone string Event timezone name timezone string Event timezone name
item_meta_properties object Item-specific meta data parameters and default values.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
@@ -79,6 +80,10 @@ timezone string Event timezone
The attribute ``timezone`` has been added. The attribute ``timezone`` has been added.
.. versionchanged:: 3.7
The attribute ``item_meta_properties`` has been added.
Endpoints Endpoints
--------- ---------
@@ -133,6 +138,7 @@ Endpoints
"seating_plan": null, "seating_plan": null,
"seat_category_mapping": {}, "seat_category_mapping": {},
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.banktransfer" "pretix.plugins.banktransfer"
"pretix.plugins.stripe" "pretix.plugins.stripe"
@@ -204,6 +210,7 @@ Endpoints
"seat_category_mapping": {}, "seat_category_mapping": {},
"meta_data": {}, "meta_data": {},
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.banktransfer" "pretix.plugins.banktransfer"
"pretix.plugins.stripe" "pretix.plugins.stripe"
@@ -256,6 +263,7 @@ Endpoints
"has_subevents": false, "has_subevents": false,
"meta_data": {}, "meta_data": {},
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal"
@@ -290,6 +298,7 @@ Endpoints
"has_subevents": false, "has_subevents": false,
"meta_data": {}, "meta_data": {},
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal"
@@ -344,6 +353,7 @@ Endpoints
"has_subevents": false, "has_subevents": false,
"meta_data": {}, "meta_data": {},
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal"
@@ -378,6 +388,7 @@ Endpoints
"seat_category_mapping": {}, "seat_category_mapping": {},
"meta_data": {}, "meta_data": {},
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal"
@@ -444,6 +455,7 @@ Endpoints
"seat_category_mapping": {}, "seat_category_mapping": {},
"meta_data": {}, "meta_data": {},
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer",
"pretix.plugins.stripe", "pretix.plugins.stripe",

View File

@@ -59,6 +59,9 @@ Endpoints
} }
:query integer page: The page number in case of a multi-page result set, default is 1 :query integer page: The page number in case of a multi-page result set, default is 1
:query string secret: Only show gift cards with the given secret.
:query boolean testmode: Filter for gift cards that are (not) in test mode.
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error :statuscode 200: no error
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
@@ -94,6 +97,7 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the gift card to fetch :param id: The ``id`` field of the gift card to fetch
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
:statuscode 200: no error :statuscode 200: no error
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource. :statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
@@ -227,6 +231,7 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to modify :param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the gift card to modify :param id: The ``id`` field of the gift card to modify
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
:statuscode 200: no error :statuscode 200: no error
:statuscode 400: The gift card could not be modified due to invalid submitted data :statuscode 400: The gift card could not be modified due to invalid submitted data
:statuscode 401: Authentication failure :statuscode 401: Authentication failure

View File

@@ -114,6 +114,7 @@ bundles list of objects Definition of b
└ designated_price money (string) Designated price of the bundled product. This will be └ designated_price money (string) Designated price of the bundled product. This will be
used to split the price of the base item e.g. for mixed used to split the price of the base item e.g. for mixed
taxation. This is not added to the price. taxation. This is not added to the price.
meta_data object Values set for event-specific meta data parameters.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
.. versionchanged:: 2.7 .. versionchanged:: 2.7
@@ -154,6 +155,10 @@ bundles list of objects Definition of b
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added. The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
.. versionchanged:: 3.7
The attribute ``meta_data`` has been added.
Notes Notes
----- -----
@@ -208,6 +213,7 @@ Endpoints
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {},
"position": 0, "position": 0,
"picture": null, "picture": null,
"available_from": null, "available_from": null,
@@ -303,6 +309,7 @@ Endpoints
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {},
"position": 0, "position": 0,
"picture": null, "picture": null,
"available_from": null, "available_from": null,
@@ -379,6 +386,7 @@ Endpoints
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {},
"position": 0, "position": 0,
"picture": null, "picture": null,
"available_from": null, "available_from": null,
@@ -442,6 +450,7 @@ Endpoints
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {},
"position": 0, "position": 0,
"picture": null, "picture": null,
"available_from": null, "available_from": null,
@@ -537,6 +546,7 @@ Endpoints
"tax_rule": 1, "tax_rule": 1,
"admission": false, "admission": false,
"issue_giftcard": false, "issue_giftcard": false,
"meta_data": {},
"position": 0, "position": 0,
"picture": null, "picture": null,
"available_from": null, "available_from": null,

View File

@@ -151,6 +151,10 @@ last_modified datetime Last modificati
The ``order.fees.canceled`` attribute has been added. The ``order.fees.canceled`` attribute has been added.
.. versionchanged:: 3.8
The ``reactivate`` operation has been added.
.. _order-position-resource: .. _order-position-resource:
@@ -173,6 +177,13 @@ price money (string) Price of this p
attendee_name string Specified attendee name for this position (or ``null``) attendee_name string Specified attendee name for this position (or ``null``)
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name) attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
attendee_email string Specified attendee email address for this position (or ``null``) attendee_email string Specified attendee email address for this position (or ``null``)
company string Attendee company name (or ``null``)
street string Attendee street (or ``null``)
zipcode string Attendee ZIP code (or ``null``)
city string Attendee city (or ``null``)
country string Attendee country code (or ``null``)
state string Attendee state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
voucher integer Internal ID of the voucher used for this position (or ``null``) voucher integer Internal ID of the voucher used for this position (or ``null``)
tax_rate decimal (string) VAT rate applied for this position tax_rate decimal (string) VAT rate applied for this position
tax_value money (string) VAT included in this position tax_value money (string) VAT included in this position
@@ -236,6 +247,10 @@ pdf_data object Data object req
The attribute ``canceled`` has been added. The attribute ``canceled`` has been added.
.. versionchanged:: 3.8
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
.. _order-payment-resource: .. _order-payment-resource:
Order payment resource Order payment resource
@@ -380,6 +395,12 @@ List of all orders
"full_name": "Peter", "full_name": "Peter",
}, },
"attendee_email": null, "attendee_email": null,
"company": "Sample company",
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "DE",
"state": null,
"voucher": null, "voucher": null,
"tax_rate": "0.00", "tax_rate": "0.00",
"tax_value": "0.00", "tax_value": "0.00",
@@ -536,6 +557,12 @@ Fetching individual orders
"full_name": "Peter", "full_name": "Peter",
}, },
"attendee_email": null, "attendee_email": null,
"company": "Sample company",
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "DE",
"state": null,
"voucher": null, "voucher": null,
"tax_rate": "0.00", "tax_rate": "0.00",
"tax_rule": null, "tax_rule": null,
@@ -816,9 +843,9 @@ Creating orders
* ``consume_carts`` (optional) A list of cart IDs. All cart positions with these IDs will be deleted if the * ``consume_carts`` (optional) A list of cart IDs. All cart positions with these IDs will be deleted if the
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
creation. creation.
* ``email`` * ``email`` (optional)
* ``locale`` * ``locale``
* ``sales_channel`` * ``sales_channel`` (optional)
* ``payment_provider`` (optional) The identifier of the payment provider set for this order. This needs to be an * ``payment_provider`` (optional) The identifier of the payment provider set for this order. This needs to be an
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"`` existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
@@ -851,15 +878,21 @@ Creating orders
* ``positionid`` (optional, see below) * ``positionid`` (optional, see below)
* ``item`` * ``item``
* ``variation`` * ``variation`` (optional)
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product) * ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.) * ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
* ``attendee_name`` **or** ``attendee_name_parts`` * ``attendee_name`` **or** ``attendee_name_parts`` (optional)
* ``voucher`` (optional, the ``code`` attribute of a valid voucher) * ``voucher`` (optional, the ``code`` attribute of a valid voucher)
* ``attendee_email`` * ``attendee_email`` (optional)
* ``company`` (optional)
* ``street`` (optional)
* ``zipcode`` (optional)
* ``city`` (optional)
* ``country`` (optional)
* ``state`` (optional)
* ``secret`` (optional) * ``secret`` (optional)
* ``addon_to`` (optional, see below) * ``addon_to`` (optional, see below)
* ``subevent`` * ``subevent`` (optional)
* ``answers`` * ``answers``
* ``question`` * ``question``
@@ -891,6 +924,13 @@ Creating orders
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
immediately after the position itself. immediately after the position itself.
Starting with pretix 3.7, you can add ``"simulate": true`` to the body to do a "dry run" of your order. This will
validate your order and return you an order object with the resulting prices, but will not create an actual order.
You can use this for testing or to look up prices. In this case, some attributes are ignored, such as whether
to send an email or what payment provider will be used. Note that some returned fields will contain empty values
(e.g. all ``id`` fields of positions will be zero) and some will contain fake values (e.g. the order code will
always be ``PREVIEW``). pretix plugins will not be triggered, so some special behavior might be missing as well.
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -1050,6 +1090,42 @@ Order state operations
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist. :statuscode 404: The requested order does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/reactivate/
Reactivates a canceled order. This will set the order to pending or paid state. Only possible if all products are
still available.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/reactivate/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"code": "ABC12",
"status": "n",
...
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param code: The ``code`` field of the order to modify
:statuscode 200: no error
:statuscode 400: The order cannot be reactivated
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/ .. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
Marks a paid order as unpaid. Marks a paid order as unpaid.

View File

@@ -66,7 +66,7 @@ event-related views, there is also a signal that allows you to add the view to t
from django.urls import resolve, reverse from django.urls import resolve, reverse
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.control.signals import nav_event from pretix.control.signals import nav_event

View File

@@ -20,7 +20,7 @@ Order events
There are multiple signals that will be sent out in the ordering cycle: There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text :members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins Check-ins
""""""""" """""""""
@@ -33,11 +33,11 @@ Frontend
-------- --------
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, item_description :members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:members: order_info, order_meta_from_request :members: order_info, order_info_top, order_meta_from_request
Request flow Request flow
"""""""""""" """"""""""""

View File

@@ -61,7 +61,7 @@ A working example would be::
from pretix.base.plugins import PluginConfig from pretix.base.plugins import PluginConfig
except ImportError: except ImportError:
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!") raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class PaypalApp(PluginConfig): class PaypalApp(PluginConfig):

View File

@@ -69,7 +69,7 @@ We now need a way to translate the action codes like ``pretix.event.changed`` in
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
implementation could look like:: implementation could look like::
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from pretix.base.signals import logentry_display from pretix.base.signals import logentry_display
@receiver(signal=logentry_display) @receiver(signal=logentry_display)

277
doc/plugins/digital.rst Normal file
View File

@@ -0,0 +1,277 @@
Digital content
===============
The digital content plugin provides a HTTP API that allows you to create new digital content for your ticket holders,
such as live streams, videos, or material downloads.
Resource description
--------------------
The digital content resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal content ID
title multi-lingual string The content title (required)
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
url string The location of the digital content
description multi-lingual string A public description of the item. May contain Markdown
syntax and is not required.
available_from datetime The first date time at which this content will be shown
(or ``null``).
available_until datetime The last date time at which this content will b e shown
(or ``null``).
all_products boolean If ``true``, the content is available to all buyers of tickets for this event. The ``limit_products`` field is ignored in this case.
limit_products list of integers List of product/item IDs. This content is only shown to buyers of these ticket types.
position integer An integer, used for sorting
subevent integer Date in an event series this content should be shown for. Should be ``null`` if this is not an event series or if this should be shown to all customers.
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
Returns a list of all digital content configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
Returns information on one content item, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the content to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
Create a new digital content.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 166
{
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 2,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
:param organizer: The ``slug`` field of the organizer to create new content for
:param event: The ``slug`` field of the event to create new content for
:statuscode 201: no error
:statuscode 400: The content could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create digital contents.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
Update a content. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 34
{
"url": "https://mywebsite.com"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 2,
"subevent": null,
"title": {
"en": "Concert livestream"
},
"content_type": "link",
"url": "https://mywebsite.com",
"description": {
"en": "Watch our event live here on YouTube!"
},
"all_products": true,
"limit_products": [],
"available_from": "2020-03-22T23:00:00Z",
"available_until": null,
"position": 1
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the content to modify
:statuscode 200: no error
:statuscode 400: The content could not be modified due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
Delete a digital content.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the content to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it

View File

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

View File

@@ -114,6 +114,17 @@ If you want to disable voucher input in the widget, you can pass the ``disable-v
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget> <pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
Filtering products
------------------
You can filter the products shown in the widget by passing in a list of product IDs::
<pretix-widget event="https://pretix.eu/demo/democon/" items="23,42"></pretix-widget>
Alternatively, you can select one or more categories to be shown::
<pretix-widget event="https://pretix.eu/demo/democon/" categories="12,25"></pretix-widget>
Multi-event selection Multi-event selection
--------------------- ---------------------

View File

@@ -14,30 +14,23 @@ and with pretix, you can do this. On this page, you find out the necessary steps
With the pretix.eu hosted service With the pretix.eu hosted service
--------------------------------- ---------------------------------
Step 1: DNS Configuration Go to "Organizers" in the backend and select your organizer account. Then, go to "Settings" and "Custom Domain".
#########################
This page will show you instructions on how to set up your own domain. Basically, it works like this:
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
domain provider. domain provider.
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias"). Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
The value of the record should be ``www.pretix.eu``. The value of the record should be the one shown on the "Custom Domain" page in pretix' backend.
Step 2: Wait for the DNS entry to propagate
###########################################
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``. of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
anyways, you should get a pretix-themed error page with the headline "Unknown domain". anyways, you should get a pretix-themed error page with the headline "Unknown domain".
Step 3: Tell us Now, tell us about your domain on the "Custom Domain" page to get started.
###############
Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL
certificate for you (for free!) and configure the domain.
With a custom pretix installation With a custom pretix installation
--------------------------------- ---------------------------------

View File

@@ -1 +1 @@
__version__ = "3.6.0" __version__ = "3.8.0"

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from oauth2_provider.generators import ( from oauth2_provider.generators import (
generate_client_id, generate_client_secret, generate_client_id, generate_client_secret,
) )

View File

@@ -2,7 +2,7 @@ from datetime import timedelta
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy from django.utils.translation import gettext_lazy
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@@ -56,7 +56,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent'))) else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
if len(new_quotas) == 0: if len(new_quotas) == 0:
raise ValidationError( raise ValidationError(
ugettext_lazy('The product "{}" is not assigned to a quota.').format( gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(validated_data.get('item')) str(validated_data.get('item'))
) )
) )
@@ -64,8 +64,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
avail = quota.availability() avail = quota.availability()
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1): if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
raise ValidationError( raise ValidationError(
ugettext_lazy('There is not enough quota available on quota "{}" to perform ' gettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format( 'the operation.').format(
quota.name quota.name
) )
) )
@@ -88,7 +88,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
else: else:
validated_data['seat'] = seat validated_data['seat'] = seat
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')): if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)) raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated: elif seated:
raise ValidationError('The specified product requires to choose a seat.') raise ValidationError('The specified product requires to choose a seat.')

View File

@@ -1,4 +1,4 @@
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError

View File

@@ -2,7 +2,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from django_countries.serializers import CountryFieldMixin from django_countries.serializers import CountryFieldMixin
from hierarkey.proxy import HierarkeyProxy from hierarkey.proxy import HierarkeyProxy
from pytz import common_timezones from pytz import common_timezones
@@ -34,6 +34,19 @@ class MetaDataField(Field):
} }
class MetaPropertyField(Field):
def to_representation(self, value):
return {
v.name: v.default for v in value.item_meta_properties.all()
}
def to_internal_value(self, data):
return {
'item_meta_properties': data
}
class SeatCategoryMappingField(Field): class SeatCategoryMappingField(Field):
def to_representation(self, value): def to_representation(self, value):
@@ -77,6 +90,7 @@ class TimeZoneField(ChoiceField):
class EventSerializer(I18nAwareModelSerializer): class EventSerializer(I18nAwareModelSerializer):
meta_data = MetaDataField(required=False, source='*') meta_data = MetaDataField(required=False, source='*')
item_meta_properties = MetaPropertyField(required=False, source='*')
plugins = PluginsField(required=False, source='*') plugins = PluginsField(required=False, source='*')
seat_category_mapping = SeatCategoryMappingField(source='*', required=False) seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones]) timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
@@ -86,7 +100,7 @@ class EventSerializer(I18nAwareModelSerializer):
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from', fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start', 'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping', 'timezone') 'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties')
def validate(self, data): def validate(self, data):
data = super().validate(data) data = super().validate(data)
@@ -131,6 +145,12 @@ class EventSerializer(I18nAwareModelSerializer):
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key)) raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
return value return value
@cached_property
def item_meta_props(self):
return {
p.name: p for p in self.context['request'].event.item_meta_properties.all()
}
def validate_seating_plan(self, value): def validate_seating_plan(self, value):
if value and value.organizer != self.context['request'].organizer: if value and value.organizer != self.context['request'].organizer:
raise ValidationError('Invalid seating plan.') raise ValidationError('Invalid seating plan.')
@@ -172,6 +192,7 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic @transaction.atomic
def create(self, validated_data): def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None) meta_data = validated_data.pop('meta_data', None)
item_meta_properties = validated_data.pop('item_meta_properties', None)
validated_data.pop('seat_category_mapping', None) validated_data.pop('seat_category_mapping', None)
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(',')) plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
tz = validated_data.pop('timezone', None) tz = validated_data.pop('timezone', None)
@@ -188,6 +209,15 @@ class EventSerializer(I18nAwareModelSerializer):
value=value value=value
) )
# Item Meta properties
if item_meta_properties is not None:
for key, value in item_meta_properties.items():
event.item_meta_properties.create(
name=key,
default=value,
event=event
)
# Seats # Seats
if event.seating_plan: if event.seating_plan:
generate_seats(event, None, event.seating_plan, {}) generate_seats(event, None, event.seating_plan, {})
@@ -202,6 +232,7 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic @transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None) meta_data = validated_data.pop('meta_data', None)
item_meta_properties = validated_data.pop('item_meta_properties', None)
plugins = validated_data.pop('plugins', None) plugins = validated_data.pop('plugins', None)
seat_category_mapping = validated_data.pop('seat_category_mapping', None) seat_category_mapping = validated_data.pop('seat_category_mapping', None)
tz = validated_data.pop('timezone', None) tz = validated_data.pop('timezone', None)
@@ -228,6 +259,26 @@ class EventSerializer(I18nAwareModelSerializer):
if prop.name not in meta_data: if prop.name not in meta_data:
current_object.delete() current_object.delete()
# Item Meta properties
if item_meta_properties is not None:
current = [imp for imp in event.item_meta_properties.all()]
for key, value in item_meta_properties.items():
prop = self.item_meta_props.get(key)
if prop in current:
prop.default = value
prop.save()
else:
prop = event.item_meta_properties.create(
name=key,
default=value,
event=event
)
current.append(prop)
for prop in current:
if prop.name not in list(item_meta_properties.keys()):
prop.delete()
# Seats # Seats
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None): if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
current_mappings = { current_mappings = {
@@ -481,6 +532,9 @@ class EventSettingsSerializer(serializers.Serializer):
'checkout_email_helptext', 'checkout_email_helptext',
'presale_has_ended_text', 'presale_has_ended_text',
'voucher_explanation_text', 'voucher_explanation_text',
'banner_text',
'banner_text_bottom',
'show_dates_on_frontpage',
'show_date_to', 'show_date_to',
'show_times', 'show_times',
'show_items_outside_presale_period', 'show_items_outside_presale_period',
@@ -506,6 +560,10 @@ class EventSettingsSerializer(serializers.Serializer):
'attendee_names_required', 'attendee_names_required',
'attendee_emails_asked', 'attendee_emails_asked',
'attendee_emails_required', 'attendee_emails_required',
'attendee_addresses_asked',
'attendee_addresses_required',
'attendee_company_asked',
'attendee_company_required',
'confirm_text', 'confirm_text',
'order_email_asked_twice', 'order_email_asked_twice',
'payment_term_days', 'payment_term_days',
@@ -528,6 +586,7 @@ class EventSettingsSerializer(serializers.Serializer):
'invoice_address_vatid', 'invoice_address_vatid',
'invoice_address_company_required', 'invoice_address_company_required',
'invoice_address_beneficiary', 'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_name_required', 'invoice_name_required',
'invoice_address_not_asked_free', 'invoice_address_not_asked_free',
'invoice_show_payments', 'invoice_show_payments',
@@ -558,6 +617,10 @@ class EventSettingsSerializer(serializers.Serializer):
'cancel_allow_user_paid_keep', 'cancel_allow_user_paid_keep',
'cancel_allow_user_paid_keep_fees', 'cancel_allow_user_paid_keep_fees',
'cancel_allow_user_paid_keep_percentage', 'cancel_allow_user_paid_keep_percentage',
'cancel_allow_user_paid_adjust_fees',
'cancel_allow_user_paid_adjust_fees_explanation',
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -0,0 +1,29 @@
from collections import OrderedDict
from rest_framework import serializers
def remove_duplicates_from_list(data):
return list(OrderedDict.fromkeys(data))
class ListMultipleChoiceField(serializers.MultipleChoiceField):
def to_internal_value(self, data):
if isinstance(data, str) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail('empty')
internal_value_data = [
super(serializers.MultipleChoiceField, self).to_internal_value(item)
for item in data
]
return remove_duplicates_from_list(internal_value_data)
def to_representation(self, value):
representation_data = [
self.choice_strings_to_values.get(str(item), item) for item in value
]
return remove_duplicates_from_list(representation_data)

View File

@@ -2,13 +2,15 @@ from decimal import Decimal
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.utils.translation import ugettext_lazy as _ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import ( from pretix.base.models import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
QuestionOption, Quota, Question, QuestionOption, Quota,
) )
@@ -110,6 +112,7 @@ class ItemSerializer(I18nAwareModelSerializer):
bundles = InlineItemBundleSerializer(many=True, required=False) bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False) variations = InlineItemVariationSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True) tax_rate = ItemTaxRateField(source='*', read_only=True)
meta_data = MetaDataField(required=False, source='*')
class Meta: class Meta:
model = Item model = Item
@@ -119,7 +122,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling', 'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations', 'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets', 'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard') 'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
read_only_fields = ('has_variations', 'picture') read_only_fields = ('has_variations', 'picture')
def validate(self, data): def validate(self, data):
@@ -167,18 +170,65 @@ class ItemSerializer(I18nAwareModelSerializer):
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count']) ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
return value return value
@cached_property
def item_meta_properties(self):
return {
p.name: p for p in self.context['request'].event.item_meta_properties.all()
}
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
if key not in self.item_meta_properties:
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
return value
@transaction.atomic @transaction.atomic
def create(self, validated_data): def create(self, validated_data):
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {} variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {} addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {} bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
item = Item.objects.create(**validated_data) item = Item.objects.create(**validated_data)
for variation_data in variations_data: for variation_data in variations_data:
ItemVariation.objects.create(item=item, **variation_data) ItemVariation.objects.create(item=item, **variation_data)
for addon_data in addons_data: for addon_data in addons_data:
ItemAddOn.objects.create(base_item=item, **addon_data) ItemAddOn.objects.create(base_item=item, **addon_data)
for bundle_data in bundles_data: for bundle_data in bundles_data:
ItemBundle.objects.create(base_item=item, **bundle_data) ItemBundle.objects.create(base_item=item, **bundle_data)
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
ItemMetaValue.objects.create(
property=self.item_meta_properties.get(key),
value=value,
item=item
)
return item
def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None)
item = super().update(instance, validated_data)
# Meta data
if meta_data is not None:
current = {mv.property: mv for mv in item.meta_values.select_related('property')}
for key, value in meta_data.items():
prop = self.item_meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
item.meta_values.create(
property=self.item_meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in meta_data:
current_object.delete()
return item return item
@@ -237,8 +287,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
if value: if value:
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE): if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
raise ValidationError('Question dependencies can only be set to boolean or choice questions.') raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
if value == self.instance: if value == self.instance:
raise ValidationError('A question cannot depend on itself.') raise ValidationError('A question cannot depend on itself.')
return value return value
def validate(self, data): def validate(self, data):

View File

@@ -5,7 +5,7 @@ from decimal import Decimal
import pycountry import pycountry
from django.db.models import F, Q from django.db.models import F, Q
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy from django.utils.translation import gettext_lazy
from django_countries.fields import Country from django_countries.fields import Country
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@@ -25,6 +25,7 @@ from pretix.base.models.orders import (
) )
from pretix.base.pdf import get_variables from pretix.base.pdf import get_variables
from pretix.base.services.cart import error_messages from pretix.base.services.cart import error_messages
from pretix.base.services.locking import NoLockManager
from pretix.base.services.pricing import get_price from pretix.base.services.pricing import get_price
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.base.signals import register_ticket_outputs from pretix.base.signals import register_ticket_outputs
@@ -38,7 +39,7 @@ class CompatibleCountryField(serializers.Field):
def to_representation(self, instance: InvoiceAddress): def to_representation(self, instance: InvoiceAddress):
if instance.country: if instance.country:
return str(instance.country) return str(instance.country)
else: elif hasattr(instance, 'country_old'):
return instance.country_old return instance.country_old
@@ -96,6 +97,11 @@ class AnswerQuestionOptionsIdentifierField(serializers.Field):
return [o.identifier for o in instance.options.all()] return [o.identifier for o in instance.options.all()]
class AnswerQuestionOptionsField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.pk for o in instance.options.all()]
class InlineSeatSerializer(I18nAwareModelSerializer): class InlineSeatSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
@@ -106,6 +112,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
class AnswerSerializer(I18nAwareModelSerializer): class AnswerSerializer(I18nAwareModelSerializer):
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True) question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True) option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
options = AnswerQuestionOptionsField(source='*', read_only=True)
class Meta: class Meta:
model = QuestionAnswer model = QuestionAnswer
@@ -189,6 +196,11 @@ class PdfDataSerializer(serializers.Field):
for k, v in ev._cached_meta_data.items(): for k, v in ev._cached_meta_data.items():
res['meta:' + k] = v res['meta:' + k] = v
if not hasattr(instance.item, '_cached_meta_data'):
instance.item._cached_meta_data = instance.item.meta_data
for k, v in instance.item._cached_meta_data.items():
res['itemmeta:' + k] = v
return res return res
@@ -199,10 +211,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True) order = serializers.SlugRelatedField(slug_field='code', read_only=True)
pdf_data = PdfDataSerializer(source='*') pdf_data = PdfDataSerializer(source='*')
seat = InlineSeatSerializer(read_only=True) seat = InlineSeatSerializer(read_only=True)
country = CompatibleCountryField(source='*')
class Meta: class Meta:
model = OrderPosition model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled') 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
@@ -504,12 +518,22 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
max_digits=10) max_digits=10)
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(), voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
required=False, allow_null=True) required=False, allow_null=True)
country = CompatibleCountryField(source='*')
class Meta: class Meta:
model = OrderPosition model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email', fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher') 'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for k, v in self.fields.items():
if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'):
v.required = False
v.allow_blank = True
v.allow_null = True
def validate_secret(self, secret): def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists(): if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
raise ValidationError( raise ValidationError(
@@ -564,6 +588,24 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
) )
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'): if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country')):
raise ValidationError(
{'country': ['Invalid country code.']}
)
if data.get('state'):
cc = str(data.get('country') or self.instance.country or '')
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(
{'state': ['States are not supported in country "{}".'.format(cc)]}
)
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
raise ValidationError(
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
)
return data return data
@@ -580,6 +622,28 @@ class CompatibleJSONField(serializers.JSONField):
return value return value
class WrappedList:
def __init__(self, data):
self._data = data
def all(self):
return self._data
class WrappedModel:
def __init__(self, model):
self._wrapped = model
def __getattr__(self, item):
return getattr(self._wrapped, item)
def save(self, *args, **kwargs):
raise NotImplementedError
def delete(self, *args, **kwargs):
raise NotImplementedError
class OrderCreateSerializer(I18nAwareModelSerializer): class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(required=False) invoice_address = InvoiceAddressSerializer(required=False)
positions = OrderPositionCreateSerializer(many=True, required=True) positions = OrderPositionCreateSerializer(many=True, required=True)
@@ -600,6 +664,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
force = serializers.BooleanField(default=False, required=False) force = serializers.BooleanField(default=False, required=False)
payment_date = serializers.DateTimeField(required=False, allow_null=True) payment_date = serializers.DateTimeField(required=False, allow_null=True)
send_mail = serializers.BooleanField(default=False, required=False) send_mail = serializers.BooleanField(default=False, required=False)
simulate = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -609,7 +674,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
model = Order model = Order
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel', fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_mail') 'force', 'send_mail', 'simulate')
def validate_payment_provider(self, pp): def validate_payment_provider(self, pp):
if pp is None: if pp is None:
@@ -701,6 +766,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_info = validated_data.pop('payment_info', '{}') payment_info = validated_data.pop('payment_info', '{}')
payment_date = validated_data.pop('payment_date', now()) payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False) force = validated_data.pop('force', False)
simulate = validated_data.pop('simulate', False)
self._send_mail = validated_data.pop('send_mail', False) self._send_mail = validated_data.pop('send_mail', False)
if 'invoice_address' in validated_data: if 'invoice_address' in validated_data:
@@ -714,7 +780,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else: else:
ia = None ia = None
with self.context['event'].lock() as now_dt: lockfn = self.context['event'].lock
if simulate:
lockfn = NoLockManager
with lockfn() as now_dt:
free_seats = set() free_seats = set()
seats_seen = set() seats_seen = set()
consume_carts = validated_data.pop('consume_carts', []) consume_carts = validated_data.pop('consume_carts', [])
@@ -823,7 +892,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else: else:
pos_data['seat'] = seat pos_data['seat'] = seat
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen: if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)] errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
seats_seen.add(seat) seats_seen.add(seat)
elif seated: elif seated:
errs[i]['seat'] = ['The specified product requires to choose a seat.'] errs[i]['seat'] = ['The specified product requires to choose a seat.']
@@ -838,7 +907,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if pos_data.get('variation') if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent'))) else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
if len(new_quotas) == 0: if len(new_quotas) == 0:
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format( errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item')) str(pos_data.get('item'))
)] )]
else: else:
@@ -850,7 +919,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
quota_avail_cache[quota][1] -= 1 quota_avail_cache[quota][1] -= 1
if quota_avail_cache[quota][1] < 0: if quota_avail_cache[quota][1] < 0:
errs[i]['item'] = [ errs[i]['item'] = [
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format( gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name quota.name
) )
] ]
@@ -864,11 +933,20 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
order.set_expires(subevents=[p.get('subevent') for p in positions_data]) order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.meta_info = "{}" order.meta_info = "{}"
order.total = Decimal('0.00') order.total = Decimal('0.00')
order.save() if simulate:
order = WrappedModel(order)
order.last_modified = now()
order.code = 'PREVIEW'
else:
order.save()
if ia: if ia:
ia.order = order if not simulate:
ia.save() ia.order = order
ia.save()
else:
order.invoice_address = ia
ia.last_modified = now()
pos_map = {} pos_map = {}
for pos_data in positions_data: for pos_data in positions_data:
@@ -880,7 +958,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
'_legacy': attendee_name '_legacy': attendee_name
} }
pos = OrderPosition(**pos_data) pos = OrderPosition(**pos_data)
pos.order = order if simulate:
pos.order = order._wrapped
else:
pos.order = order
if addon_to: if addon_to:
pos.addon_to = pos_map[addon_to] pos.addon_to = pos_map[addon_to]
@@ -911,19 +992,33 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address=ia, invoice_address=ia,
).gross ).gross
if pos.voucher: if simulate:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1) pos = WrappedModel(pos)
pos.save() pos.id = 0
answers = []
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = WrappedModel(QuestionAnswer(**answ_data))
answ.options = WrappedList(options)
answers.append(answ)
pos.answers = answers
pos.pseudonymization_id = "PREVIEW"
else:
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
pos_map[pos.positionid] = pos pos_map[pos.positionid] = pos
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
for cp in delete_cps: if not simulate:
cp.delete() for cp in delete_cps:
cp.delete()
order.total = sum([p.price for p in order.positions.all()]) order.total = sum([p.price for p in pos_map.values()])
fees = []
for fee_data in fees_data: for fee_data in fees_data:
is_percentage = fee_data.pop('_treat_value_as_percentage', False) is_percentage = fee_data.pop('_treat_value_as_percentage', False)
if is_percentage: if is_percentage:
@@ -955,17 +1050,26 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
fee_data['tax_rule'] = tr fee_data['tax_rule'] = tr
fee_data['value'] = val fee_data['value'] = val
f = OrderFee(**fee_data) f = OrderFee(**fee_data)
f.order = order f.order = order._wrapped if simulate else order
f._calculate_tax() f._calculate_tax()
f.save() fees.append(f)
if not simulate:
f.save()
else: else:
f = OrderFee(**fee_data) f = OrderFee(**fee_data)
f.order = order f.order = order._wrapped if simulate else order
f._calculate_tax() f._calculate_tax()
f.save() fees.append(f)
if not simulate:
f.save()
order.total += sum([f.value for f in order.fees.all()]) order.total += sum([f.value for f in fees])
order.save(update_fields=['total']) if simulate:
order.fees = fees
order.positions = pos_map.values()
return order # ignore payments
else:
order.save(update_fields=['total'])
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider: if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
payment_provider = 'free' payment_provider = 'free'

View File

@@ -1,7 +1,7 @@
from decimal import Decimal from decimal import Decimal
from django.db.models import Q from django.db.models import Q
from django.utils.translation import get_language, ugettext_lazy as _ from django.utils.translation import get_language, gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError

View File

@@ -2,7 +2,7 @@ import logging
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from oauth2_provider.exceptions import OAuthToolkitError from oauth2_provider.exceptions import OAuthToolkitError
from oauth2_provider.forms import AllowForm from oauth2_provider.forms import AllowForm
from oauth2_provider.views import ( from oauth2_provider.views import (

View File

@@ -9,7 +9,7 @@ from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse, HttpResponse from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from django_filters.rest_framework import DjangoFilterBackend, FilterSet from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from rest_framework import mixins, serializers, status, viewsets from rest_framework import mixins, serializers, status, viewsets
@@ -44,7 +44,7 @@ from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import ( from pretix.base.services.orders import (
OrderChangeManager, OrderError, _order_placed_email, OrderChangeManager, OrderError, _order_placed_email,
_order_placed_email_attendee, approve_order, cancel_order, deny_order, _order_placed_email_attendee, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded, extend_order, mark_order_expired, mark_order_refunded, reactivate_order,
) )
from pretix.base.services.pricing import get_price from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate from pretix.base.services.tickets import generate
@@ -261,6 +261,29 @@ class OrderViewSet(viewsets.ModelViewSet):
) )
return self.retrieve(request, [], **kwargs) return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST'])
def reactivate(self, request, **kwargs):
order = self.get_object()
if order.status != Order.STATUS_CANCELED:
return Response(
{'detail': 'The order is not allowed to be reactivated.'},
status=status.HTTP_400_BAD_REQUEST
)
try:
reactivate_order(
order,
user=request.user if request.user.is_authenticated else None,
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
)
except OrderError as e:
return Response(
{'detail': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST']) @action(detail=True, methods=['POST'])
def approve(self, request, **kwargs): def approve(self, request, **kwargs):
send_mail = request.data.get('send_email', True) send_mail = request.data.get('send_email', True)
@@ -466,6 +489,9 @@ class OrderViewSet(viewsets.ModelViewSet):
send_mail = serializer._send_mail send_mail = serializer._send_mail
order = serializer.instance order = serializer.instance
serializer = OrderSerializer(order, context=serializer.context) serializer = OrderSerializer(order, context=serializer.context)
if not order.pk:
# Simulation
return Response(serializer.data, status=status.HTTP_201_CREATED)
order.log_action( order.log_action(
'pretix.event.order.placed', 'pretix.event.order.placed',
@@ -1078,11 +1104,14 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
auth=request.auth auth=request.auth
) )
if mark_refunded: if mark_refunded:
mark_order_refunded( try:
r.order, mark_order_refunded(
user=request.user if request.user.is_authenticated else None, r.order,
auth=(request.auth if request.auth else None), user=request.user if request.user.is_authenticated else None,
) auth=(request.auth if request.auth else None),
)
except OrderError as e:
raise ValidationError(str(e))
elif mark_pending: elif mark_pending:
if r.order.status == Order.STATUS_PAID and r.order.pending_sum > 0: if r.order.status == Order.STATUS_PAID and r.order.pending_sum > 0:
r.order.status = Order.STATUS_PENDING r.order.status = Order.STATUS_PENDING

View File

@@ -1,8 +1,11 @@
from decimal import Decimal from decimal import Decimal
import django_filters
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import filters, serializers, status, viewsets from rest_framework import filters, serializers, status, viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
@@ -55,7 +58,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
write_permission = 'can_change_organizer_settings' write_permission = 'can_change_organizer_settings'
def get_queryset(self): def get_queryset(self):
return self.request.organizer.seating_plans.all() return self.request.organizer.seating_plans.order_by('name')
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
@@ -98,14 +101,29 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
instance.delete() instance.delete()
with scopes_disabled():
class GiftCardFilter(FilterSet):
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
class Meta:
model = GiftCard
fields = ['secret', 'testmode']
class GiftCardViewSet(viewsets.ModelViewSet): class GiftCardViewSet(viewsets.ModelViewSet):
serializer_class = GiftCardSerializer serializer_class = GiftCardSerializer
queryset = GiftCard.objects.none() queryset = GiftCard.objects.none()
permission = 'can_manage_gift_cards' permission = 'can_manage_gift_cards'
write_permission = 'can_manage_gift_cards' write_permission = 'can_manage_gift_cards'
filter_backends = (DjangoFilterBackend,)
filterset_class = GiftCardFilter
def get_queryset(self): def get_queryset(self):
return self.request.organizer.issued_gift_cards.all() if self.request.GET.get('include_accepted') == 'true':
qs = self.request.organizer.accepted_gift_cards
else:
qs = self.request.organizer.issued_gift_cards.all()
return qs
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
@@ -126,6 +144,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
@transaction.atomic() @transaction.atomic()
def perform_update(self, serializer): def perform_update(self, serializer):
if 'include_accepted' in self.request.GET:
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
GiftCard.objects.select_for_update().get(pk=self.get_object().pk) GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
old_value = serializer.instance.value old_value = serializer.instance.value
value = serializer.validated_data.pop('value') value = serializer.validated_data.pop('value')
@@ -148,16 +168,19 @@ class GiftCardViewSet(viewsets.ModelViewSet):
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value( value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('value') request.data.get('value')
) )
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
request.data.get('text', '')
)
if gc.value + value < Decimal('0.00'): if gc.value + value < Decimal('0.00'):
return Response({ return Response({
'value': ['The gift card does not have sufficient credit for this operation.'] 'value': ['The gift card does not have sufficient credit for this operation.']
}, status=status.HTTP_409_CONFLICT) }, status=status.HTTP_409_CONFLICT)
gc.transactions.create(value=value) gc.transactions.create(value=value, text=text)
gc.log_action( gc.log_action(
'pretix.giftcards.transaction.manual', 'pretix.giftcards.transaction.manual',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data={'value': value} data={'value': value, 'text': text}
) )
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK) return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
@@ -172,7 +195,7 @@ class TeamViewSet(viewsets.ModelViewSet):
write_permission = 'can_change_teams' write_permission = 'can_change_teams'
def get_queryset(self): def get_queryset(self):
return self.request.organizer.teams.all() return self.request.organizer.teams.order_by('pk')
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
@@ -245,7 +268,7 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team')) return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
def get_queryset(self): def get_queryset(self):
return self.team.invites.all() return self.team.invites.order_by('email')
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
@@ -282,7 +305,7 @@ class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team')) return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
def get_queryset(self): def get_queryset(self):
return self.team.tokens.all() return self.team.tokens.order_by('name')
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()

View File

@@ -7,7 +7,7 @@ import requests
from celery.exceptions import MaxRetriesExceededError from celery.exceptions import MaxRetriesExceededError
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled
from requests import RequestException from requests import RequestException
@@ -125,6 +125,10 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.canceled', 'pretix.event.order.canceled',
_('Order canceled'), _('Order canceled'),
), ),
ParametrizedOrderWebhookEvent(
'pretix.event.order.reactivated',
_('Order reactivated'),
),
ParametrizedOrderWebhookEvent( ParametrizedOrderWebhookEvent(
'pretix.event.order.expired', 'pretix.event.order.expired',
_('Order expired'), _('Order expired'),

View File

@@ -85,6 +85,16 @@ class BaseAuthBackend:
""" """
return return
def get_next_url(self, request):
"""
This method will be called after a successful login to determine the next URL. Pretix in general uses the
``'next'`` query parameter. However, external authentication methods could use custom attributes with hardcoded
names for security purposes. For example, OAuth uses ``'state'`` for keeping track of application state.
"""
if "next" in request.GET:
return request.GET.get("next")
return None
class NativeAuthBackend(BaseAuthBackend): class NativeAuthBackend(BaseAuthBackend):
identifier = 'native' identifier = 'native'

View File

@@ -2,7 +2,7 @@ import logging
from collections import OrderedDict from collections import OrderedDict
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.signals import register_sales_channels from pretix.base.signals import register_sales_channels

View File

@@ -9,7 +9,7 @@ from django.core.mail.backends.smtp import EmailBackend
from django.dispatch import receiver from django.dispatch import receiver
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import get_language, ugettext_lazy as _ from django.utils.translation import get_language, gettext_lazy as _
from inlinestyler.utils import inline_css from inlinestyler.utils import inline_css
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
@@ -267,6 +267,10 @@ def base_placeholders(sender, **kwargs):
SimpleFunctionalMailTextPlaceholder( SimpleFunctionalMailTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name 'event', ['event'], lambda event: event.name, lambda event: event.name
), ),
SimpleFunctionalMailTextPlaceholder(
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
lambda event_or_subevent: event_or_subevent.name
),
SimpleFunctionalMailTextPlaceholder( SimpleFunctionalMailTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug 'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
), ),
@@ -279,6 +283,11 @@ def base_placeholders(sender, **kwargs):
SimpleFunctionalMailTextPlaceholder( SimpleFunctionalMailTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency 'currency', ['event'], lambda event: event.currency, lambda event: event.currency
), ),
SimpleFunctionalMailTextPlaceholder(
'refund_amount', ['event_or_subevent', 'refund_amount'],
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
),
SimpleFunctionalMailTextPlaceholder( SimpleFunctionalMailTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total, 'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency), event.currency),

View File

@@ -7,7 +7,7 @@ from typing import Tuple
from defusedcsv import csv from defusedcsv import csv
from django import forms from django import forms
from django.utils.formats import localize from django.utils.formats import localize
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES from openpyxl.cell.cell import KNOWN_TYPES
@@ -180,9 +180,9 @@ class MultiSheetListExporter(ListExporter):
] ]
for s, l in self.sheets: for s, l in self.sheets:
choices += [ choices += [
(s + ':default', str(l) + ' ' + ugettext('CSV (with commas)')), (s + ':default', str(l) + ' ' + gettext('CSV (with commas)')),
(s + ':excel', str(l) + ' ' + ugettext('CSV (Excel-style)')), (s + ':excel', str(l) + ' ' + gettext('CSV (Excel-style)')),
(s + ':semicolon', str(l) + ' ' + ugettext('CSV (with semicolons)')), (s + ':semicolon', str(l) + ' ' + gettext('CSV (with semicolons)')),
] ]
ff = OrderedDict( ff = OrderedDict(
[ [

View File

@@ -5,7 +5,7 @@ from zipfile import ZipFile
from django import forms from django import forms
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import QuestionAnswer from pretix.base.models import QuestionAnswer

View File

@@ -6,7 +6,7 @@ import dateutil
from django import forms from django import forms
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext, ugettext_lazy from django.utils.translation import gettext, gettext_lazy
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Invoice, OrderPayment from pretix.base.models import Invoice, OrderPayment
@@ -79,7 +79,7 @@ class DekodiNREIExporter(BaseExporter):
payments.append({ payments.append({
'PTID': '5', 'PTID': '5',
'PTN': 'Lastschrift', 'PTN': 'Lastschrift',
'PTNo4': ugettext('Event ticket {event}-{code}').format( 'PTNo4': gettext('Event ticket {event}-{code}').format(
event=self.event.slug.upper(), event=self.event.slug.upper(),
code=invoice.order.code code=invoice.order.code
), ),
@@ -199,19 +199,19 @@ class DekodiNREIExporter(BaseExporter):
[ [
('date_from', ('date_from',
forms.DateField( forms.DateField(
label=ugettext_lazy('Start date'), label=gettext_lazy('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False, required=False,
help_text=ugettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does ' help_text=gettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.') 'not always correspond to the order or payment date.')
)), )),
('date_to', ('date_to',
forms.DateField( forms.DateField(
label=ugettext_lazy('End date'), label=gettext_lazy('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False, required=False,
help_text=ugettext_lazy('Only include invoices issued on or before this date. Note that the invoice date ' help_text=gettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.') 'does not always correspond to the order or payment date.')
)), )),
] ]
) )

View File

@@ -7,7 +7,7 @@ import dateutil.parser
from django import forms from django import forms
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import OrderPayment from pretix.base.models import OrderPayment

View File

@@ -2,7 +2,7 @@ from collections import OrderedDict
from django import forms from django import forms
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import OrderPosition from pretix.base.models import OrderPosition

View File

@@ -8,10 +8,10 @@ from django.db.models import (
) )
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.models import ( from pretix.base.models import (
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question, GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
) )
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -22,7 +22,7 @@ from ..signals import register_data_exporters
class OrderListExporter(MultiSheetListExporter): class OrderListExporter(MultiSheetListExporter):
identifier = 'orderlist' identifier = 'orderlist'
verbose_name = ugettext_lazy('Order data') verbose_name = gettext_lazy('Order data')
@property @property
def sheets(self): def sheets(self):
@@ -305,6 +305,12 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Attendee name') + ': ' + str(label)) headers.append(_('Attendee name') + ': ' + str(label))
headers += [ headers += [
_('Attendee email'), _('Attendee email'),
_('Company'),
_('Address'),
_('ZIP code'),
_('City'),
_('Country'),
pgettext('address', 'State'),
_('Voucher'), _('Voucher'),
_('Pseudonymization ID'), _('Pseudonymization ID'),
] ]
@@ -364,6 +370,12 @@ class OrderListExporter(MultiSheetListExporter):
) )
row += [ row += [
op.attendee_email, op.attendee_email,
op.company or '',
op.street or '',
op.zipcode or '',
op.city or '',
op.country if op.country else '',
op.state or '',
op.voucher.code if op.voucher else '', op.voucher.code if op.voucher else '',
op.pseudonymization_id, op.pseudonymization_id,
] ]
@@ -414,7 +426,7 @@ class OrderListExporter(MultiSheetListExporter):
class PaymentListExporter(ListExporter): class PaymentListExporter(ListExporter):
identifier = 'paymentlist' identifier = 'paymentlist'
verbose_name = ugettext_lazy('Order payments and refunds') verbose_name = gettext_lazy('Order payments and refunds')
@property @property
def additional_form_fields(self): def additional_form_fields(self):
@@ -485,7 +497,7 @@ class PaymentListExporter(ListExporter):
class QuotaListExporter(ListExporter): class QuotaListExporter(ListExporter):
identifier = 'quotalist' identifier = 'quotalist'
verbose_name = ugettext_lazy('Quota availabilities') verbose_name = gettext_lazy('Quota availabilities')
def iterate_list(self, form_data): def iterate_list(self, form_data):
headers = [ headers = [
@@ -514,7 +526,7 @@ class QuotaListExporter(ListExporter):
class InvoiceDataExporter(MultiSheetListExporter): class InvoiceDataExporter(MultiSheetListExporter):
identifier = 'invoicedata' identifier = 'invoicedata'
verbose_name = ugettext_lazy('Invoice data') verbose_name = gettext_lazy('Invoice data')
@property @property
def sheets(self): def sheets(self):
@@ -698,6 +710,45 @@ class InvoiceDataExporter(MultiSheetListExporter):
return '{}_invoices'.format(self.event.slug) return '{}_invoices'.format(self.event.slug)
class GiftcardRedemptionListExporter(ListExporter):
identifier = 'giftcardredemptionlist'
verbose_name = gettext_lazy('Giftcard Redemptions')
def iterate_list(self, form_data):
tz = pytz.timezone(self.event.settings.timezone)
payments = OrderPayment.objects.filter(
order__event=self.event,
provider='giftcard'
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event=self.event,
provider='giftcard'
).order_by('created')
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
headers = [
_('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
]
yield headers
for obj in objs:
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
row = [
obj.order.code,
obj.full_id,
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
gc.secret,
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
gc.issuer
]
yield row
def get_filename(self):
return '{}_giftcardredemptions'.format(self.event.slug)
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist") @receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
def register_orderlist_exporter(sender, **kwargs): def register_orderlist_exporter(sender, **kwargs):
return OrderListExporter return OrderListExporter
@@ -716,3 +767,8 @@ def register_quotalist_exporter(sender, **kwargs):
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata") @receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
def register_invoicedata_exporter(sender, **kwargs): def register_invoicedata_exporter(sender, **kwargs):
return InvoiceDataExporter return InvoiceDataExporter
@receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist")
def register_giftcardredemptionlist_exporter(sender, **kwargs):
return GiftcardRedemptionListExporter

View File

@@ -3,7 +3,6 @@ import logging
import i18nfield.forms import i18nfield.forms
from django import forms from django import forms
from django.forms.models import ModelFormMetaclass from django.forms.models import ModelFormMetaclass
from django.utils import six
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from formtools.wizard.views import SessionWizardView from formtools.wizard.views import SessionWizardView
from hierarkey.forms import HierarkeyForm from hierarkey.forms import HierarkeyForm
@@ -25,7 +24,7 @@ class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class I18nModelForm(six.with_metaclass(ModelFormMetaclass, BaseI18nModelForm)): class I18nModelForm(BaseI18nModelForm, metaclass=ModelFormMetaclass):
pass pass

View File

@@ -3,9 +3,10 @@ from django.conf import settings
from django.contrib.auth.password_validation import ( from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password, password_validators_help_texts, validate_password,
) )
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import User from pretix.base.models import User
from pretix.helpers.dicts import move_to_end
class LoginForm(forms.Form): class LoginForm(forms.Form):
@@ -36,7 +37,7 @@ class LoginForm(forms.Form):
if not settings.PRETIX_LONG_SESSIONS or backend.url: if not settings.PRETIX_LONG_SESSIONS or backend.url:
del self.fields['keep_logged_in'] del self.fields['keep_logged_in']
else: else:
self.fields.move_to_end('keep_logged_in') move_to_end(self.fields, 'keep_logged_in')
def clean(self): def clean(self):
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required): if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):

View File

@@ -18,7 +18,7 @@ from django.forms import Select
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ( from django.utils.translation import (
get_language, pgettext_lazy, ugettext_lazy as _, get_language, gettext_lazy as _, pgettext_lazy,
) )
from django_countries import countries from django_countries import countries
from django_countries.fields import Country, CountryField from django_countries.fields import Country, CountryField
@@ -41,6 +41,7 @@ from pretix.base.settings import (
) )
from pretix.base.templatetags.rich_text import rich_text from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import SplitDateTimeField from pretix.control.forms import SplitDateTimeField
from pretix.helpers.countries import CachedCountries
from pretix.helpers.escapejson import escapejson_attr from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.i18n import get_format_without_seconds from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields from pretix.presale.signals import question_form_fields
@@ -214,6 +215,10 @@ def guess_country(event):
return country return country
class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html'
class BaseQuestionsForm(forms.Form): class BaseQuestionsForm(forms.Form):
""" """
This form class is responsible for asking order-related questions. This includes This form class is responsible for asking order-related questions. This includes
@@ -241,7 +246,7 @@ class BaseQuestionsForm(forms.Form):
if item.admission and event.settings.attendee_names_asked: if item.admission and event.settings.attendee_names_asked:
self.fields['attendee_name_parts'] = NamePartsFormField( self.fields['attendee_name_parts'] = NamePartsFormField(
max_length=255, max_length=255,
required=event.settings.attendee_names_required, required=event.settings.attendee_names_required and not self.all_optional,
scheme=event.settings.name_scheme, scheme=event.settings.name_scheme,
titles=event.settings.name_scheme_titles, titles=event.settings.name_scheme_titles,
label=_('Attendee name'), label=_('Attendee name'),
@@ -249,7 +254,7 @@ class BaseQuestionsForm(forms.Form):
) )
if item.admission and event.settings.attendee_emails_asked: if item.admission and event.settings.attendee_emails_asked:
self.fields['attendee_email'] = forms.EmailField( self.fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required, required=event.settings.attendee_emails_required and not self.all_optional,
label=_('Attendee email'), label=_('Attendee email'),
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email), initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
widget=forms.EmailInput( widget=forms.EmailInput(
@@ -258,6 +263,75 @@ class BaseQuestionsForm(forms.Form):
} }
) )
) )
if item.admission and event.settings.attendee_company_asked:
self.fields['company'] = forms.CharField(
required=event.settings.attendee_company_required and not self.all_optional,
label=_('Company'),
initial=(cartpos.company if cartpos else orderpos.company),
)
if item.admission and event.settings.attendee_addresses_asked:
self.fields['street'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Address'),
widget=forms.Textarea(attrs={
'rows': 2,
'placeholder': _('Street and Number'),
'autocomplete': 'street-address'
}),
initial=(cartpos.street if cartpos else orderpos.street),
)
self.fields['zipcode'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('ZIP code'),
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
widget=forms.TextInput(attrs={
'autocomplete': 'postal-code',
}),
)
self.fields['city'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('City'),
initial=(cartpos.city if cartpos else orderpos.city),
widget=forms.TextInput(attrs={
'autocomplete': 'address-level2',
}),
)
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
self.fields['country'] = CountryField(
countries=CachedCountries
).formfield(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Country'),
initial=country,
widget=forms.Select(attrs={
'autocomplete': 'country',
}),
)
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
cc = None
if fprefix + 'country' in self.data:
cc = str(self.data[fprefix + 'country'])
elif country:
cc = str(country)
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
elif fprefix + 'state' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'state']
self.fields['state'] = forms.ChoiceField(
label=pgettext_lazy('address', 'State'),
required=False,
choices=c,
widget=forms.Select(attrs={
'autocomplete': 'address-level1',
}),
)
self.fields['state'].widget.is_required = True
for q in questions: for q in questions:
# Do we already have an answer? Provide it as the initial value # Do we already have an answer? Provide it as the initial value
@@ -309,12 +383,14 @@ class BaseQuestionsForm(forms.Form):
initial=initial.answer if initial else None, initial=initial.answer if initial else None,
) )
elif q.type == Question.TYPE_COUNTRYCODE: elif q.type == Question.TYPE_COUNTRYCODE:
field = CountryField().formfield( field = CountryField(
countries=CachedCountries
).formfield(
label=label, required=required, label=label, required=required,
help_text=help_text, help_text=help_text,
widget=forms.Select, widget=forms.Select,
empty_label='', empty_label='',
initial=initial.answer if initial else None, initial=initial.answer if initial else guess_country(event),
) )
elif q.type == Question.TYPE_CHOICE: elif q.type == Question.TYPE_CHOICE:
field = forms.ModelChoiceField( field = forms.ModelChoiceField(
@@ -332,7 +408,7 @@ class BaseQuestionsForm(forms.Form):
label=label, required=required, label=label, required=required,
help_text=help_text, help_text=help_text,
to_field_name='identifier', to_field_name='identifier',
widget=forms.CheckboxSelectMultiple, widget=QuestionCheckboxSelectMultiple,
initial=initial.options.all() if initial else None, initial=initial.options.all() if initial else None,
) )
elif q.type == Question.TYPE_FILE: elif q.type == Question.TYPE_FILE:
@@ -419,6 +495,10 @@ class BaseQuestionsForm(forms.Form):
def clean(self): def clean(self):
d = super().clean() d = super().clean()
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not d.get('state'):
self.add_error('state', _('This field is required.'))
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)} question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
def question_is_visible(parentid, qvals): def question_is_visible(parentid, qvals):
@@ -457,7 +537,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class Meta: class Meta:
model = InvoiceAddress model = InvoiceAddress
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state', fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
'vat_id', 'internal_reference', 'beneficiary') 'vat_id', 'internal_reference', 'beneficiary', 'custom_field')
widgets = { widgets = {
'is_business': BusinessBooleanRadio, 'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={ 'street': forms.Textarea(attrs={
@@ -500,6 +580,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not event.settings.invoice_address_vatid: if not event.settings.invoice_address_vatid:
del self.fields['vat_id'] del self.fields['vat_id']
self.fields['country'].choices = CachedCountries()
c = [('', pgettext_lazy('address', 'Select state'))] c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = self.prefix + '-' if self.prefix else '' fprefix = self.prefix + '-' if self.prefix else ''
cc = None cc = None
@@ -561,6 +643,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not event.settings.invoice_address_beneficiary: if not event.settings.invoice_address_beneficiary:
del self.fields['beneficiary'] del self.fields['beneficiary']
if event.settings.invoice_address_custom_field:
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
else:
del self.fields['custom_field']
for k, v in self.fields.items(): for k, v in self.fields.items():
if v.widget.attrs.get('autocomplete') or k == 'name_parts': if v.widget.attrs.get('autocomplete') or k == 'name_parts':
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '') v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')

View File

@@ -4,7 +4,7 @@ from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password, password_validators_help_texts, validate_password,
) )
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pytz import common_timezones from pytz import common_timezones
from pretix.base.models import User from pretix.base.models import User

View File

@@ -2,7 +2,7 @@ import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator from django.core.validators import BaseValidator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString

View File

@@ -4,7 +4,7 @@ from django import forms
from django.utils.formats import get_format from django.utils.formats import get_format
from django.utils.functional import lazy from django.utils.functional import lazy
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class DatePickerWidget(forms.DateInput): class DatePickerWidget(forms.DateInput):

View File

@@ -3,7 +3,7 @@ from contextlib import contextmanager
from django.conf import settings from django.conf import settings
from django.utils import translation from django.utils import translation
from django.utils.formats import date_format, number_format from django.utils.formats import date_format, number_format
from django.utils.translation import ugettext from django.utils.translation import gettext
from i18nfield.fields import ( # noqa from i18nfield.fields import ( # noqa
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput, I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
) )
@@ -69,6 +69,6 @@ class LazyLocaleException(Exception):
def __str__(self): def __str__(self):
if self.msgargs: if self.msgargs:
return ugettext(self.msg) % self.msgargs return gettext(self.msg) % self.msgargs
else: else:
return ugettext(self.msg) return gettext(self.msg)

View File

@@ -10,7 +10,7 @@ from django.contrib.staticfiles import finders
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.formats import date_format, localize from django.utils.formats import date_format, localize
from django.utils.translation import ( from django.utils.translation import (
get_language, pgettext, ugettext, ugettext_lazy, get_language, gettext, gettext_lazy, pgettext,
) )
from PIL.Image import BICUBIC from PIL.Image import BICUBIC
from reportlab.lib import pagesizes from reportlab.lib import pagesizes
@@ -264,7 +264,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas): def _draw_invoice_to(self, canvas):
p = Paragraph(self.invoice.address_invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal']) p = Paragraph(bleach.clean(self.invoice.address_invoice_to, tags=[]).strip().replace('\n', '<br />\n'),
style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height) p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height) p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
@@ -422,7 +423,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.saveState() canvas.saveState()
canvas.setFont('OpenSansBd', 30) canvas.setFont('OpenSansBd', 30)
canvas.setFillColorRGB(32, 0, 0) canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE')) canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
canvas.restoreState() canvas.restoreState()
def _on_first_page(self, canvas: Canvas, doc): def _on_first_page(self, canvas: Canvas, doc):
@@ -459,6 +460,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _get_intro(self): def _get_intro(self):
story = [] story = []
if self.invoice.custom_field:
story.append(Paragraph(
'{}: {}'.format(self.invoice.event.settings.invoice_address_custom_field, self.invoice.custom_field),
self.stylesheet['Normal']
))
if self.invoice.internal_reference: if self.invoice.internal_reference:
story.append(Paragraph( story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference), pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
@@ -681,7 +688,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
class Modern1Renderer(ClassicInvoiceRenderer): class Modern1Renderer(ClassicInvoiceRenderer):
identifier = 'modern1' identifier = 'modern1'
verbose_name = ugettext_lazy('Modern Invoice Renderer (pretix 2.7)') verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
bottom_margin = 16.9 * mm bottom_margin = 16.9 * mm
top_margin = 16.9 * mm top_margin = 16.9 * mm
right_margin = 20 * mm right_margin = 20 * mm

View File

@@ -13,11 +13,17 @@ class Command(BaseCommand):
return parser return parser
def handle(self, *args, **options): def handle(self, *args, **options):
try:
from django_extensions.management.commands import shell_plus # noqa
cmd = 'shell_plus'
except ImportError:
cmd = 'shell'
parser = self.create_parser(sys.argv[0], sys.argv[1]) parser = self.create_parser(sys.argv[0], sys.argv[1])
flags = parser.parse_known_args(sys.argv[2:])[1] flags = parser.parse_known_args(sys.argv[2:])[1]
if "--override" in flags: if "--override" in flags:
with scopes_disabled(): with scopes_disabled():
return call_command("shell_plus", *args, **options) return call_command(cmd, *args, **options)
lookups = {} lookups = {}
for flag in flags: for flag in flags:
@@ -36,4 +42,4 @@ class Command(BaseCommand):
for app_name, app_value in lookups.items() for app_name, app_value in lookups.items()
} }
with scope(**scope_options): with scope(**scope_options):
return call_command("shell_plus", *args, **options) return call_command(cmd, *args, **options)

View File

@@ -15,7 +15,9 @@ from django.utils.translation.trans_real import (
) )
from pretix.base.settings import GlobalSettingsObject from pretix.base.settings import GlobalSettingsObject
from pretix.multidomain.urlreverse import get_domain from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
)
_supported = None _supported = None
@@ -231,7 +233,10 @@ class SecurityMiddleware(MiddlewareMixin):
dynamicdomain += " " + settings.SITE_URL dynamicdomain += " " + settings.SITE_URL
if hasattr(request, 'organizer') and request.organizer: if hasattr(request, 'organizer') and request.organizer:
domain = get_domain(request.organizer) if hasattr(request, 'event') and request.event:
domain = get_event_domain(request.event, fallback=True)
else:
domain = get_organizer_domain(request.organizer)
if domain: if domain:
siteurlsplit = urlsplit(settings.SITE_URL) siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443): if siteurlsplit.port and siteurlsplit.port not in (80, 443):

View File

@@ -6,7 +6,7 @@ import django.core.validators
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
import pretix.base.validators import pretix.base.validators
from pretix.base.i18n import language from pretix.base.i18n import language

View File

@@ -7,7 +7,7 @@ from django.db import migrations, models
from django.db.models import F from django.db.models import F
from django.db.models.functions import Concat from django.db.models.functions import Concat
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
import pretix.base.models.auth import pretix.base.models.auth
import pretix.base.validators import pretix.base.validators

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.2.9 on 2020-02-18 08:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0143_auto_20200217_1211'),
]
operations = [
migrations.AddField(
model_name='invoiceaddress',
name='custom_field',
field=models.CharField(max_length=255, null=True, blank=True),
),
migrations.AddField(
model_name='invoice',
name='custom_field',
field=models.CharField(max_length=255, null=True),
),
]

View File

@@ -0,0 +1,42 @@
# Generated by Django 2.2.8 on 2020-02-10 10:38
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.base
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0144_invoiceaddress_custom_field'),
]
operations = [
migrations.CreateModel(
name='ItemMetaProperty',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(db_index=True, max_length=50)),
('default', models.TextField()),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_meta_properties', to='pretixbase.Event')),
],
options={
'abstract': False,
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='ItemMetaValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('value', models.TextField()),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.Item')),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_values', to='pretixbase.ItemMetaProperty')),
],
options={
'unique_together': {('item', 'property')},
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2020-03-02 11:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0145_auto_20200210_1038'),
]
operations = [
migrations.AddField(
model_name='giftcardtransaction',
name='text',
field=models.TextField(null=True),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 2.2.9 on 2020-03-21 15:12
from django.db import migrations, models
import pretix.base.models.auth
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0146_giftcardtransaction_text'),
]
operations = [
migrations.AddField(
model_name='user',
name='session_token',
field=models.CharField(default=pretix.base.models.auth.generate_session_token, max_length=32),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.4 on 2020-03-25 10:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0147_user_session_token'),
]
operations = [
migrations.CreateModel(
name='CancellationRequest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('cancellation_fee', models.DecimalField(decimal_places=2, max_digits=10)),
('refund_as_giftcard', models.BooleanField(default=False)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cancellation_requests', to='pretixbase.Order')),
],
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 3.0.4 on 2020-03-25 14:40
from django.db import migrations, models
from django.db.models import Count, OuterRef, Q, Subquery
from django.utils.timezone import now
def fill_cancellation_date(apps, schema_editor):
Order = apps.get_model('pretixbase', 'Order')
LogEntry = apps.get_model('pretixbase', 'LogEntry')
OrderPosition = apps.get_model('pretixbase', 'OrderPosition')
s = OrderPosition.all.filter(
order=OuterRef('pk'),
canceled=False,
).order_by().values('order').annotate(k=Count('id')).values('k')
for o in Order.objects.annotate(
pcnt=Subquery(s)
).filter(
Q(pcnt=0) | Q(pcnt__isnull=True) | Q(status="c")
).values('id').iterator():
le = LogEntry.objects.filter(
content_type__model="order",
object_id=o['id'],
action_type='pretix.event.order.canceled'
).order_by('-datetime').only('datetime').first()
if le:
Order.objects.filter(pk=o['id']).update(
cancellation_date=le.datetime,
last_modified=now()
)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0148_cancellationrequest'),
]
operations = [
migrations.AddField(
model_name='order',
name='cancellation_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.RunPython(fill_cancellation_date, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,74 @@
# Generated by Django 3.0.4 on 2020-04-01 11:24
import django_countries.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0149_order_cancellation_date'),
]
operations = [
migrations.AddField(
model_name='cartposition',
name='city',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='cartposition',
name='company',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='cartposition',
name='country',
field=django_countries.fields.CountryField(max_length=2, null=True),
),
migrations.AddField(
model_name='cartposition',
name='state',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='cartposition',
name='street',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='cartposition',
name='zipcode',
field=models.CharField(max_length=30, null=True),
),
migrations.AddField(
model_name='orderposition',
name='city',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='orderposition',
name='company',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='orderposition',
name='country',
field=django_countries.fields.CountryField(max_length=2, null=True),
),
migrations.AddField(
model_name='orderposition',
name='state',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='orderposition',
name='street',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='orderposition',
name='zipcode',
field=models.CharField(max_length=30, null=True),
),
]

View File

@@ -10,9 +10,9 @@ from .event import (
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
from .invoices import Invoice, InvoiceLine, invoice_filename from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import ( from .items import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
QuestionOption, Quota, SubEventItem, SubEventItemVariation, ItemVariation, Question, QuestionOption, Quota, SubEventItem,
itempicture_upload_to, SubEventItemVariation, itempicture_upload_to,
) )
from .log import LogEntry from .log import LogEntry
from .notifications import NotificationSetting from .notifications import NotificationSetting

View File

@@ -12,9 +12,9 @@ from django.contrib.auth.tokens import default_token_generator
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string, salted_hmac
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_otp.models import Device from django_otp.models import Device
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from u2flib_server.utils import ( from u2flib_server.utils import (
@@ -54,6 +54,10 @@ def generate_notifications_token():
return get_random_string(length=32) return get_random_string(length=32)
def generate_session_token():
return get_random_string(length=32)
class SuperuserPermissionSet: class SuperuserPermissionSet:
def __contains__(self, item): def __contains__(self, item):
return True return True
@@ -110,6 +114,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
) )
notifications_token = models.CharField(max_length=255, default=generate_notifications_token) notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
auth_backend = models.CharField(max_length=255, default='native') auth_backend = models.CharField(max_length=255, default='native')
session_token = models.CharField(max_length=32, default=generate_session_token)
objects = UserManager() objects = UserManager()
@@ -382,6 +387,20 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
self._staff_session_cache[session_key] = sess self._staff_session_cache[session_key] = sess
return self._staff_session_cache[session_key] return self._staff_session_cache[session_key]
def get_session_auth_hash(self):
"""
Return an HMAC that needs to
"""
key_salt = "pretix.base.models.User.get_session_auth_hash"
payload = self.password
payload += self.email
payload += self.session_token
return salted_hmac(key_salt, payload).hexdigest()
def update_session_token(self):
self.session_token = generate_session_token()
self.save(update_fields=['session_token'])
class StaffSession(models.Model): class StaffSession(models.Model):
user = models.ForeignKey('User', on_delete=models.PROTECT) user = models.ForeignKey('User', on_delete=models.PROTECT)

View File

@@ -1,7 +1,7 @@
from django.db import models from django.db import models
from django.db.models import Exists, OuterRef from django.db.models import Exists, OuterRef
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager from django_scopes import ScopedManager
from pretix.base.models import LoggedModel from pretix.base.models import LoggedModel

View File

@@ -3,7 +3,7 @@ import string
from django.db import models from django.db import models
from django.db.models import Max from django.db.models import Max
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
from pretix.base.models import LoggedModel from pretix.base.models import LoggedModel
@@ -94,6 +94,7 @@ class Device(LoggedModel):
return { return {
'can_view_orders', 'can_view_orders',
'can_change_orders', 'can_change_orders',
'can_manage_gift_cards'
} }
def get_event_permission_set(self, organizer, event) -> set: def get_event_permission_set(self, organizer, event) -> set:

View File

@@ -15,9 +15,10 @@ from django.db import models
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.template.defaultfilters import date as _date from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField, I18nTextField from i18nfield.fields import I18nCharField, I18nTextField
@@ -293,7 +294,7 @@ class Event(EventMixin, LoggedModel):
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."), "This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
validators=[ validators=[
RegexValidator( RegexValidator(
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]+$", regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*$",
message=_("The slug may only contain letters, numbers, dots and dashes."), message=_("The slug may only contain letters, numbers, dots and dashes."),
), ),
EventSlugBanlistValidator() EventSlugBanlistValidator()
@@ -370,6 +371,8 @@ class Event(EventMixin, LoggedModel):
""" """
self.settings.invoice_renderer = 'modern1' self.settings.invoice_renderer = 'modern1'
self.settings.invoice_include_expire_date = True self.settings.invoice_include_expire_date = True
self.settings.ticketoutput_pdf__enabled = True
self.settings.ticketoutput_passbook__enabled = True
@property @property
def social_image(self): def social_image(self):
@@ -385,7 +388,7 @@ class Event(EventMixin, LoggedModel):
if img: if img:
return urljoin(build_absolute_uri(self, 'presale:event.index'), img) return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
def free_seats(self, ignore_voucher=None, sales_channel='web'): def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .orders import CartPosition, Order, OrderPosition from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher from .vouchers import Voucher
vqs = Voucher.objects.filter( vqs = Voucher.objects.filter(
@@ -416,7 +419,7 @@ class Event(EventMixin, LoggedModel):
vqs vqs
) )
).filter(has_order=False, has_cart=False, has_voucher=False) ).filter(has_order=False, has_cart=False, has_voucher=False)
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel: if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False) qs = qs.filter(blocked=False)
return qs return qs
@@ -515,7 +518,7 @@ class Event(EventMixin, LoggedModel):
), tz) ), tz)
def copy_data_from(self, other): def copy_data_from(self, other):
from . import ItemAddOn, ItemCategory, Item, Question, Quota from . import ItemAddOn, ItemCategory, Item, Question, Quota, ItemMetaValue
from ..signals import event_copy_data from ..signals import event_copy_data
self.plugins = other.plugins self.plugins = other.plugins
@@ -540,6 +543,14 @@ class Event(EventMixin, LoggedModel):
c.save() c.save()
c.log_action('pretix.object.cloned') c.log_action('pretix.object.cloned')
item_meta_properties_map = {}
for imp in other.item_meta_properties.all():
item_meta_properties_map[imp.pk] = imp
imp.pk = None
imp.event = self
imp.save()
imp.log_action('pretix.object.cloned')
item_map = {} item_map = {}
variation_map = {} variation_map = {}
for i in Item.objects.filter(event=other).prefetch_related('variations'): for i in Item.objects.filter(event=other).prefetch_related('variations'):
@@ -561,6 +572,12 @@ class Event(EventMixin, LoggedModel):
v.item = i v.item = i
v.save() v.save()
for imv in ItemMetaValue.objects.filter(item__event=other).prefetch_related('item', 'property'):
imv.pk = None
imv.property = item_meta_properties_map[imv.property.pk]
imv.item = item_map[imv.item.pk]
imv.save()
for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'): for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'):
ia.pk = None ia.pk = None
ia.base_item = item_map[ia.base_item.pk] ia.base_item = item_map[ia.base_item.pk]
@@ -608,8 +625,10 @@ class Event(EventMixin, LoggedModel):
q.dependency_question = question_map[q.dependency_question_id] q.dependency_question = question_map[q.dependency_question_id]
q.save(update_fields=['dependency_question']) q.save(update_fields=['dependency_question'])
checkin_list_map = {}
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'): for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
items = list(cl.limit_products.all()) items = list(cl.limit_products.all())
checkin_list_map[cl.pk] = cl
cl.pk = None cl.pk = None
cl.event = self cl.event = self
cl.save() cl.save()
@@ -664,7 +683,7 @@ class Event(EventMixin, LoggedModel):
event_copy_data.send( event_copy_data.send(
sender=self, other=other, sender=self, other=other,
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map, tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
question_map=question_map question_map=question_map, checkin_list_map=checkin_list_map
) )
def get_payment_providers(self, cached=False) -> dict: def get_payment_providers(self, cached=False) -> dict:
@@ -1016,9 +1035,13 @@ class SubEvent(EventMixin, LoggedModel):
ordering = ("date_from", "name") ordering = ("date_from", "name")
def __str__(self): def __str__(self):
return '{} - {}'.format(self.name, self.get_date_range_display()) return '{} - {} {}'.format(
self.name,
self.get_date_range_display(),
date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") if self.settings.show_times else ""
).strip()
def free_seats(self, ignore_voucher=None, sales_channel='web'): def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .orders import CartPosition, Order, OrderPosition from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher from .vouchers import Voucher
vqs = Voucher.objects.filter( vqs = Voucher.objects.filter(
@@ -1052,7 +1075,7 @@ class SubEvent(EventMixin, LoggedModel):
vqs vqs
) )
).filter(has_order=False, has_cart=False, has_voucher=False) ).filter(has_order=False, has_cart=False, has_voucher=False)
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel: if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False) qs = qs.filter(blocked=False)
return qs return qs

View File

@@ -5,7 +5,7 @@ from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.banlist import banned from pretix.base.banlist import banned
from pretix.base.models import LoggedModel from pretix.base.models import LoggedModel
@@ -83,6 +83,7 @@ class GiftCard(LoggedModel):
class Meta: class Meta:
unique_together = (('secret', 'issuer'),) unique_together = (('secret', 'issuer'),)
ordering = ("issuance",)
class GiftCardTransaction(models.Model): class GiftCardTransaction(models.Model):
@@ -119,6 +120,7 @@ class GiftCardTransaction(models.Model):
blank=True, blank=True,
on_delete=models.PROTECT on_delete=models.PROTECT
) )
text = models.TextField(blank=True, null=True)
class Meta: class Meta:
ordering = ("datetime",) ordering = ("datetime",)

View File

@@ -111,6 +111,7 @@ class Invoice(models.Model):
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255) file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
internal_reference = models.TextField(blank=True) internal_reference = models.TextField(blank=True)
custom_field = models.CharField(max_length=255, null=True)
objects = ScopedManager(organizer='event__organizer') objects = ScopedManager(organizer='event__organizer')
@@ -120,13 +121,19 @@ class Invoice(models.Model):
@property @property
def full_invoice_from(self): def full_invoice_from(self):
taxidrow = ""
if self.invoice_from_tax_id:
if str(self.invoice_from_country) == "AU":
taxidrow = "ABN: %s" % self.invoice_from_tax_id
else:
taxidrow = pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id
parts = [ parts = [
self.invoice_from_name, self.invoice_from_name,
self.invoice_from, self.invoice_from,
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""), (self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
self.invoice_from_country.name if self.invoice_from_country else "", self.invoice_from_country.name if self.invoice_from_country else "",
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "", pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id if self.invoice_from_tax_id else "", taxidrow,
] ]
return '\n'.join([p.strip() for p in parts if p and p.strip()]) return '\n'.join([p.strip() for p in parts if p and p.strip()])
@@ -150,9 +157,12 @@ class Invoice(models.Model):
state_name = self.invoice_to_state state_name = self.invoice_to_state
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS: if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long': if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
state_name = pycountry.subdivisions.get( try:
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state) state_name = pycountry.subdivisions.get(
).name code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
).name
except:
pass
parts = [ parts = [
self.invoice_to_company, self.invoice_to_company,

View File

@@ -1,6 +1,6 @@
import sys import sys
import uuid import uuid
from collections import Counter from collections import Counter, OrderedDict
from datetime import date, datetime, time from datetime import date, datetime, time
from decimal import Decimal, DecimalException from decimal import Decimal, DecimalException
from typing import Tuple from typing import Tuple
@@ -9,13 +9,14 @@ import dateutil.parser
import pytz import pytz
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.db.models import F, Func, Q, Sum from django.db.models import F, Func, Q, Sum
from django.utils import formats from django.utils import formats
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware, now from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country from django_countries.fields import Country
from django_scopes import ScopedManager from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField from i18nfield.fields import I18nCharField, I18nTextField
@@ -454,7 +455,8 @@ class Item(LoggedModel):
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'), t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
rate=Decimal('0.00'), name='') rate=Decimal('0.00'), name='')
else: else:
t = self.tax_rule.tax(price, base_price_is=base_price_is, currency=currency) t = self.tax_rule.tax(price, base_price_is=base_price_is,
currency=currency or self.event.currency)
if include_bundled: if include_bundled:
for b in self.bundles.all(): for b in self.bundles.all():
@@ -591,6 +593,16 @@ class Item(LoggedModel):
if from_date > until_date: if from_date > until_date:
raise ValidationError(_('The item\'s availability cannot end before it starts.')) raise ValidationError(_('The item\'s availability cannot end before it starts.'))
@property
def meta_data(self):
data = {p.name: p.default for p in self.event.item_meta_properties.all()}
if hasattr(self, 'meta_values_cached'):
data.update({v.property.name: v.value for v in self.meta_values_cached})
else:
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
return OrderedDict((k, v) for k, v in sorted(data.items(), key=lambda k: k[0]))
class ItemVariation(models.Model): class ItemVariation(models.Model):
""" """
@@ -1105,10 +1117,13 @@ class Question(LoggedModel):
return None return None
if self.type == Question.TYPE_CHOICE: if self.type == Question.TYPE_CHOICE:
try: q = Q(identifier=answer)
return self.options.get(Q(pk=answer) | Q(identifier=answer)) if isinstance(answer, int) or answer.isdigit():
except: q |= Q(pk=answer)
o = self.options.filter(q).first()
if not o:
raise ValidationError(_('Invalid option selected.')) raise ValidationError(_('Invalid option selected.'))
return o
elif self.type == Question.TYPE_CHOICE_MULTIPLE: elif self.type == Question.TYPE_CHOICE_MULTIPLE:
if isinstance(answer, str): if isinstance(answer, str):
l_ = list(self.options.filter( l_ = list(self.options.filter(
@@ -1541,3 +1556,57 @@ class Quota(LoggedModel):
else: else:
if subevent: if subevent:
raise ValidationError(_('The subevent does not belong to this event.')) raise ValidationError(_('The subevent does not belong to this event.'))
class ItemMetaProperty(LoggedModel):
"""
An event can have ItemMetaProperty objects attached to define meta information fields
for its items. This information can be re-used for example in ticket layouts.
:param event: The event this property is defined for.
:type event: Event
:param name: Name
:type name: Name of the property, used in various places
:param default: Default value
:type default: str
"""
event = models.ForeignKey(Event, related_name="item_meta_properties", on_delete=models.CASCADE)
name = models.CharField(
max_length=50, db_index=True,
help_text=_(
"Can not contain spaces or special characters except underscores"
),
validators=[
RegexValidator(
regex="^[a-zA-Z0-9_]+$",
message=_("The property name may only contain letters, numbers and underscores."),
),
],
verbose_name=_("Name"),
)
default = models.TextField(blank=True)
class ItemMetaValue(LoggedModel):
"""
A meta-data value assigned to an item.
:param item: The item this metadata is valid for
:type item: Item
:param property: The property this value belongs to
:type property: ItemMetaProperty
:param value: The actual value
:type value: str
"""
item = models.ForeignKey('Item', on_delete=models.CASCADE, related_name='meta_values')
property = models.ForeignKey('ItemMetaProperty', on_delete=models.CASCADE, related_name='item_values')
value = models.TextField()
class Meta:
unique_together = ('item', 'property')
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)

View File

@@ -6,7 +6,7 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.signals import logentry_object_link from pretix.base.signals import logentry_object_link

View File

@@ -1,5 +1,5 @@
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class NotificationSetting(models.Model): class NotificationSetting(models.Model):

View File

@@ -4,6 +4,7 @@ import json
import logging import logging
import os import os
import string import string
from collections import Counter
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Union
@@ -25,7 +26,7 @@ from django.utils.encoding import escape_uri_path
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country, CountryField from django_countries.fields import Country, CountryField
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
@@ -43,6 +44,7 @@ from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete from pretix.base.signals import order_gracefully_delete
from ...helpers.countries import CachedCountries
from .base import LockModel, LoggedModel from .base import LockModel, LoggedModel
from .event import Event, SubEvent from .event import Event, SubEvent
from .items import Item, ItemVariation, Question, QuestionOption, Quota from .items import Item, ItemVariation, Question, QuestionOption, Quota
@@ -151,6 +153,9 @@ class Order(LockModel, LoggedModel):
datetime = models.DateTimeField( datetime = models.DateTimeField(
verbose_name=_("Date"), db_index=True verbose_name=_("Date"), db_index=True
) )
cancellation_date = models.DateTimeField(
null=True, blank=True
)
expires = models.DateTimeField( expires = models.DateTimeField(
verbose_name=_("Expiration date") verbose_name=_("Expiration date")
) )
@@ -426,7 +431,7 @@ class Order(LockModel, LoggedModel):
def cancel_allowed(self): def cancel_allowed(self):
return ( return (
self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and self.count_positions self.status in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED) and self.count_positions
) )
@cached_property @cached_property
@@ -448,16 +453,17 @@ class Order(LockModel, LoggedModel):
@cached_property @cached_property
def user_cancel_fee(self): def user_cancel_fee(self):
fee = Decimal('0.00') fee = Decimal('0.00')
if self.event.settings.cancel_allow_user_paid_keep:
fee += self.event.settings.cancel_allow_user_paid_keep
if self.event.settings.cancel_allow_user_paid_keep_percentage:
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * self.total
if self.event.settings.cancel_allow_user_paid_keep_fees: if self.event.settings.cancel_allow_user_paid_keep_fees:
fee += self.fees.filter( fee += self.fees.filter(
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE) fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
OrderFee.FEE_TYPE_CANCELLATION)
).aggregate( ).aggregate(
s=Sum('value') s=Sum('value')
)['s'] or 0 )['s'] or 0
if self.event.settings.cancel_allow_user_paid_keep_percentage:
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * (self.total - fee)
if self.event.settings.cancel_allow_user_paid_keep:
fee += self.event.settings.cancel_allow_user_paid_keep
return round_decimal(fee, self.event.currency) return round_decimal(fee, self.event.currency)
@property @property
@@ -468,6 +474,8 @@ class Order(LockModel, LoggedModel):
""" """
from .checkin import Checkin from .checkin import Checkin
if self.cancellation_requests.exists():
return False
positions = list( positions = list(
self.positions.all().annotate( self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'))) has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
@@ -693,16 +701,19 @@ class Order(LockModel, LoggedModel):
return self._is_still_available(count_waitinglist=count_waitinglist, force=force) return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False) -> Union[bool, str]: def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False,
check_voucher_usage=False) -> Union[bool, str]:
error_messages = { error_messages = {
'unavailable': _('The ordered product "{item}" is no longer available.'), 'unavailable': _('The ordered product "{item}" is no longer available.'),
'seat_unavailable': _('The seat "{seat}" is no longer available.'), 'seat_unavailable': _('The seat "{seat}" is no longer available.'),
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'), 'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
} }
now_dt = now_dt or now() now_dt = now_dt or now()
positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher') positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
quota_cache = {} quota_cache = {}
v_budget = {} v_budget = {}
v_usage = Counter()
try: try:
for i, op in enumerate(positions): for i, op in enumerate(positions):
if op.seat: if op.seat:
@@ -721,6 +732,13 @@ class Order(LockModel, LoggedModel):
)) ))
v_budget[op.voucher] -= disc v_budget[op.voucher] -= disc
if op.voucher and check_voucher_usage:
v_usage[op.voucher.pk] += 1
if v_usage[op.voucher.pk] + op.voucher.redeemed > op.voucher.max_usages:
raise Quota.QuotaExceededException(error_messages['voucher_usages'].format(
voucher=op.voucher.code
))
quotas = list(op.quotas) quotas = list(op.quotas)
if len(quotas) == 0: if len(quotas) == 0:
raise Quota.QuotaExceededException(error_messages['unavailable'].format( raise Quota.QuotaExceededException(error_messages['unavailable'].format(
@@ -1048,6 +1066,13 @@ class AbstractPosition(models.Model):
'Seat', null=True, blank=True, on_delete=models.PROTECT 'Seat', null=True, blank=True, on_delete=models.PROTECT
) )
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
country = CountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
class Meta: class Meta:
abstract = True abstract = True
@@ -2089,11 +2114,13 @@ class InvoiceAddress(models.Model):
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False) zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False) city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False) country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country')) country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True) state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'), vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.')) help_text=_('Only for business customers within the EU.'))
vat_id_validated = models.BooleanField(default=False) vat_id_validated = models.BooleanField(default=False)
custom_field = models.CharField(max_length=255, null=True, blank=True)
internal_reference = models.TextField( internal_reference = models.TextField(
verbose_name=_('Internal reference'), verbose_name=_('Internal reference'),
help_text=_('This reference will be printed on your invoice for your convenience.'), help_text=_('This reference will be printed on your invoice for your convenience.'),
@@ -2195,6 +2222,13 @@ class CachedCombinedTicket(models.Model):
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
class CancellationRequest(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='cancellation_requests')
created = models.DateTimeField(auto_now_add=True)
cancellation_fee = models.DecimalField(max_digits=10, decimal_places=2)
refund_as_giftcard = models.BooleanField(default=False)
@receiver(post_delete, sender=CachedTicket) @receiver(post_delete, sender=CachedTicket)
def cachedticket_delete(sender, instance, **kwargs): def cachedticket_delete(sender, instance, **kwargs):
if instance.file: if instance.file:

View File

@@ -5,7 +5,7 @@ from django.db import models
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models.base import LoggedModel from pretix.base.models.base import LoggedModel
from pretix.base.validators import OrganizerSlugBanlistValidator from pretix.base.validators import OrganizerSlugBanlistValidator

View File

@@ -8,7 +8,7 @@ from django.db import models
from django.db.models import F, Q from django.db.models import F, Q
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext, ugettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent

View File

@@ -4,7 +4,7 @@ from decimal import Decimal
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.formats import localize from django.utils.formats import localize
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField from django_countries.fields import CountryField
from i18nfield.fields import I18nCharField from i18nfield.fields import I18nCharField

View File

@@ -8,7 +8,7 @@ from django.db.models import F, OuterRef, Q, Subquery, Sum
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
from pretix.base.banlist import banned from pretix.base.banlist import banned

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models, transaction from django.db import models, transaction
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager from django_scopes import ScopedManager
from pretix.base.email import get_email_context from pretix.base.email import get_email_context

View File

@@ -3,7 +3,7 @@ from collections import OrderedDict, namedtuple
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import Event, LogEntry from pretix.base.models import Event, LogEntry
from pretix.base.signals import register_notification_types from pretix.base.signals import register_notification_types
@@ -223,6 +223,12 @@ def register_default_notification_types(sender, **kwargs):
_('Order canceled'), _('Order canceled'),
_('Order {order.code} has been canceled.') _('Order {order.code} has been canceled.')
), ),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.reactivated',
_('Order reactivated'),
_('Order {order.code} has been reactivated.')
),
ParametrizedOrderNotificationType( ParametrizedOrderNotificationType(
sender, sender,
'pretix.event.order.expired', 'pretix.event.order.expired',

View File

@@ -325,7 +325,7 @@ class InvoiceAddressState(ImportColumn):
@property @property
def verbose_name(self): def verbose_name(self):
return _('Invoice address') + ': ' + _('State') return _('Invoice address') + ': ' + pgettext('address', 'State')
def clean(self, value, previous_values): def clean(self, value, previous_values):
if value: if value:
@@ -398,6 +398,99 @@ class AttendeeEmail(ImportColumn):
position.attendee_email = value position.attendee_email = value
class AttendeeCompany(ImportColumn):
identifier = 'attendee_company'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('Company')
def assign(self, value, order, position, invoice_address, **kwargs):
position.company = value or ''
class AttendeeStreet(ImportColumn):
identifier = 'attendee_street'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('Address')
def assign(self, value, order, position, invoice_address, **kwargs):
position.address = value or ''
class AttendeeZip(ImportColumn):
identifier = 'attendee_zipcode'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('ZIP code')
def assign(self, value, order, position, invoice_address, **kwargs):
position.zipcode = value or ''
class AttendeeCity(ImportColumn):
identifier = 'attendee_city'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('City')
def assign(self, value, order, position, invoice_address, **kwargs):
position.city = value or ''
class AttendeeCountry(ImportColumn):
identifier = 'attendee_country'
default_value = None
@property
def initial(self):
return 'static:' + str(guess_country(self.event))
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('Country')
def static_choices(self):
return list(countries)
def clean(self, value, previous_values):
if value and not Country(value).numeric:
raise ValidationError(_("Please enter a valid country code."))
return value
def assign(self, value, order, position, invoice_address, **kwargs):
position.country = value or ''
class AttendeeState(ImportColumn):
identifier = 'attendee_state'
@property
def verbose_name(self):
return _('Attendee address') + ': ' + _('State')
def clean(self, value, previous_values):
if value:
if previous_values.get('attendee_country') not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(_("States are not supported for this country."))
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[previous_values.get('attendee_country')]
match = [
s for s in pycountry.subdivisions.get(country_code=previous_values.get('attendee_country'))
if s.type in types and (s.code[3:] == value or s.name == value)
]
if len(match) == 0:
raise ValidationError(_("Please enter a valid state."))
return match[0].code[3:]
def assign(self, value, order, position, invoice_address, **kwargs):
position.state = value or ''
class Price(ImportColumn): class Price(ImportColumn):
identifier = 'price' identifier = 'price'
verbose_name = gettext_lazy('Price') verbose_name = gettext_lazy('Price')
@@ -596,6 +689,12 @@ def get_all_columns(event):
default.append(AttendeeNamePart(event, n, l)) default.append(AttendeeNamePart(event, n, l))
default += [ default += [
AttendeeEmail(event), AttendeeEmail(event),
AttendeeCompany(event),
AttendeeStreet(event),
AttendeeZip(event),
AttendeeCity(event),
AttendeeCountry(event),
AttendeeState(event),
Price(event), Price(event),
Secret(event), Secret(event),
Locale(event), Locale(event),

View File

@@ -17,7 +17,7 @@ from django.http import HttpRequest
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import Countries from django_countries import Countries
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
@@ -29,6 +29,7 @@ from pretix.base.models import (
OrderRefund, Quota, OrderRefund, Quota,
) )
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.services.cart import get_fees
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
@@ -1106,8 +1107,16 @@ class GiftCardPayment(BasePaymentProvider):
return return
cs['gift_cards'] = cs['gift_cards'] + [gc.pk] cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
remainder = cart['total'] - gc.value total = sum(p.total for p in cart['positions'])
if remainder >= Decimal('0.00'): # Recompute fees. Some plugins, e.g. pretix-servicefees, change their fee schedule if a gift card is
# applied.
fees = get_fees(
self.event, request, total, cart['invoice_address'], cs.get('payment'),
cart['raw']
)
total += sum([f.value for f in fees])
remainder = total - gc.value
if remainder > Decimal('0.00'):
del cs['payment'] del cs['payment']
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format( messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
money_filter(remainder, self.event.currency) money_filter(remainder, self.event.currency)
@@ -1210,6 +1219,7 @@ class GiftCardPayment(BasePaymentProvider):
) )
refund.info_data = { refund.info_data = {
'gift_card': gc.pk, 'gift_card': gc.pk,
'gift_card_code': gc.secret,
'transaction_id': trans.pk, 'transaction_id': trans.pk,
} }
refund.done() refund.done()

View File

@@ -17,7 +17,7 @@ from django.dispatch import receiver
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from PyPDF2 import PdfFileReader from PyPDF2 import PdfFileReader
from pytz import timezone from pytz import timezone
from reportlab.graphics import renderPDF from reportlab.graphics import renderPDF
@@ -205,6 +205,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Sample city"), "editor_sample": _("Sample city"),
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else '' "evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
}), }),
("attendee_company", {
"label": _("Attendee company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
}),
("addons", { ("addons", {
"label": _("List of Add-Ons"), "label": _("List of Add-Ons"),
"editor_sample": _("Addon 1\nAddon 2"), "editor_sample": _("Addon 1\nAddon 2"),
@@ -431,6 +436,8 @@ class Renderer:
return '(error)' return '(error)'
if o['content'] == 'other': if o['content'] == 'other':
return o['text'] return o['text']
elif o['content'].startswith('itemmeta:'):
return op.item.meta_data.get(o['content'][9:]) or ''
elif o['content'].startswith('meta:'): elif o['content'].startswith('meta:'):
return ev.meta_data.get(o['content'][5:]) or '' return ev.meta_data.get(o['content'][5:]) or ''
elif o['content'] in self.variables: elif o['content'] in self.variables:

View File

@@ -7,7 +7,7 @@ from dateutil import parser
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
BASE_CHOICES = ( BASE_CHOICES = (
@@ -324,7 +324,7 @@ class ModelRelativeDateTimeField(models.CharField):
return value.to_string() return value.to_string()
return value return value
def from_db_value(self, value, expression, connection, context): def from_db_value(self, value, expression, connection):
if value is None: if value is None:
return None return None
return RelativeDateWrapper.from_string(value) return RelativeDateWrapper.from_string(value)

View File

@@ -0,0 +1,224 @@
import logging
from decimal import Decimal
from django.db import transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery
from i18nfield.strings import LazyI18nString
from pretix.base.decimal import round_decimal
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
Event, InvoiceAddress, Order, OrderFee, OrderPosition, OrderRefund,
SubEvent, User, WaitingListEntry,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException, TolerantDict, mail
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _cancel_order, _try_auto_refund,
)
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
logger = logging.getLogger(__name__)
def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent):
with language(wle.locale):
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
try:
mail(
wle.email,
str(subject).format_map(TolerantDict(email_context)),
message,
email_context,
wle.event,
locale=wle.locale
)
except SendMailException:
logger.exception('Waiting list canceled email could not be sent')
def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent,
refund_amount: Decimal, user: User, positions: list):
with language(order.locale):
try:
ia = order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = InvoiceAddress()
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
order=order, position_or_address=ia, event=order.event)
real_subject = str(subject).format_map(TolerantDict(email_context))
try:
order.send_mail(
real_subject, message, email_context,
'pretix.event.order.email.event_canceled',
user,
)
except SendMailException:
logger.exception('Order canceled email could not be sent')
for p in positions:
if subevent and p.subevent_id != subevent.id:
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
real_subject = str(subject).format_map(TolerantDict(email_context))
email_context = get_email_context(event_or_subevent=subevent or order.event,
event=order.event,
refund_amount=refund_amount,
position_or_address=p,
order=order, position=p)
try:
order.send_mail(
real_subject, message, email_context,
'pretix.event.order.email.event_canceled',
position=p,
user=user
)
except SendMailException:
logger.exception('Order canceled email could not be sent to attendee')
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
send: bool=False, send_subject: dict=None, send_message: dict=None,
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
user: int=None, refund_as_giftcard: bool=False):
send_subject = LazyI18nString(send_subject)
send_message = LazyI18nString(send_message)
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
send_waitinglist_message = LazyI18nString(send_waitinglist_message)
if user:
user = User.objects.get(pk=user)
s = OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(k=Count('id')).values('k')
orders_to_cancel = event.orders.annotate(pcnt=Subquery(s, output_field=IntegerField())).filter(
status__in=[Order.STATUS_PAID, Order.STATUS_PENDING, Order.STATUS_EXPIRED],
pcnt__gt=0
).all()
if subevent:
subevent = event.subevents.get(pk=subevent)
has_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).filter(
subevent=subevent
)
has_other_subevent = OrderPosition.objects.filter(order_id=OuterRef('pk')).exclude(
subevent=subevent
)
orders_to_change = orders_to_cancel.annotate(
has_subevent=Exists(has_subevent),
has_other_subevent=Exists(has_other_subevent),
).filter(
has_subevent=True, has_other_subevent=True
)
orders_to_cancel = orders_to_cancel.annotate(
has_subevent=Exists(has_subevent),
has_other_subevent=Exists(has_other_subevent),
).filter(
has_subevent=True, has_other_subevent=False
)
subevent.log_action(
'pretix.subevent.canceled', user=user,
)
subevent.active = False
subevent.save(update_fields=['active'])
subevent.log_action(
'pretix.subevent.changed', user=user, data={'active': False, '_source': 'cancel_event'}
)
else:
orders_to_change = event.orders.none()
event.log_action(
'pretix.event.canceled', user=user,
)
for i in event.items.filter(active=True):
i.active = False
i.save(update_fields=['active'])
i.log_action(
'pretix.event.item.changed', user=user, data={'active': False, '_source': 'cancel_event'}
)
failed = 0
for o in orders_to_cancel.only('id', 'total'):
try:
fee = Decimal('0.00')
fee_sum = Decimal('0.00')
keep_fee_objects = []
if keep_fees:
for f in o.fees.all():
if f.fee_type in keep_fees:
fee += f.value
keep_fee_objects.append(f)
fee_sum += f.value
if keep_fee_percentage:
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum)
if keep_fee_fixed:
fee += Decimal(keep_fee_fixed)
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects)
refund_amount = o.payment_refund_sum
try:
if auto_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard)
finally:
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
except LockTimeoutException:
logger.exception("Could not cancel order")
failed += 1
except OrderError:
logger.exception("Could not cancel order")
failed += 1
for o in orders_to_change.values_list('id', flat=True):
with transaction.atomic():
o = event.orders.select_for_update().get(pk=o)
total = Decimal('0.00')
positions = []
ocm = OrderChangeManager(o, user=user, notify=False)
for p in o.positions.all():
if p.subevent == subevent:
total += p.price
ocm.cancel(p)
positions.append(p)
fee = Decimal('0.00')
if keep_fee_fixed:
fee += Decimal(keep_fee_fixed)
if keep_fee_percentage:
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * total
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
if fee:
f = OrderFee(
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
value=fee,
order=o,
tax_rule=o.event.settings.tax_rate_default,
)
f._calculate_tax()
ocm.add_fee(f)
ocm.commit()
refund_amount = o.payment_refund_sum - o.total
if auto_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN)
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions)
for wle in event.waitinglistentries.filter(subevent=subevent, voucher__isnull=True):
_send_wle_mail(wle, send_waitinglist_subject, send_waitinglist_message, subevent)
return failed

View File

@@ -9,7 +9,7 @@ from django.db import DatabaseError, transaction
from django.db.models import Count, Exists, OuterRef, Q from django.db.models import Count, Exists, OuterRef, Q
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext as _ from django.utils.translation import gettext as _, pgettext_lazy
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
@@ -213,7 +213,7 @@ class CartManager:
has_variations=Count('variations'), has_variations=Count('variations'),
).filter( ).filter(
id__in=[i for i in item_ids if i and i not in self._items_cache] id__in=[i for i in item_ids if i and i not in self._items_cache]
) ).order_by()
}) })
self._variations_cache.update({ self._variations_cache.update({
v.pk: v v.pk: v
@@ -221,7 +221,7 @@ class CartManager:
'quotas' 'quotas'
).select_related('item', 'item__event').filter( ).select_related('item', 'item__event').filter(
id__in=[i for i in variation_ids if i and i not in self._variations_cache] id__in=[i for i in variation_ids if i and i not in self._variations_cache]
) ).order_by()
}) })
def _check_max_cart_size(self): def _check_max_cart_size(self):
@@ -303,32 +303,6 @@ class CartManager:
if op.item.require_bundling and not op.addon_to == 'FAKE': if op.item.require_bundling and not op.addon_to == 'FAKE':
raise CartError(error_messages['bundled_only']) raise CartError(error_messages['bundled_only'])
if op.item.max_per_order or op.item.min_per_order:
new_total = (
len([1 for p in self.positions if p.item_id == op.item.pk]) +
sum([_op.count for _op in self._operations + current_ops
if isinstance(_op, self.AddOperation) and _op.item == op.item]) +
op.count -
len([1 for _op in self._operations + current_ops
if isinstance(_op, self.RemoveOperation) and _op.position.item_id == op.item.pk])
)
if op.item.max_per_order and new_total > op.item.max_per_order:
raise CartError(
_(error_messages['max_items_per_product']) % {
'max': op.item.max_per_order,
'product': op.item.name
}
)
if op.item.min_per_order and new_total < op.item.min_per_order:
raise CartError(
_(error_messages['min_items_per_product']) % {
'min': op.item.min_per_order,
'product': op.item.name
}
)
def _get_price(self, item: Item, variation: Optional[ItemVariation], def _get_price(self, item: Item, variation: Optional[ItemVariation],
voucher: Optional[Voucher], custom_price: Optional[Decimal], voucher: Optional[Voucher], custom_price: Optional[Decimal],
subevent: Optional[SubEvent], cp_is_net: bool=None, force_custom_price=False, subevent: Optional[SubEvent], cp_is_net: bool=None, force_custom_price=False,
@@ -787,37 +761,48 @@ class CartManager:
return vouchers_ok return vouchers_ok
def _check_min_per_product(self): def _check_min_max_per_product(self):
per_product = Counter() items = Counter()
min_per_product = {}
for p in self.positions: for p in self.positions:
per_product[p.item_id] += 1 items[p.item] += 1
min_per_product[p.item.pk] = p.item.min_per_order
for op in self._operations: for op in self._operations:
if isinstance(op, self.AddOperation): if isinstance(op, self.AddOperation):
per_product[op.item.pk] += op.count items[op.item] += op.count
min_per_product[op.item.pk] = op.item.min_per_order
elif isinstance(op, self.RemoveOperation): elif isinstance(op, self.RemoveOperation):
per_product[op.position.item_id] -= 1 items[op.position.item] -= 1
min_per_product[op.position.item.pk] = op.position.item.min_per_order
err = None err = None
for itemid, num in per_product.items(): for item, count in items.items():
min_p = min_per_product[itemid] if count == 0:
if min_p and num < min_p: continue
if item.max_per_order and count > item.max_per_order:
raise CartError(
_(error_messages['max_items_per_product']) % {
'max': item.max_per_order,
'product': item.name
}
)
if item.min_per_order and count < item.min_per_order:
self._operations = [o for o in self._operations if not ( self._operations = [o for o in self._operations if not (
isinstance(o, self.AddOperation) and o.item.pk == itemid isinstance(o, self.AddOperation) and o.item.pk == item.pk
)] )]
removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)] removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)]
for p in self.positions: for p in self.positions:
if p.item_id == itemid and p.pk not in removals: if p.item_id == item.pk and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p)) self._operations.append(self.RemoveOperation(position=p))
err = _(error_messages['min_items_per_product_removed']) % { err = _(error_messages['min_items_per_product_removed']) % {
'min': min_p, 'min': item.min_per_order,
'product': p.item.name 'product': item.name
} }
if not err:
raise CartError(
_(error_messages['min_items_per_product']) % {
'min': item.min_per_order,
'product': item.name
}
)
return err return err
def _perform_operations(self): def _perform_operations(self):
@@ -826,7 +811,7 @@ class CartManager:
err = None err = None
new_cart_positions = [] new_cart_positions = []
err = err or self._check_min_per_product() err = err or self._check_min_max_per_product()
self._operations.sort(key=lambda a: self.order[type(a)]) self._operations.sort(key=lambda a: self.order[type(a)])
seats_seen = set() seats_seen = set()

View File

@@ -2,7 +2,7 @@ from django.db import transaction
from django.db.models import Prefetch from django.db.models import Prefetch
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from pretix.base.models import ( from pretix.base.models import (
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption, Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,

View File

@@ -2,7 +2,7 @@ from typing import Any, Dict
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.utils.timezone import override from django.utils.timezone import override
from django.utils.translation import ugettext from django.utils.translation import gettext
from pretix.base.i18n import LazyLocaleException, language from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import CachedFile, Event, cachedfile_name from pretix.base.models import CachedFile, Event, cachedfile_name
@@ -26,7 +26,7 @@ def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any])
d = ex.render(form_data) d = ex.render(form_data)
if d is None: if d is None:
raise ExportError( raise ExportError(
ugettext('Your export did not contain any data.') gettext('Your export did not contain any data.')
) )
file.filename, file.type, data = d file.filename, file.type, data = d
file.file.save(cachedfile_name(file, file.filename), ContentFile(data)) file.file.save(cachedfile_name(file, file.filename), ContentFile(data))

View File

@@ -15,7 +15,7 @@ from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import pgettext, ugettext as _ from django.utils.translation import gettext as _, pgettext
from django_countries.fields import Country from django_countries.fields import Country
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
@@ -37,6 +37,10 @@ logger = logging.getLogger(__name__)
@transaction.atomic @transaction.atomic
def build_invoice(invoice: Invoice) -> Invoice: def build_invoice(invoice: Invoice) -> Invoice:
invoice.locale = invoice.event.settings.get('invoice_language', invoice.event.settings.locale)
if invoice.locale == '__user__':
invoice.locale = invoice.order.locale or invoice.event.settings.locale
lp = invoice.order.payments.last() lp = invoice.order.payments.last()
with language(invoice.locale): with language(invoice.locale):
@@ -85,6 +89,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
).split("\n") if a.strip() ).split("\n") if a.strip()
) )
invoice.internal_reference = ia.internal_reference invoice.internal_reference = ia.internal_reference
invoice.custom_field = ia.custom_field
invoice.invoice_to_company = ia.company invoice.invoice_to_company = ia.company
invoice.invoice_to_name = ia.name invoice.invoice_to_name = ia.name
invoice.invoice_to_street = ia.street invoice.invoice_to_street = ia.street
@@ -249,17 +254,11 @@ def regenerate_invoice(invoice: Invoice):
def generate_invoice(order: Order, trigger_pdf=True): def generate_invoice(order: Order, trigger_pdf=True):
locale = order.event.settings.get('invoice_language', order.event.settings.locale)
if locale:
if locale == '__user__':
locale = order.locale or order.event.settings.locale
invoice = Invoice( invoice = Invoice(
order=order, order=order,
event=order.event, event=order.event,
organizer=order.event.organizer, organizer=order.event.organizer,
date=timezone.now().date(), date=timezone.now().date(),
locale=locale
) )
invoice = build_invoice(invoice) invoice = build_invoice(invoice)
if trigger_pdf: if trigger_pdf:
@@ -313,7 +312,7 @@ def build_preview_invoice_pdf(event):
with rolledback_transaction(), language(locale): with rolledback_transaction(), language(locale):
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(), order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
expires=timezone.now(), code="PREVIEW", total=119) expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
invoice = Invoice( invoice = Invoice(
order=order, event=event, invoice_no="PREVIEW", order=order, event=event, invoice_no="PREVIEW",
date=timezone.now().date(), locale=locale, organizer=event.organizer date=timezone.now().date(), locale=locale, organizer=event.organizer
@@ -351,7 +350,7 @@ def build_preview_invoice_pdf(event):
if event.tax_rules.exists(): if event.tax_rules.exists():
for i, tr in enumerate(event.tax_rules.all()): for i, tr in enumerate(event.tax_rules.all()):
tax = tr.tax(Decimal('100.00')) tax = tr.tax(Decimal('100.00'), base_price_is='gross')
InvoiceLine.objects.create( InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product {}").format(i + 1), invoice=invoice, description=_("Sample product {}").format(i + 1),
gross_value=tax.gross, tax_value=tax.tax, gross_value=tax.gross, tax_value=tax.tax,

View File

@@ -20,7 +20,7 @@ from django.core.mail import (
) )
from django.core.mail.message import SafeMIMEText from django.core.mail.message import SafeMIMEText
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import pgettext, ugettext as _ from django.utils.translation import gettext as _, pgettext
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
@@ -126,7 +126,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
renderer = ClassicMailRenderer(None) renderer = ClassicMailRenderer(None)
content_plain = body_plain = render_mail(template, context) content_plain = body_plain = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context)) subject = str(subject).format_map(TolerantDict(context))
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM) sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM) or settings.MAIL_FROM
if event: if event:
sender_name = event.settings.mail_from_name or str(event.name) sender_name = event.settings.mail_from_name or str(event.name)
sender = formataddr((sender_name, sender)) sender = formataddr((sender_name, sender))
@@ -276,20 +276,6 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
cm = lambda: scopes_disabled() # noqa cm = lambda: scopes_disabled() # noqa
with cm(): with cm():
if invoices:
invoices = Invoice.objects.filter(pk__in=invoices)
for inv in invoices:
if inv.file:
try:
with language(inv.order.locale):
email.attach(
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
inv.file.file.read(),
'application/pdf'
)
except:
logger.exception('Could not attach invoice to email')
pass
if event: if event:
if order: if order:
try: try:
@@ -344,6 +330,21 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user) email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
if invoices:
invoices = Invoice.objects.filter(pk__in=invoices)
for inv in invoices:
if inv.file:
try:
with language(inv.order.locale):
email.attach(
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
inv.file.file.read(),
'application/pdf'
)
except:
logger.exception('Could not attach invoice to email')
pass
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order) email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
try: try:

View File

@@ -1,5 +1,6 @@
from django.conf import settings from django.conf import settings
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.timezone import override
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled
from inlinestyler.utils import inline_css from inlinestyler.utils import inline_css
@@ -79,7 +80,7 @@ def send_notification(logentry_id: int, action_type: str, user_id: int, method:
if not notification_type: if not notification_type:
return # Ignore, e.g. plugin not active for this event return # Ignore, e.g. plugin not active for this event
with language(user.locale): with language(user.locale), override(logentry.event.timezone if logentry.event else user.timezone):
notification = notification_type.build_notification(logentry) notification = notification_type.build_notification(logentry)
if method == "mail": if method == "mail":

View File

@@ -43,7 +43,11 @@ def parse_csv(file, length=None):
if '\r' in data and '\n' not in data: if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n') data = data.replace('\r', '\n')
dialect = csv.Sniffer().sniff(data.split("\n")[0], delimiters=";,.#:") try:
dialect = csv.Sniffer().sniff(data.split("\n")[0], delimiters=";,.#:")
except csv.Error:
return None
if dialect is None: if dialect is None:
return None return None

View File

@@ -15,7 +15,7 @@ from django.db.transaction import get_connection
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication from pretix.api.models import OAuthApplication
@@ -94,6 +94,54 @@ def mark_order_paid(*args, **kwargs):
raise NotImplementedError("This method is no longer supported since pretix 1.17.") raise NotImplementedError("This method is no longer supported since pretix 1.17.")
def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None):
"""
Reactivates a canceled order. If ``force`` is not set to ``True``, this will fail if there is not
enough quota.
"""
if order.status != Order.STATUS_CANCELED:
raise OrderError('The order was not canceled.')
with order.event.lock() as now_dt:
is_available = force or order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True)
if is_available is True:
if order.payment_refund_sum >= order.total:
order.status = Order.STATUS_PAID
else:
order.status = Order.STATUS_PENDING
order.cancellation_date = None
order.set_expires(now(),
order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
with transaction.atomic():
order.save(update_fields=['expires', 'status', 'cancellation_date'])
order.log_action(
'pretix.event.order.reactivated',
user=user,
auth=auth,
data={
'expires': order.expires,
}
)
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') + 1))
for gc in position.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update().get(pk=gc.pk)
gc.transactions.create(value=position.price, order=order)
break
else:
raise OrderError(is_available)
order_approved.send(order.event, order=order)
if order.status == Order.STATUS_PAID:
order_paid.send(order.event, order=order)
num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order)
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None): def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None):
""" """
Extends the deadline of an order. If the order is already expired, the quota will be checked to Extends the deadline of an order. If the order is already expired, the quota will be checked to
@@ -117,9 +165,10 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
'state_change': was_expired 'state_change': was_expired
} }
) )
if was_expired: if was_expired:
num_invoices = order.invoices.filter(is_cancellation=False).count() num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices: if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order) generate_invoice(order)
if order.status == Order.STATUS_PENDING: if order.status == Order.STATUS_PENDING:
@@ -271,15 +320,16 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None, def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
cancellation_fee=None): cancellation_fee=None, keep_fees=None):
""" """
Mark this order as canceled Mark this order as canceled
:param order: The order to change :param order: The order to change
:param user: The user that performed the change :param user: The user that performed the change
""" """
# If new actions are added to this function, make sure to add the reverse operation to reactivate_order()
with transaction.atomic(): with transaction.atomic():
if isinstance(order, int): if isinstance(order, int):
order = Order.objects.get(pk=order) order = Order.objects.select_for_update().get(pk=order)
if isinstance(user, int): if isinstance(user, int):
user = User.objects.get(pk=user) user = User.objects.get(pk=user)
if isinstance(api_token, int): if isinstance(api_token, int):
@@ -294,8 +344,8 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if not order.cancel_allowed(): if not order.cancel_allowed():
raise OrderError(_('You cannot cancel this order.')) raise OrderError(_('You cannot cancel this order.'))
invoices = [] invoices = []
i = order.invoices.filter(is_cancellation=False, refered__isnull=True).last() i = order.invoices.filter(is_cancellation=False).last()
if i: if i and not i.refered.exists():
invoices.append(generate_cancellation(i)) invoices.append(generate_cancellation(i))
for position in order.positions.all(): for position in order.positions.all():
@@ -318,31 +368,38 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
position.canceled = True position.canceled = True
position.save(update_fields=['canceled']) position.save(update_fields=['canceled'])
new_fee = cancellation_fee
for fee in order.fees.all(): for fee in order.fees.all():
fee.canceled = True if keep_fees and fee in keep_fees:
fee.save(update_fields=['canceled']) new_fee -= fee.value
else:
fee.canceled = True
fee.save(update_fields=['canceled'])
f = OrderFee( if new_fee:
fee_type=OrderFee.FEE_TYPE_CANCELLATION, f = OrderFee(
value=cancellation_fee, fee_type=OrderFee.FEE_TYPE_CANCELLATION,
tax_rule=order.event.settings.tax_rate_default, value=new_fee,
order=order, tax_rule=order.event.settings.tax_rate_default,
) order=order,
f._calculate_tax() )
f.save() f._calculate_tax()
f.save()
if order.payment_refund_sum < cancellation_fee: if order.payment_refund_sum < cancellation_fee:
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.')) raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
order.status = Order.STATUS_PAID order.status = Order.STATUS_PAID
order.total = f.value order.total = cancellation_fee
order.save(update_fields=['status', 'total']) order.cancellation_date = now()
order.save(update_fields=['status', 'cancellation_date', 'total'])
if i: if i:
invoices.append(generate_invoice(order)) invoices.append(generate_invoice(order))
else: else:
with order.event.lock(): with order.event.lock():
order.status = Order.STATUS_CANCELED order.status = Order.STATUS_CANCELED
order.save(update_fields=['status']) order.cancellation_date = now()
order.save(update_fields=['status', 'cancellation_date'])
for position in order.positions.all(): for position in order.positions.all():
if position.voucher: if position.voucher:
@@ -350,6 +407,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device, order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
data={'cancellation_fee': cancellation_fee}) data={'cancellation_fee': cancellation_fee})
order.cancellation_requests.all().delete()
if send_mail: if send_mail:
email_template = order.event.settings.mail_text_order_canceled email_template = order.event.settings.mail_text_order_canceled
@@ -553,6 +611,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
bprice = bundle.designated_price or 0 bprice = bundle.designated_price or 0
except ItemBundle.DoesNotExist: except ItemBundle.DoesNotExist:
bprice = cp.price bprice = cp.price
except ItemBundle.MultipleObjectsReturned:
raise OrderError("Invalid product configuration (duplicate bundle)")
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False, price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
invoice_address=address, force_custom_price=True, max_discount=max_discount) invoice_address=address, force_custom_price=True, max_discount=max_discount)
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False, pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
@@ -631,7 +691,7 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
total = sum([c.price for c in positions]) total = sum([c.price for c in positions])
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total, for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total,
meta_info=meta_info, positions=positions): meta_info=meta_info, positions=positions, gift_cards=gift_cards):
if resp: if resp:
fees += resp fees += resp
total += sum(f.value for f in fees) total += sum(f.value for f in fees)
@@ -968,7 +1028,6 @@ def send_download_reminders(sender, **kwargs):
F('event__date_from') F('event__date_from')
) )
).filter( ).filter(
status=Order.STATUS_PAID,
download_reminder_sent=False, download_reminder_sent=False,
datetime__lte=now() - timedelta(hours=2), datetime__lte=now() - timedelta(hours=2),
first_date__gte=today, first_date__gte=today,
@@ -998,6 +1057,22 @@ def send_download_reminders(sender, **kwargs):
if not all([r for rr, r in allow_ticket_download.send(event, order=o)]): if not all([r for rr, r in allow_ticket_download.send(event, order=o)]):
continue continue
if not o.ticket_download_available:
continue
positions = o.positions.select_related('item')
if o.status != Order.STATUS_PAID:
if o.status != Order.STATUS_PENDING or o.require_approval or not \
o.event.settings.ticket_download_pending:
continue
send = False
for p in positions:
if p.generate_ticket:
send = True
break
if not send:
continue
with language(o.locale): with language(o.locale):
o.download_reminder_sent = True o.download_reminder_sent = True
o.save(update_fields=['download_reminder_sent']) o.save(update_fields=['download_reminder_sent'])
@@ -1015,6 +1090,9 @@ def send_download_reminders(sender, **kwargs):
if event.settings.mail_send_download_reminder_attendee: if event.settings.mail_send_download_reminder_attendee:
for p in o.positions.all(): for p in o.positions.all():
if not p.generate_ticket:
continue
if p.subevent_id: if p.subevent_id:
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace( reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
hour=0, minute=0, second=0, microsecond=0 hour=0, minute=0, second=0, microsecond=0
@@ -1075,6 +1153,7 @@ class OrderChangeManager:
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat')) AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat'))
SplitOperation = namedtuple('SplitOperation', ('position',)) SplitOperation = namedtuple('SplitOperation', ('position',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value')) FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee',))
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee',)) CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee',))
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',)) RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
@@ -1145,6 +1224,27 @@ class OrderChangeManager:
self._quotadiff.subtract(position.quotas) self._quotadiff.subtract(position.quotas)
self._operations.append(self.SubeventOperation(position, subevent)) self._operations.append(self.SubeventOperation(position, subevent))
def change_item_and_subevent(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation],
subevent: SubEvent):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation'])
price = get_price(item, variation, voucher=position.voucher, subevent=subevent,
invoice_address=self._invoice_address)
if price is None: # NOQA
raise OrderError(self.error_messages['product_invalid'])
new_quotas = (variation.quotas.filter(subevent=subevent)
if variation else item.quotas.filter(subevent=subevent))
if not new_quotas:
raise OrderError(self.error_messages['quota_missing'])
self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas)
self._operations.append(self.ItemOperation(position, item, variation))
self._operations.append(self.SubeventOperation(position, subevent))
def regenerate_secret(self, position: OrderPosition): def regenerate_secret(self, position: OrderPosition):
self._operations.append(self.RegenerateSecretOperation(position)) self._operations.append(self.RegenerateSecretOperation(position))
@@ -1188,6 +1288,11 @@ class OrderChangeManager:
self._operations.append(self.CancelFeeOperation(fee)) self._operations.append(self.CancelFeeOperation(fee))
self._invoice_dirty = True self._invoice_dirty = True
def add_fee(self, fee: OrderFee):
self._totaldiff += fee.value
self._invoice_dirty = True
self._operations.append(self.AddFeeOperation(fee))
def change_fee(self, fee: OrderFee, value: Decimal): def change_fee(self, fee: OrderFee, value: Decimal):
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross') value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross')
self._totaldiff += value.gross - fee.value self._totaldiff += value.gross - fee.value
@@ -1448,6 +1553,13 @@ class OrderChangeManager:
invoice_address=self._invoice_address invoice_address=self._invoice_address
).gross ).gross
) )
elif isinstance(op, self.AddFeeOperation):
self.order.log_action('pretix.event.order.changed.addfee', user=self.user, auth=self.auth, data={
'fee': op.fee.pk,
})
op.fee.order = self.order
op.fee._calculate_tax()
op.fee.save()
elif isinstance(op, self.FeeValueOperation): elif isinstance(op, self.FeeValueOperation):
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={ self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
'fee': op.fee.pk, 'fee': op.fee.pk,
@@ -1710,7 +1822,8 @@ class OrderChangeManager:
i = self.order.invoices.filter(is_cancellation=False).last() i = self.order.invoices.filter(is_cancellation=False).last()
if self.reissue_invoice and i and self._invoice_dirty: if self.reissue_invoice and i and self._invoice_dirty:
self._invoices.append(generate_cancellation(i)) self._invoices.append(generate_cancellation(i))
self._invoices.append(generate_invoice(self.order)) if invoice_qualified(self.order):
self._invoices.append(generate_invoice(self.order))
def _check_complete_cancel(self): def _check_complete_cancel(self):
current = self.order.positions.count() current = self.order.positions.count()
@@ -1800,61 +1913,127 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
raise OrderError(str(error_messages['busy'])) raise OrderError(str(error_messages['busy']))
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER,
refund_as_giftcard=False):
notify_admin = False
error = False
if isinstance(order, int):
order = Order.objects.get(pk=order)
refund_amount = order.pending_sum * -1
if refund_amount <= Decimal('0.00'):
return
if refund_as_giftcard:
proposals = {}
can_auto_refund = True
can_auto_refund_sum = refund_amount
with transaction.atomic():
giftcard = order.event.organizer.issued_gift_cards.create(
currency=order.event.currency,
testmode=order.testmode
)
giftcard.log_action('pretix.giftcards.created', data={})
r = order.refunds.create(
order=order,
payment=None,
source=source,
state=OrderRefund.REFUND_STATE_CREATED,
execution_date=now(),
amount=can_auto_refund_sum,
provider='giftcard',
info=json.dumps({
'gift_card': giftcard.pk
})
)
try:
r.payment_provider.execute_refund(r)
except PaymentException as e:
with transaction.atomic():
r.state = OrderRefund.REFUND_STATE_FAILED
r.save()
order.log_action('pretix.event.order.refund.failed', {
'local_id': r.local_id,
'provider': r.provider,
'error': str(e)
})
error = True
notify_admin = True
else:
if r.state != OrderRefund.REFUND_STATE_DONE:
notify_admin = True
else:
proposals = order.propose_auto_refunds(refund_amount)
can_auto_refund_sum = sum(proposals.values())
can_auto_refund = (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount
if can_auto_refund:
for p, value in proposals.items():
with transaction.atomic():
r = order.refunds.create(
payment=p,
source=source,
state=OrderRefund.REFUND_STATE_CREATED,
amount=value,
provider=p.provider
)
order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
})
try:
r.payment_provider.execute_refund(r)
except PaymentException as e:
with transaction.atomic():
r.state = OrderRefund.REFUND_STATE_FAILED
r.save()
order.log_action('pretix.event.order.refund.failed', {
'local_id': r.local_id,
'provider': r.provider,
'error': str(e)
})
error = True
notify_admin = True
else:
if r.state not in (OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_DONE):
notify_admin = True
if refund_amount - can_auto_refund_sum > Decimal('0.00'):
if manual_refund:
with transaction.atomic():
r = order.refunds.create(
source=source,
state=OrderRefund.REFUND_STATE_CREATED,
amount=refund_amount - can_auto_refund_sum,
provider='manual'
)
order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
})
else:
notify_admin = True
if notify_admin:
order.log_action('pretix.event.order.refund.requested')
if error:
raise OrderError(
_(
'There was an error while trying to send the money back to you. Please contact the event organizer '
'for further information.')
)
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) @app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
@scopes_disabled() @scopes_disabled()
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None, def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
device=None, cancellation_fee=None, try_auto_refund=False): device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False):
try: try:
try: try:
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application, ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
cancellation_fee) cancellation_fee)
if try_auto_refund: if try_auto_refund:
notify_admin = False _try_auto_refund(order, refund_as_giftcard=refund_as_giftcard)
error = False
order = Order.objects.get(pk=order)
refund_amount = order.pending_sum * -1
proposals = order.propose_auto_refunds(refund_amount)
can_auto_refund = sum(proposals.values()) == refund_amount
if can_auto_refund:
for p, value in proposals.items():
with transaction.atomic():
r = order.refunds.create(
payment=p,
source=OrderRefund.REFUND_SOURCE_BUYER,
state=OrderRefund.REFUND_STATE_CREATED,
amount=value,
provider=p.provider
)
order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
})
try:
r.payment_provider.execute_refund(r)
except PaymentException as e:
with transaction.atomic():
r.state = OrderRefund.REFUND_STATE_FAILED
r.save()
order.log_action('pretix.event.order.refund.failed', {
'local_id': r.local_id,
'provider': r.provider,
'error': str(e)
})
error = True
notify_admin = True
else:
if r.state != OrderRefund.REFUND_STATE_DONE:
notify_admin = True
elif refund_amount != Decimal('0.00'):
notify_admin = True
if notify_admin:
order.log_action('pretix.event.order.refund.requested')
if error:
raise OrderError(
_('There was an error while trying to send the money back to you. Please contact the event organizer for further information.')
)
return ret return ret
except LockTimeoutException: except LockTimeoutException:
self.retry() self.retry()

View File

@@ -1,5 +1,5 @@
from django.db.models import Count, Q from django.db.models import Count, Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.i18n import LazyLocaleException from pretix.base.i18n import LazyLocaleException
from pretix.base.models import CartPosition, Seat from pretix.base.models import CartPosition, Seat
@@ -25,7 +25,7 @@ def validate_plan_change(event, subevent, plan):
subevent=subevent, subevent=subevent,
).filter( ).filter(
Q(has_v=True) | Q(has_op=True) Q(has_v=True) | Q(has_op=True)
).values_list('seat_guid', flat=True) ).values_list('seat_guid', flat=True).order_by()
) )
new_seats = { new_seats = {
ss.guid for ss in plan.iter_all_seats() ss.guid for ss in plan.iter_all_seats()
@@ -40,7 +40,7 @@ def generate_seats(event, subevent, plan, mapping):
current_seats = {} current_seats = {}
for s in event.seats.select_related('product').annotate( for s in event.seats.select_related('product').annotate(
has_op=Count('orderposition'), has_v=Count('vouchers') has_op=Count('orderposition'), has_v=Count('vouchers')
).filter(subevent=subevent): ).filter(subevent=subevent).order_by():
if s.seat_guid in current_seats: if s.seat_guid in current_seats:
s.delete() # Duplicates should not exist s.delete() # Duplicates should not exist
else: else:

View File

@@ -8,7 +8,7 @@ from dateutil.parser import parse
from django.conf import settings from django.conf import settings
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import CachedFile, Event, cachedfile_name from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.services.tasks import ProfiledEventTask from pretix.base.services.tasks import ProfiledEventTask

View File

@@ -6,7 +6,7 @@ from django.db.models import (
Case, Count, DateTimeField, F, Max, OuterRef, Subquery, Sum, Value, When, Case, Count, DateTimeField, F, Max, OuterRef, Subquery, Sum, Value, When,
) )
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
from pretix.base.models.event import SubEvent from pretix.base.models.event import SubEvent

View File

@@ -3,7 +3,7 @@ import os
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.base.i18n import language from pretix.base.i18n import language

View File

@@ -5,7 +5,7 @@ from datetime import timedelta
import requests import requests
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import gettext_lazy as _, gettext_noop
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
@@ -91,7 +91,7 @@ def send_update_notification_email():
gs.settings.update_check_email, gs.settings.update_check_email,
_('pretix update available'), _('pretix update available'),
LazyI18nString.from_gettext( LazyI18nString.from_gettext(
ugettext_noop( gettext_noop(
'Hi!\n\nAn update is available for pretix or for one of the plugins you installed in your ' 'Hi!\n\nAn update is available for pretix or for one of the plugins you installed in your '
'pretix installation. Please click on the following link for more information:\n\n {url} \n\n' 'pretix installation. Please click on the following link for more information:\n\n {url} \n\n'
'You can always find information on the latest updates on the pretix.eu blog:\n\n' 'You can always find information on the latest updates on the pretix.eu blog:\n\n'

View File

@@ -42,6 +42,8 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
continue continue
if wle.subevent and not wle.subevent.presale_is_running: if wle.subevent and not wle.subevent.presale_is_running:
continue continue
if not wle.item.active or (wle.variation and not wle.variation.active):
continue
quotas = (wle.variation.quotas.filter(subevent=wle.subevent) quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
if wle.variation if wle.variation

View File

@@ -11,14 +11,15 @@ from django.core.files import File
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import Model from django.db.models import Model
from django.utils.translation import ( from django.utils.translation import (
pgettext, pgettext_lazy, ugettext_lazy as _, ugettext_noop, gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
) )
from django_countries import countries from django_countries import countries
from hierarkey.models import GlobalSettingsBase, Hierarkey from hierarkey.models import GlobalSettingsBase, Hierarkey
from i18nfield.forms import I18nFormField, I18nTextarea from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
from rest_framework import serializers from rest_framework import serializers
from pretix.api.serializers.fields import ListMultipleChoiceField
from pretix.api.serializers.i18n import I18nField from pretix.api.serializers.i18n import I18nField
from pretix.base.models.tax import TaxRule from pretix.base.models.tax import TaxRule
from pretix.base.reldate import ( from pretix.base.reldate import (
@@ -104,6 +105,44 @@ DEFAULTS = {
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}), widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
) )
}, },
'attendee_company_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for company per ticket"),
)
},
'attendee_company_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require company per ticket"),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_company_asked'}),
)
},
'attendee_addresses_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for postal addresses per ticket"),
)
},
'attendee_addresses_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require postal addresses per ticket"),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_addresses_asked'}),
)
},
'order_email_asked_twice': { 'order_email_asked_twice': {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool,
@@ -182,6 +221,20 @@ DEFAULTS = {
required=False required=False
) )
}, },
'invoice_address_custom_field': {
'default': '',
'type': LazyI18nString,
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Custom address field"),
widget=I18nTextInput,
help_text=_("If you want to add a custom text field, e.g. for a country-specific registration number, to "
"your invoice address form, please fill in the label here. This label will both be used for "
"asking the user to input their details as well as for displaying the value on the invoice. "
"The field will not be required.")
)
},
'invoice_address_vatid': { 'invoice_address_vatid': {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool,
@@ -337,6 +390,7 @@ DEFAULTS = {
"you use slow payment methods like bank transfer, we recommend 14 days. If you only use real-time " "you use slow payment methods like bank transfer, we recommend 14 days. If you only use real-time "
"payment methods, we recommend still setting two or three days to allow people to retry failed " "payment methods, we recommend still setting two or three days to allow people to retry failed "
"payments."), "payments."),
required=True,
validators=[MinValueValidator(0), validators=[MinValueValidator(0),
MaxValueValidator(1000000)] MaxValueValidator(1000000)]
), ),
@@ -350,7 +404,7 @@ DEFAULTS = {
'type': RelativeDateWrapper, 'type': RelativeDateWrapper,
'form_class': RelativeDateField, 'form_class': RelativeDateField,
'serializer_class': SerializerRelativeDateField, 'serializer_class': SerializerRelativeDateField,
'form_kawrgs': dict( 'form_kwargs': dict(
label=_('Last date of payments'), label=_('Last date of payments'),
help_text=_("The last date any payments are accepted. This has precedence over the number of " help_text=_("The last date any payments are accepted. This has precedence over the number of "
"days configured above. If you use the event series feature and an order contains tickets for " "days configured above. If you use the event series feature and an order contains tickets for "
@@ -527,6 +581,7 @@ DEFAULTS = {
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Domestic tax ID"), label=_("Domestic tax ID"),
help_text=_("e.g. tax number in Germany, ABN in Australia, …")
) )
}, },
'invoice_address_from_vat_id': { 'invoice_address_from_vat_id': {
@@ -623,7 +678,7 @@ DEFAULTS = {
'locales': { 'locales': {
'default': json.dumps([settings.LANGUAGE_CODE]), 'default': json.dumps([settings.LANGUAGE_CODE]),
'type': list, 'type': list,
'serializer_class': serializers.MultipleChoiceField, 'serializer_class': ListMultipleChoiceField,
'serializer_kwargs': dict( 'serializer_kwargs': dict(
choices=settings.LANGUAGES, choices=settings.LANGUAGES,
required=True, required=True,
@@ -652,6 +707,17 @@ DEFAULTS = {
label=_("Default language"), label=_("Default language"),
) )
}, },
'show_dates_on_frontpage': {
'default': 'True',
'type': bool,
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Show event times and dates on the ticket shop"),
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
"does however not affect the display in other locations."),
)
},
'show_date_to': { 'show_date_to': {
'default': 'True', 'default': 'True',
'type': bool, 'type': bool,
@@ -754,8 +820,8 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Use feature"), label=_("Allow users to download tickets"),
help_text=_("Use pretix to generate tickets for the user to download and print out."), help_text=_("If this is off, nobody can download a ticket."),
) )
}, },
'ticket_download_date': { 'ticket_download_date': {
@@ -776,7 +842,11 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Offer to download tickets separately for add-on products"), label=_("Generate tickets for add-on products"),
help_text=_('By default, tickets are only issued for products selected individually, not for add-on '
'products. With this option, a separate ticket is issued for every add-on product as well.'),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
'data-checkbox-dependency-visual': 'on'}),
) )
}, },
'ticket_download_nonadm': { 'ticket_download_nonadm': {
@@ -785,7 +855,11 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Generate tickets for non-admission products"), label=_("Generate tickets for all products"),
help_text=_('If turned off, tickets are only issued for products that are marked as an "admission ticket"'
'in the product settings. You can also turn off tickt issuing in every product separately.'),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
'data-checkbox-dependency-visual': 'on'}),
) )
}, },
'ticket_download_pending': { 'ticket_download_pending': {
@@ -794,7 +868,10 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Offer to download tickets even before an order is paid"), label=_("Generate tickets for pending orders"),
help_text=_('If turned off, ticket downloads are only possible after an order has been marked as paid.'),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
'data-checkbox-dependency-visual': 'on'}),
) )
}, },
'event_list_availability': { 'event_list_availability': {
@@ -879,6 +956,66 @@ DEFAULTS = {
label=_("Keep a percentual cancellation fee"), label=_("Keep a percentual cancellation fee"),
) )
}, },
'cancel_allow_user_paid_adjust_fees': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Allow customers to voluntarily choose a lower refund"),
help_text=_("With this option enabled, your customers can choose to get a smaller refund to support you.")
)
},
'cancel_allow_user_paid_adjust_fees_explanation': {
'default': LazyI18nString.from_gettext(gettext_noop(
'However, if you want us to help keep the lights on here, please consider using the slider below to '
'request a smaller refund. Thank you!'
)),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Voluntary lower refund explanation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown in between the explanation of how the refunds work and the slider "
"which your customers can use to choose the amount they would like to receive. You can use it "
"e.g. to explain choosing a lower refund will help your organisation.")
)
},
'cancel_allow_user_paid_require_approval': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Customers can only request a cancellation that needs to be approved by the event organizer "
"before the order is canceled and a refund is issued."),
)
},
'cancel_allow_user_paid_refund_as_giftcard': {
'default': 'off',
'type': str,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=[
('off', _('All refunds are issued to the original payment method')),
('option', _('Customers can choose between a gift card and a refund to their payment method')),
('force', _('All refunds are issued as gift cards')),
],
),
'form_class': forms.ChoiceField,
'form_kwargs': dict(
label=_('Refund method'),
choices=[
('off', _('All refunds are issued to the original payment method')),
('option', _('Customers can choose between a gift card and a refund to their payment method')),
('force', _('All refunds are issued as gift cards')),
],
widget=forms.RadioSelect,
# When adding a new ordering, remember to also define it in the event model
)
},
'cancel_allow_user_paid_until': { 'cancel_allow_user_paid_until': {
'default': None, 'default': None,
'type': RelativeDateWrapper, 'type': RelativeDateWrapper,
@@ -978,7 +1115,7 @@ DEFAULTS = {
}, },
'mail_text_resend_link': { 'mail_text_resend_link': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
you receive this message because you asked us to send you the link you receive this message because you asked us to send you the link
to your order for {event}. to your order for {event}.
@@ -991,7 +1128,7 @@ Your {event} team"""))
}, },
'mail_text_resend_all_links': { 'mail_text_resend_all_links': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
somebody requested a list of your orders for {event}. somebody requested a list of your orders for {event}.
The list is as follows: The list is as follows:
@@ -1003,7 +1140,7 @@ Your {event} team"""))
}, },
'mail_text_order_free_attendee': { 'mail_text_order_free_attendee': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name}, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
you have been registered for {event} successfully. you have been registered for {event} successfully.
@@ -1015,7 +1152,7 @@ Your {event} team"""))
}, },
'mail_text_order_free': { 'mail_text_order_free': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
your order for {event} was successful. As you only ordered free products, your order for {event} was successful. As you only ordered free products,
no payment is required. no payment is required.
@@ -1032,7 +1169,7 @@ Your {event} team"""))
}, },
'mail_text_order_placed_require_approval': { 'mail_text_order_placed_require_approval': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we successfully received your order for {event}. Since you ordered we successfully received your order for {event}. Since you ordered
a product that requires approval by the event organizer, we ask you to a product that requires approval by the event organizer, we ask you to
@@ -1046,7 +1183,7 @@ Your {event} team"""))
}, },
'mail_text_order_placed': { 'mail_text_order_placed': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we successfully received your order for {event} with a total value we successfully received your order for {event} with a total value
of {total_with_currency}. Please complete your payment before {expire_date}. of {total_with_currency}. Please complete your payment before {expire_date}.
@@ -1065,7 +1202,7 @@ Your {event} team"""))
}, },
'mail_text_order_placed_attendee': { 'mail_text_order_placed_attendee': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name}, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
a ticket for {event} has been ordered for you. a ticket for {event} has been ordered for you.
@@ -1077,7 +1214,7 @@ Your {event} team"""))
}, },
'mail_text_order_changed': { 'mail_text_order_changed': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
your order for {event} has been changed. your order for {event} has been changed.
@@ -1089,7 +1226,7 @@ Your {event} team"""))
}, },
'mail_text_order_paid': { 'mail_text_order_paid': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we successfully received your payment for {event}. Thank you! we successfully received your payment for {event}. Thank you!
@@ -1107,7 +1244,7 @@ Your {event} team"""))
}, },
'mail_text_order_paid_attendee': { 'mail_text_order_paid_attendee': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name}, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
a ticket for {event} that has been ordered for you is now paid. a ticket for {event} that has been ordered for you is now paid.
@@ -1123,7 +1260,7 @@ Your {event} team"""))
}, },
'mail_text_order_expire_warning': { 'mail_text_order_expire_warning': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we did not yet receive a full payment for your order for {event}. we did not yet receive a full payment for your order for {event}.
Please keep in mind that we only guarantee your order if we receive Please keep in mind that we only guarantee your order if we receive
@@ -1137,7 +1274,7 @@ Your {event} team"""))
}, },
'mail_text_waiting_list': { 'mail_text_waiting_list': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
you submitted yourself to the waiting list for {event}, you submitted yourself to the waiting list for {event},
for the product {product}. for the product {product}.
@@ -1160,7 +1297,7 @@ Your {event} team"""))
}, },
'mail_text_order_canceled': { 'mail_text_order_canceled': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
your order {code} for {event} has been canceled. your order {code} for {event} has been canceled.
@@ -1172,7 +1309,7 @@ Your {event} team"""))
}, },
'mail_text_order_approved': { 'mail_text_order_approved': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
we approved your order for {event} and will be happy to welcome you we approved your order for {event} and will be happy to welcome you
at our event. at our event.
@@ -1188,7 +1325,7 @@ Your {event} team"""))
}, },
'mail_text_order_denied': { 'mail_text_order_denied': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
unfortunately, we denied your order request for {event}. unfortunately, we denied your order request for {event}.
@@ -1203,7 +1340,7 @@ Your {event} team"""))
}, },
'mail_text_order_custom_mail': { 'mail_text_order_custom_mail': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
You can change your order details and view the status of your order at You can change your order details and view the status of your order at
{url} {url}
@@ -1221,7 +1358,7 @@ Your {event} team"""))
}, },
'mail_text_download_reminder_attendee': { 'mail_text_download_reminder_attendee': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name}, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
you are registered for {event}. you are registered for {event}.
@@ -1233,7 +1370,7 @@ Your {event} team"""))
}, },
'mail_text_download_reminder': { 'mail_text_download_reminder': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, 'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
you bought a ticket for {event}. you bought a ticket for {event}.
@@ -1283,6 +1420,14 @@ Your {event} team"""))
'default': '#D36060', 'default': '#D36060',
'type': str 'type': str
}, },
'theme_color_background': {
'default': '#FFFFFF',
'type': str
},
'theme_round_borders': {
'default': 'True',
'type': bool
},
'primary_font': { 'primary_font': {
'default': 'Open Sans', 'default': 'Open Sans',
'type': str 'type': str
@@ -1307,6 +1452,22 @@ Your {event} team"""))
'default': None, 'default': None,
'type': File 'type': File
}, },
'logo_image_large': {
'default': 'False',
'type': bool
},
'logo_show_title': {
'default': 'True',
'type': bool
},
'organizer_logo_image': {
'default': None,
'type': File
},
'organizer_logo_image_large': {
'default': 'False',
'type': bool
},
'og_image': { 'og_image': {
'default': None, 'default': None,
'type': File 'type': File
@@ -1325,6 +1486,32 @@ Your {event} team"""))
widget=I18nTextarea widget=I18nTextarea
) )
}, },
'banner_text': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Banner text (top)"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above every page of your shop. Please only use this for "
"very important messages.")
)
},
'banner_text_bottom': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Banner text (bottom)"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown below every page of your shop. Please only use this for "
"very important messages.")
)
},
'voucher_explanation_text': { 'voucher_explanation_text': {
'default': '', 'default': '',
'type': LazyI18nString, 'type': LazyI18nString,
@@ -1339,7 +1526,7 @@ Your {event} team"""))
) )
}, },
'checkout_email_helptext': { 'checkout_email_helptext': {
'default': LazyI18nString.from_gettext(ugettext_noop( 'default': LazyI18nString.from_gettext(gettext_noop(
'Make sure to enter a valid email address. We will send you an order ' 'Make sure to enter a valid email address. We will send you an order '
'confirmation including a link that you need to access your order later.' 'confirmation including a link that you need to access your order later.'
)), )),
@@ -1733,7 +1920,7 @@ def validate_settings(event, settings_dict):
}) })
if settings_dict.get('invoice_address_company_required') and not settings_dict.get('invoice_address_required'): if settings_dict.get('invoice_address_company_required') and not settings_dict.get('invoice_address_required'):
raise ValidationError({ raise ValidationError({
'invoice_address_company_requred': _('You have to require invoice addresses to require for company names.') 'invoice_address_company_required': _('You have to require invoice addresses to require for company names.')
}) })
payment_term_last = settings_dict.get('payment_term_last') payment_term_last = settings_dict.get('payment_term_last')

View File

@@ -7,7 +7,7 @@ from django.db.models import Max, Q
from django.db.models.functions import Greatest from django.db.models.functions import Greatest
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.api.serializers.order import ( from pretix.api.serializers.order import (
AnswerSerializer, InvoiceAddressSerializer, AnswerSerializer, InvoiceAddressSerializer,
@@ -194,15 +194,23 @@ class WaitingListShredder(BaseDataShredder):
le.save(update_fields=['data', 'shredded']) le.save(update_fields=['data', 'shredded'])
class AttendeeNameShredder(BaseDataShredder): class AttendeeInfoShredder(BaseDataShredder):
verbose_name = _('Attendee names') verbose_name = _('Attendee info')
identifier = 'attendee_names' identifier = 'attendee_info'
description = _('This will remove all attendee names from order positions, as well as logged changes to them.') description = _('This will remove all attendee names and postal addresses from order positions, as well as logged '
'changes to them.')
def generate_files(self) -> List[Tuple[str, str, str]]: def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'attendee-names.json', 'application/json', json.dumps({ yield 'attendee-info.json', 'application/json', json.dumps({
'{}-{}'.format(op.order.code, op.positionid): op.attendee_name '{}-{}'.format(op.order.code, op.positionid): {
for op in OrderPosition.all.filter( 'name': op.attendee_name,
'company': op.company,
'street': op.street,
'zipcode': op.zipcode,
'city': op.city,
'country': str(op.country) if op.country else None,
'state': op.state
} for op in OrderPosition.all.filter(
order__event=self.event order__event=self.event
).filter( ).filter(
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False)) Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
@@ -214,8 +222,10 @@ class AttendeeNameShredder(BaseDataShredder):
OrderPosition.all.filter( OrderPosition.all.filter(
order__event=self.event order__event=self.event
).filter( ).filter(
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False)) Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False) |
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}) Q(company__isnull=False) | Q(street__isnull=False) | Q(zipcode__isnull=False) | Q(city__isnull=False)
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}, company=None, street=None,
zipcode=None, city=None)
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""): for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
d = le.parsed_data d = le.parsed_data
@@ -227,6 +237,14 @@ class AttendeeNameShredder(BaseDataShredder):
d['data'][i]['attendee_name_parts'] = { d['data'][i]['attendee_name_parts'] = {
'_legacy': '' '_legacy': ''
} }
if 'company' in row:
d['data'][i]['company'] = ''
if 'street' in row:
d['data'][i]['street'] = ''
if 'zipcode' in row:
d['data'][i]['zipcode'] = ''
if 'city' in row:
d['data'][i]['city'] = ''
le.data = json.dumps(d) le.data = json.dumps(d)
le.shredded = True le.shredded = True
le.save(update_fields=['data', 'shredded']) le.save(update_fields=['data', 'shredded'])
@@ -357,7 +375,7 @@ class PaymentInfoShredder(BaseDataShredder):
def register_payment_provider(sender, **kwargs): def register_payment_provider(sender, **kwargs):
return [ return [
EmailAddressShredder, EmailAddressShredder,
AttendeeNameShredder, AttendeeInfoShredder,
InvoiceAddressShredder, InvoiceAddressShredder,
QuestionAnswerShredder, QuestionAnswerShredder,
InvoiceShredder, InvoiceShredder,

View File

@@ -341,6 +341,16 @@ as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event. As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
""" """
order_reactivated = EventPluginSignal(
providing_args=["order"]
)
"""
This signal is sent out every time a canceled order is reactivated. The order object is given
as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_expired = EventPluginSignal( order_expired = EventPluginSignal(
providing_args=["order"] providing_args=["order"]
) )
@@ -472,7 +482,7 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
""" """
event_copy_data = EventPluginSignal( event_copy_data = EventPluginSignal(
providing_args=["other", "tax_map", "category_map", "item_map", "question_map", "variation_map"] providing_args=["other", "tax_map", "category_map", "item_map", "question_map", "variation_map", "checkin_list_map"]
) )
""" """
This signal is sent out when a new event is created as a clone of an existing event, i.e. This signal is sent out when a new event is created as a clone of an existing event, i.e.
@@ -484,9 +494,9 @@ but you might need to modify that data.
The ``sender`` keyword argument will contain the event of the **new** event. The ``other`` The ``sender`` keyword argument will contain the event of the **new** event. The ``other``
keyword argument will contain the event to **copy from**. The keyword arguments keyword argument will contain the event to **copy from**. The keyword arguments
``tax_map``, ``category_map``, ``item_map``, ``question_map``, and ``variation_map`` contain ``tax_map``, ``category_map``, ``item_map``, ``question_map``, ``variation_map`` and
mappings from object IDs in the original event to objects in the new event of the respective ``checkin_list_map`` contain mappings from object IDs in the original event to objects
types. in the new event of the respective types.
""" """
item_copy_data = EventPluginSignal( item_copy_data = EventPluginSignal(
@@ -517,7 +527,7 @@ an OrderedDict of (setting name, form field).
""" """
order_fee_calculation = EventPluginSignal( order_fee_calculation = EventPluginSignal(
providing_args=['positions', 'invoice_address', 'meta_info', 'total'] providing_args=['positions', 'invoice_address', 'meta_info', 'total', 'gift_cards']
) )
""" """
This signals allows you to add fees to an order while it is being created. You are expected to This signals allows you to add fees to an order while it is being created. You are expected to
@@ -528,7 +538,8 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
argument will contain the cart positions and ``invoice_address`` the invoice address (useful for argument will contain the cart positions and ``invoice_address`` the invoice address (useful for
tax calculation). The argument ``meta_info`` contains the order's meta dictionary. The ``total`` tax calculation). The argument ``meta_info`` contains the order's meta dictionary. The ``total``
keyword argument will contain the total cart sum without any fees. You should not rely on this keyword argument will contain the total cart sum without any fees. You should not rely on this
``total`` value for fee calculations as other fees might interfere. ``total`` value for fee calculations as other fees might interfere. The ``gift_cards`` argument lists
the gift cards in use.
""" """
order_fee_type_name = EventPluginSignal( order_fee_type_name = EventPluginSignal(

View File

@@ -0,0 +1,2 @@
{% load rich_text %}
{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} {{ widget.label|rich_text_snippet }}</label>{% endif %}

View File

@@ -7,24 +7,28 @@ from django import template
from django.conf import settings from django.conf import settings
from django.core import signing from django.core import signing
from django.urls import reverse from django.urls import reverse
from django.utils.http import is_safe_url from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
register = template.Library() register = template.Library()
ALLOWED_TAGS = [ ALLOWED_TAGS_SNIPPET = [
'a', 'a',
'abbr', 'abbr',
'acronym', 'acronym',
'b', 'b',
'blockquote',
'br', 'br',
'code', 'code',
'em', 'em',
'i', 'i',
'strong',
'span',
# Update doc/user/markdown.rst if you change this!
]
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
'blockquote',
'li', 'li',
'ol', 'ol',
'strong',
'ul', 'ul',
'p', 'p',
'table', 'table',
@@ -34,7 +38,6 @@ ALLOWED_TAGS = [
'td', 'td',
'th', 'th',
'div', 'div',
'span',
'hr', 'hr',
'h1', 'h1',
'h2', 'h2',
@@ -63,7 +66,7 @@ ALLOWED_PROTOCOLS = ['http', 'https', 'mailto', 'tel']
def safelink_callback(attrs, new=False): def safelink_callback(attrs, new=False):
url = attrs.get((None, 'href'), '/') url = attrs.get((None, 'href'), '/')
if not is_safe_url(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'): if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
signer = signing.Signer(salt='safe-redirect') signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url)) attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank' attrs[None, 'target'] = '_blank'
@@ -95,7 +98,8 @@ def markdown_compile_email(source):
), parse_email=True) ), parse_email=True)
def markdown_compile(source): def markdown_compile(source, snippet=False):
tags = ALLOWED_TAGS_SNIPPET if snippet else ALLOWED_TAGS
return bleach.clean( return bleach.clean(
markdown.markdown( markdown.markdown(
source, source,
@@ -104,7 +108,8 @@ def markdown_compile(source):
'markdown.extensions.nl2br' 'markdown.extensions.nl2br'
] ]
), ),
tags=ALLOWED_TAGS, strip=snippet,
tags=tags,
attributes=ALLOWED_ATTRIBUTES, attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS, protocols=ALLOWED_PROTOCOLS,
) )
@@ -122,3 +127,17 @@ def rich_text(text: str, **kwargs):
parse_email=True parse_email=True
) )
return mark_safe(body_md) return mark_safe(body_md)
@register.filter
def rich_text_snippet(text: str, **kwargs):
"""
Processes markdown and cleans HTML in a text input.
"""
text = str(text)
body_md = bleach.linkify(
markdown_compile(text, snippet=True),
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
parse_email=True
)
return mark_safe(body_md)

View File

@@ -6,7 +6,7 @@ from zipfile import ZipFile
from django import forms from django import forms
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import Event, Order, OrderPosition from pretix.base.models import Event, Order, OrderPosition
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
@@ -142,7 +142,7 @@ class BaseTicketOutput:
return OrderedDict([ return OrderedDict([
('_enabled', ('_enabled',
forms.BooleanField( forms.BooleanField(
label=_('Enable output'), label=_('Enable ticket format'),
required=False, required=False,
)), )),
]) ])

View File

@@ -133,7 +133,7 @@ def timeline_for_event(event, subevent=None):
if not event.has_subevents: if not event.has_subevents:
days = event.settings.get('mail_days_download_reminder', as_type=int) days = event.settings.get('mail_days_download_reminder', as_type=int)
if days is not None: if days is not None and event.settings.ticket_download:
reminder_date = (ev.date_from - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0) reminder_date = (ev.date_from - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
tl.append(TimelineEvent( tl.append(TimelineEvent(
event=event, subevent=subevent, event=event, subevent=subevent,

View File

@@ -1,7 +1,7 @@
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class BanlistValidator: class BanlistValidator:

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