Compare commits

...

314 Commits

Author SHA1 Message Date
Raphael Michel
d0df3f73ae GitLab config 2017-11-25 19:15:22 +01:00
Raphael Michel
581f3c3d58 Release 1.8.1 2017-11-25 19:07:42 +01:00
Raphael Michel
6600c430ab [SECURITY] Respect session timeout in API 2017-11-25 19:06:46 +01:00
Raphael Michel
807eb2ea7f [SECURITY] Fix handling of session timeouts 2017-11-25 19:06:03 +01:00
Raphael Michel
7dea6fc1b7 Bump version number 2017-10-07 20:38:12 +02:00
Raphael Michel
bd306e9400 Best-effort backwards compatibility of isolated cart IDs 2017-10-07 20:37:12 +02:00
Raphael Michel
3e686211e1 Update translations 2017-10-07 18:42:02 +02:00
Raphael Michel
6d1b4b0a39 Re-order travis matrix for better productivity 2017-10-07 18:16:36 +02:00
Sanket Dasgupta
58938fc07c Fix #531 -- Make placeholders replace in subject (#594)
Placeholders in subject were not being replaced because there was
no `.format()` called on the subject.

This commit creates a context dict that is used for both the body
and the subject. It is then replaced using `.format_map()`

Fixes https://github.com/pretix/pretix/issues/531
2017-10-07 18:16:13 +02:00
Raphael Michel
96dd4e02f3 Add tests for style generation and propagated settings 2017-10-07 18:13:06 +02:00
Raphael Michel
411c537438 UI for settings propagation 2017-10-07 18:13:06 +02:00
Raphael Michel
bbd112280a Propagate setting and add organizer display settings page 2017-10-07 18:13:06 +02:00
Marvin Sipp
28d074366e added organizer color field 2017-10-07 18:13:06 +02:00
Haroon Sheikh
11d76656de Fix #538 -- Remove pyvenv from docs (#633) 2017-10-07 16:50:14 +02:00
Raphael Michel
1c96bc31d5 Re-calculate quotas for all events with recent logs 2017-10-06 11:43:08 +02:00
Raphael Michel
0030064f55 Form UX: Better label in sendmail form 2017-10-06 11:23:21 +02:00
Raphael Michel
4726f5c136 Fix i18n for confirm_text 2017-10-06 11:14:42 +02:00
Raphael Michel
c7fafedc51 Checkout UX: Pre-select payment provider if there is only one 2017-10-06 11:08:00 +02:00
Raphael Michel
3eeb70ae36 Form UX: Add more helpful placeholders 2017-10-06 11:05:24 +02:00
Raphael Michel
29b1a3dca3 Do not send navigation singals for authentication pages 2017-10-06 10:35:24 +02:00
Raphael Michel
caf844b5fb Fix wrong signal name in documentation 2017-10-05 11:55:09 +02:00
Raphael Michel
6b7bdf8c4f Item creation UX: Clearer placeholders, defaults 2017-10-05 10:47:46 +02:00
Raphael Michel
aad433a3bc Welcome wizard UX: Use primary color for button 2017-10-05 10:32:14 +02:00
Raphael Michel
3f1bb56826 Event creation UX: Show clearer that the event is now created 2017-10-05 10:31:25 +02:00
Raphael Michel
b2b3add616 Form UX: Display units for more number inputs 2017-10-05 10:21:00 +02:00
Raphael Michel
2d484d4a8e Event creation UX: Label changes 2017-10-05 10:20:00 +02:00
Raphael Michel
2f252f19c9 Form UX: Use splitted date/time widgets 2017-10-05 10:17:17 +02:00
Raphael Michel
a27f372785 Event creation UX: Pre-choose organizer if there is only one 2017-10-05 08:01:22 +02:00
Raphael Michel
f074e642ec Display quotas in event list 2017-10-04 11:25:51 +02:00
Raphael Michel
217ed905d4 Contract columns in event list table 2017-10-04 10:12:46 +02:00
Raphael Michel
b920efc955 Add database cache for quotas 2017-10-04 09:45:37 +02:00
Raphael Michel
330fadbea9 Fix wrong execution order 2017-10-04 09:43:14 +02:00
Raphael Michel
50c595e3d6 Fix migration error (unique app configuragion keys) 2017-10-02 17:40:31 +02:00
Raphael Michel
26f258c6cf Isolate cart sessions 2017-10-02 17:00:35 +02:00
Raphael Michel
f15a72e59d Fix mail_text_download_reminder email preview 2017-10-02 15:44:32 +02:00
Raphael Michel
8accaae6b1 New signal: allow_ticket_download 2017-10-02 15:07:23 +02:00
Raphael Michel
d4259501af Remove legacy ordering code 2017-10-02 14:59:01 +02:00
Jakob Schnell
fd5d5ae98e Fix #628 -- Sorting of filtered order list (#631)
* fix sorting of filtered order list

fixes #628

* implement comments on pr
2017-10-02 14:55:02 +02:00
Raphael Michel
457901ff82 Fix flake8 error 2017-10-01 17:43:51 +02:00
Raphael Michel
e201be1c65 Clarify payment fee / shipping fee relation 2017-09-29 17:08:04 +02:00
Raphael Michel
acde14372d PDF editor: Change default text 2017-09-29 17:01:13 +02:00
Raphael Michel
79988a2325 New signal order_fee_type_name 2017-09-29 16:54:27 +02:00
Raphael Michel
784f6e703c CSP: Exclude PDF editor (just doesn't work in FF) 2017-09-28 18:44:12 +02:00
Raphael Michel
29b157f287 CSP: Add reporting endpoint 2017-09-28 18:43:45 +02:00
Raphael Michel
c030bd35ca Make PDF ticket cover more extensible 2017-09-27 18:32:50 +02:00
Raphael Michel
06fe076ce2 Add request argument to pretix.control.signals.order_info 2017-09-27 18:19:47 +02:00
Raphael Michel
ae6cba067c Fix issue created in 1f889be0 2017-09-27 14:40:15 +02:00
Raphael Michel
72ae19a95d Update translation 2017-09-27 13:24:03 +02:00
Raphael Michel
1f889be07a Refactor and add signal layout_text_variables 2017-09-27 13:15:18 +02:00
Raphael Michel
39061b659a PDF Editor: More extensible implementation 2017-09-26 13:05:51 +02:00
Raphael Michel
d38f29ac7c Add signal pretix.control.signals.order_info 2017-09-26 11:47:46 +02:00
Raphael Michel
1a8e67f4de Allow clicking on typeahead results 2017-09-25 22:03:25 +02:00
Raphael Michel
8265c302ad Fix missing required=False 2017-09-25 13:33:41 +02:00
Raphael Michel
110d7c6acf Allow to enter a custom text that needs to be confirmed during checkout 2017-09-25 12:48:31 +02:00
Tobias Kunze
244b767f8f Allow markdown rendering in transaction comments. (#621)
This commit allows transaction comments to display newlines and URLs in
a useful way, helping when additional data (such as a reference to a
ticket system or a longer discussion) is required.
This PR also prevents pretix from having to bring its own chat system ;)
2017-09-25 12:25:32 +03:00
Raphael Michel
f40950efc9 Adjust to newer sentry version 2017-09-25 10:46:47 +02:00
Raphael Michel
0e0534c273 Fix incorrect timezones on event dashboard 2017-09-25 10:25:22 +02:00
Raphael Michel
9b3ea3656f PDF Output: Prevent subsequent exception on permission errors 2017-09-25 10:22:09 +02:00
Raphael Michel
62b2a367ff PDF Output: Fix AttributeError with undefined used meta data 2017-09-25 10:20:46 +02:00
Raphael Michel
ab9dd32902 Add font-src to default CSP header 2017-09-25 10:19:36 +02:00
Raphael Michel
43fc498297 Prevent some pages from search indexing 2017-09-25 10:04:37 +02:00
Raphael Michel
ef3eee7873 ContactForm: Prevent TypeError during validation 2017-09-25 09:38:35 +02:00
Raphael Michel
9f0deea9dd Rich text: Do not rewrite mailto: URLs 2017-09-25 09:37:17 +02:00
Abhiraj Hinge
e3798600ed Fixed typo in Concepts.rst (#624) 2017-09-14 16:16:56 +03:00
Raphael Michel
00834cd5e0 Fix test_checkoutflow 2017-09-13 18:29:08 +02:00
Raphael Michel
ed35c4f74e Add new signal logentry_object_link 2017-09-13 17:36:13 +02:00
Raphael Michel
9cd3e2d494 Require payment even if total consists only of fees 2017-09-13 16:42:00 +02:00
Raphael Michel
3345f48986 nav_event_settings should be an EventPluginSignal 2017-09-13 16:21:14 +02:00
Raphael Michel
b611d63975 ModelRelativeDateTimeField: Deal with None values 2017-09-13 16:20:54 +02:00
Raphael Michel
fb3866aa1a Fix TypError in PDF preview 2017-09-13 14:59:19 +02:00
Raphael Michel
a9f131b645 Make PDF download more prominent 2017-09-12 19:06:02 +02:00
Raphael Michel
e5728662c5 Allow to extend expired order even if waiting list entries exist 2017-09-12 18:50:13 +02:00
Raphael Michel
94a97fb0fd Fix broken toggling script 2017-09-09 11:09:03 +02:00
Raphael Michel
b5bea6fe7a Do not disable core modules' URLs 2017-09-08 17:50:50 +02:00
Raphael Michel
fb9d677d76 CSP: Allow blob: URLs for images in PDFs 2017-09-07 23:29:21 +02:00
Raphael Michel
7c4fc7bd0d New signals: fee_calculation_for_cart, order_fee_calculation 2017-09-07 18:59:21 +02:00
Raphael Michel
de992cecf3 New signal checkout_confirm_page_content 2017-09-07 18:15:36 +02:00
Raphael Michel
cd94549606 Fix export of answered files with binary content 2017-09-07 12:38:39 +02:00
Raphael Michel
214a6eb5ce Database field for RelativeDateTime 2017-09-06 11:25:12 +02:00
Raphael Michel
db5f0aa02d Fix #156 -- Plug-in settings navigation hook 2017-09-06 09:31:33 +02:00
Raphael Michel
ba48ab3659 Re-do squashed migration 2017-09-05 15:34:40 +02:00
Raphael Michel
d1538e07d3 Bump version 2017-09-05 12:47:10 +02:00
Raphael Michel
fe0c033b2d Bump version 2017-09-05 12:46:08 +02:00
Raphael Michel
2e58dca048 Order overview: Deterministic ordering of fees 2017-09-05 12:45:25 +02:00
Raphael Michel
d38ab8a439 Correctly set OrderFee type for new orders 2017-09-05 12:32:39 +02:00
Raphael Michel
acd7b9ba8c Squash migrations 2017-09-05 12:32:20 +02:00
Raphael Michel
56f72b225c Improve pretixdroid UI 2017-09-05 12:10:33 +02:00
Raphael Michel
8bfaf7425a Update translation 2017-09-05 11:48:01 +02:00
Raphael Michel
77a8726a03 Fix #615 -- Incorrect defaults for email texts 2017-09-05 11:46:33 +02:00
luto
119fea3379 Fix #619 -- Check format of stripe API keys in settings (#620)
* stripe tests: add settings URL to fixture, as it will be needed later

* Fix #619 -- Stripe: add basic validation for secret and publishable keys
2017-09-05 10:26:03 +03:00
Raphael Michel
e54e0d6511 New concept for fee handling (#610)
* New concept for fee handling

* More usages

* Remove all usages, make all tests pass

* API changes

* Small fixes

* Fix order of invoice lines

* Rebase migration
2017-09-05 10:11:26 +03:00
Raphael Michel
a2a88cfafa Fix tests after meta-data merge 2017-09-04 22:09:30 +02:00
Raphael Michel
5ff53d08ed Fix #586 -- Fix folded subnavigation in order detail view 2017-09-04 21:13:54 +02:00
Raphael Michel
0ddda4a668 Fix #617 -- Purge ticket cache after ticket settings/layout changes 2017-09-04 21:09:08 +02:00
Raphael Michel
d3a76e9f2f Fix #614 -- Warning message if ticket output is active but no provider 2017-09-04 21:03:17 +02:00
Raphael Michel
ea7ec2b5fc Fix #585 -- Creating invoices for refunded orders 2017-09-04 20:36:19 +02:00
Raphael Michel
b9b4ccb180 Change order of user docs pages 2017-09-04 20:13:38 +02:00
Raphael Michel
2f15d410fe Add optional timeouts for backend sessions 2017-09-04 19:50:32 +02:00
Raphael Michel
88f5af3e77 Add event meta-data 2017-09-04 19:50:32 +02:00
Martin Gross
454ca27c54 Fix #613 -- Minor typo in last change date (#616)
* Fix #556 - Max. redemptions instead of amount of vouchers

* Update Translation for Dashboard-Tile

* Fix #613 - Minor typo in last change date
2017-09-04 10:42:21 +03:00
Raphael Michel
f536cb3536 Fix grammar error 2017-08-30 18:30:59 +02:00
Raphael Michel
e6ba7379eb Display free tickets as "FREE" instead of "0.00" 2017-08-30 18:24:25 +02:00
Raphael Michel
f6b01b6e02 Improve margin arount product picture 2017-08-30 18:24:25 +02:00
Raphael Michel
ce27f8e89c Fix product list in template 2017-08-30 18:24:25 +02:00
Raphael Michel
a52635f940 Fix typo 2017-08-30 18:24:25 +02:00
Martin Gross
b608125545 Fix Dashboard-Tile Translation (number of orders) (#612)
* Fix #556 - Max. redemptions instead of amount of vouchers

* Update Translation for Dashboard-Tile
2017-08-30 15:47:03 +03:00
Raphael Michel
631cded0d6 New pretixdroid configuration system 2017-08-29 23:19:02 +02:00
Raphael Michel
43b5140754 New handling of plugin URLs (#609) 2017-08-29 10:01:50 +03:00
Raphael Michel
557a05135e Allow connect-src to media domain 2017-08-28 09:19:42 +02:00
Tobias Kunze
618416d0d2 Update translation, closes #604 (#608) 2017-08-27 10:45:13 +03:00
Tobias Preuss
9a4ee3db69 Improve reading flow. (#603) 2017-08-27 10:41:55 +03:00
Raphael Michel
999dde3fa4 Fix exception in pretixdroid API 2017-08-25 16:32:12 +02:00
Raphael Michel
1171cce550 Predictable order of question forms 2017-08-25 16:23:16 +02:00
Raphael Michel
77e13338ad Fix inconsistencies in pretixdroid API views 2017-08-25 16:22:53 +02:00
Raphael Michel
fd35b5ea72 Add "attention" flag to products 2017-08-25 15:14:54 +02:00
Raphael Michel
f98f25fb6b Improve MT940 import 2017-08-25 14:51:25 +02:00
Raphael Michel
511a49041f Add item and variation ID to pretixdroid API 2017-08-25 13:01:05 +02:00
Raphael Michel
74be5cfe96 Fix test (again) 2017-08-24 21:31:27 +02:00
Raphael Michel
1f54b36ece Fix failing test of calendar page 2017-08-24 19:32:12 +02:00
Raphael Michel
d12b77b572 Remove unneeded space 2017-08-24 18:32:31 +02:00
Raphael Michel
4928234785 Banktransfer: Allow mt940 files to have the .sta extension 2017-08-24 18:13:26 +02:00
Raphael Michel
208e3c9933 Update translation 2017-08-24 18:11:47 +02:00
Raphael Michel
d697381d8b List and calendar for all organizers 2017-08-24 17:13:22 +02:00
Raphael Michel
cd6b1a2327 Allow subevent filtering in dashboard 2017-08-24 16:12:50 +02:00
Raphael Michel
ff21380099 Improve typeahead on dashboard page 2017-08-24 14:21:30 +02:00
Raphael Michel
a773531003 Statistics: Add subevent selection 2017-08-24 12:53:59 +02:00
Raphael Michel
23ecd43885 Better dashboard layout 2017-08-24 12:36:48 +02:00
Raphael Michel
3415bf5cd3 Event list: Correct handling of event series 2017-08-24 10:44:22 +02:00
Raphael Michel
45b9f1190f Case insensitivity when validating repreated email addresses 2017-08-24 10:19:11 +02:00
Raphael Michel
ef1b09671a pretixdroid: Let attendee_name fall back to invoice address name 2017-08-24 10:17:32 +02:00
Raphael Michel
ee282af53e Add invoice address fields to ticket PDF editor 2017-08-24 09:53:31 +02:00
Raphael Michel
455a95d46c Add column ordering to more lists 2017-08-24 09:36:24 +02:00
Raphael Michel
76666b0d22 Update and fix trove classifiers 2017-08-23 17:47:13 +02:00
Raphael Michel
45fd43682a Docs: Add missing RST file 2017-08-23 15:13:20 +02:00
Raphael Michel
fd801e3323 Delete cache in migration 2017-08-23 15:09:47 +02:00
Raphael Michel
429c6ebb1b Fix TaxRule.__str__ 2017-08-23 15:07:01 +02:00
Raphael Michel
ea2f24fe23 Fix problem in migration 2017-08-23 14:49:08 +02:00
Raphael Michel
db4a2cfaac Docs: Add missing screenshots 2017-08-23 14:22:08 +02:00
Raphael Michel
583223f454 Update translation 2017-08-23 14:21:47 +02:00
Raphael Michel
f9fcc16f54 Do not rely on CSP nonce support (breaks safari) 2017-08-23 13:36:35 +02:00
Raphael Michel
50ca6ee63d Support custom fonts 2017-08-23 13:35:47 +02:00
Raphael Michel
56338be13e Tax rules and reverse charge (#559)
Tax rules and reverse charge
2017-08-23 13:13:16 +03:00
Raphael Michel
b9ec5ea83c Documentation on event creation 2017-08-23 10:15:51 +02:00
Raphael Michel
389585c47a Fix translation errors 2017-08-22 12:35:39 +02:00
Raphael Michel
e9583087eb Fix logic of multi downloads 2017-08-22 10:55:32 +02:00
Raphael Michel
57e2090d70 Fix date display in event list 2017-08-22 10:10:10 +02:00
Raphael Michel
5fbf26b8cb Make additional font styles optional 2017-08-22 10:09:46 +02:00
Raphael Michel
447c728557 [SECURITY] Rewrite all links in rich texts 2017-08-21 15:14:45 +02:00
Raphael Michel
a3ca4c81ae [SECURITY] Fix XSS vulnerability in typeahead.js 2017-08-21 15:14:45 +02:00
Raphael Michel
fb398a5520 [SECURITY] Fix XSS vulnerability in Lightbox caption 2017-08-21 15:14:45 +02:00
Raphael Michel
9a9bb92f91 [SECURITY] Support custom media URLs in CSP middleware 2017-08-21 15:14:45 +02:00
Raphael Michel
e23a5c24d6 [SECURITY] Add warning for download of unsafe files 2017-08-21 15:14:45 +02:00
Raphael Michel
1a42a54d98 [SECURITY] Tokens for downloading answer attachments 2017-08-21 15:14:45 +02:00
Raphael Michel
5c91352bae [SECURITY] Do not allow SVG files for logos 2017-08-21 15:14:45 +02:00
Raphael Michel
3428ea2f18 [SECURITY] Fix XSS injection vulnerabilities in question answers, event, quota and product names 2017-08-21 15:14:45 +02:00
Raphael Michel
24e5d337a6 [SECURITY] Update to morris.js master to fix a XSS vulnerability 2017-08-21 15:14:44 +02:00
Raphael Michel
a2c1413036 [SECURITY] Use defusedcsv for exports 2017-08-21 15:14:44 +02:00
Tobias Kunze
bab092f04b Do not override the Reply-To header (#597) 2017-08-20 13:50:48 +03:00
Tobias Kunze
2bf4e6c5c6 Fix import of celery app in documentation (#596) 2017-08-20 12:41:37 +03:00
Raphael Michel
584add97a3 Fix counting bug for global order search 2017-08-11 12:42:47 +02:00
Raphael Michel
57143a434e Add new signal voucher_form_validation 2017-08-10 17:06:16 +02:00
Raphael Michel
e31bd7600c Add bcc to mail_send 2017-08-09 16:22:14 +02:00
Raphael Michel
f02ec8b24b Improve Stripe.js loading 2017-08-09 13:56:52 +02:00
Raphael Michel
b8704f980f Only validate form of the selected payment 2017-08-09 13:56:52 +02:00
Raphael Michel
3accf74687 Fix KeyError in form submission 2017-08-09 13:56:52 +02:00
Tobias Kunze
a213ca746c Only mention an order in the mail if one is associated (#592) 2017-08-09 10:42:52 +02:00
Raphael Michel
349e306d38 Fix #576 yet again 2017-08-08 23:08:34 +02:00
Raphael Michel
ca1b1032eb Allow mails without HTML 2017-08-08 22:28:27 +02:00
Raphael Michel
a6c9fb0f8b Fix #576 again 2017-08-08 22:24:53 +02:00
Raphael Michel
c8230c55ee Update translation 2017-08-08 21:06:54 +02:00
Raphael Michel
55f77613d4 Fix #576 -- linebreaks in bank details in HTML mails 2017-08-08 20:37:01 +02:00
Raphael Michel
c9a1ff45c7 Fix import order 2017-08-07 19:31:20 +02:00
Raphael Michel
c209f66d49 Fix #590 -- Combined Ticket-PDFs are not invalidated when rotating secrets 2017-08-07 18:34:04 +02:00
Raphael Michel
3efa02eb81 Fixes to the download reminder 2017-08-07 17:10:04 +02:00
Raphael Michel
8506f66236 Show if team members have 2FA enabled 2017-08-07 16:15:32 +02:00
Sanket Dasgupta
cb2826f171 Fix #293 -- Add ticket downloading reminder (#567)
Closes https://github.com/pretix/pretix/issues/293
2017-08-07 16:15:27 +02:00
Raphael Michel
0990c9cc3d Fix AttributeError in voucher creation 2017-08-07 14:12:16 +02:00
Raphael Michel
4aa9594a61 Fix voucher redemption problem with subevents 2017-08-07 14:09:12 +02:00
Raphael Michel
ed208cf433 Optimize OrderFilterForm query 2017-08-07 14:04:16 +02:00
Raphael Michel
428faeb756 Add a minimal length for voucher codes 2017-08-07 12:11:48 +02:00
Raphael Michel
e858edd85c Do not allow vouchers to create negative prices 2017-08-07 12:11:48 +02:00
Raphael Michel
e4ab27a292 Fix missing file 2017-08-01 21:02:15 +02:00
Raphael Michel
eece5793d6 Fix travis after bbed8e5f 2017-08-01 20:43:28 +02:00
Nicole Klünder
3df737a94f fix missing space in german translation (#587) 2017-08-01 20:39:39 +02:00
Nicole Klünder
0e4c414c2e fix wrong stripe version in setup.py (#588) 2017-08-01 20:39:25 +02:00
Raphael Michel
326304db54 Fix #583 -- Wrongly documented option 2017-07-31 23:00:48 +02:00
Raphael Michel
c8e54524a3 Only use SQLite config during tests if it exists 2017-07-31 21:36:46 +02:00
Raphael Michel
d671060a47 Add sphinxcontrib-images to doc dependencies 2017-07-31 21:14:00 +02:00
Raphael Michel
93dab76da2 Complete docs page 2017-07-31 21:12:30 +02:00
Nicole Klünder
bbed8e5fae throw exception if PRETIX_CONFIG_FILE can not be opened (#581)
If the environment variable PRETIX_CONFIG_FILE is set but the file can not be read because it does not exists or permission is denied, pretix just runs with default settings. When setting up a new installation this can be confusing and difficult to debug.

I think it is safe to assume that someone who sets PRETIX_CONFIG_FILE aims to point it at a readable file, so raising with a more understandable exception is expected or at least helpful. Otherwise, the user will usually get a DisallowedHost exception because the [pretix]url config variable is not set which is not as helpful.
2017-07-31 18:33:16 +02:00
Raphael Michel
e16f8fc7e9 Add some user documentation 2017-07-31 18:31:20 +02:00
Raphael Michel
86f17094bb Hide quota options when creating a product with variations 2017-07-31 13:52:50 +02:00
Raphael Michel
b1b49758b1 Fix reversal bug 2017-07-31 12:54:57 +02:00
Raphael Michel
4790665759 bump version 2017-07-31 12:54:57 +02:00
Tobias Kunze
8ede492cba Add optional help_text to Question objects. Closes #574. (#579) 2017-07-31 10:54:57 +02:00
Raphael Michel
5f607cc034 Bump version 2017-07-30 20:36:45 +02:00
Raphael Michel
3b9f508be9 Create squash migration 2017-07-30 20:11:23 +02:00
Raphael Michel
89e381b7ea Update translations 2017-07-30 19:44:11 +02:00
Tobias Kunze
57869b2145 Wording fixes (#577) 2017-07-30 19:29:17 +02:00
Raphael Michel
46976900d7 Add documentation on mail methods 2017-07-27 15:47:22 +02:00
Raphael Michel
a1535da117 Subevent creation: Layout issue 2017-07-27 15:18:25 +02:00
Raphael Michel
f43d782b5c Hide quota options during product creation for event series 2017-07-27 15:14:28 +02:00
Daniel
5c443e2f93 Ref #569 -- Include other email to history (#570)
Log all other order-specified emails and include them into
email history.
2017-07-27 15:09:06 +02:00
Raphael Michel
54f01f63f7 Update translation 2017-07-27 11:53:14 +02:00
Raphael Michel
c64b4473e4 Add organizer_info_text 2017-07-27 11:43:35 +02:00
Raphael Michel
d413a37c1f Translation improvements 2017-07-27 11:21:28 +02:00
Raphael Michel
202fb12008 Order payment providers by name 2017-07-27 11:17:29 +02:00
Raphael Michel
59dea63870 Add product description to ticket editor 2017-07-27 10:13:55 +02:00
Raphael Michel
9a18f2b553 Small JS improvements 2017-07-26 13:41:53 +02:00
Clint
4293ec3805 Fix #475 -- add to existing quota / add new quota from product form (#562)
* added fields and logic for adding quota in creation of product

* added section for selecting quota option

* logic for hiding quota selections when needed

* fixed logic for quota selection

* formatting, removed print statements

* styling

* tests for adding quotas in product form

* cleaned up

* added divs

* reworked to include translatable text, readable values for quota options

* quota_add_existing form assignment to str(q.pk)

* made changes for radio buttons, added in sliding animation

* moved string constants for quota options, changed quota options to RadioSelect from Select
2017-07-26 13:41:23 +02:00
Raphael Michel
f3b616e495 Add exporter to output all tickets 2017-07-26 13:18:35 +02:00
Raphael Michel
003ea24990 Fix Stripe test 2017-07-25 16:52:37 +02:00
Raphael Michel
92d4566a54 Make stripe refund call optional, add request parameter to order_control_refund_render 2017-07-25 16:11:42 +02:00
Raphael Michel
70a933edc1 Hide min_per_order=1 2017-07-24 09:54:33 +02:00
Raphael Michel
d2d77f28aa Fix wrongly displayed download buttons 2017-07-24 09:14:54 +02:00
Raphael Michel
39179971c5 Fix stripe tests 2017-07-23 13:33:43 +02:00
Raphael Michel
c06f36e8c2 Do not render time before JS locale file is loaded 2017-07-23 13:06:07 +02:00
Raphael Michel
1d2d9d8b99 PayPal: Event-independent webhooks 2017-07-23 12:38:41 +02:00
Raphael Michel
5f529817ef Stripe: Event-independent webhooks 2017-07-23 12:22:48 +02:00
Raphael Michel
7c91bc2f37 Respect primary browser language 2017-07-20 11:31:34 +02:00
Raphael Michel
ef022f5a6d Add new dependencies to setup.py 2017-07-19 17:42:07 +02:00
Raphael Michel
54ce00c8b9 Update translation 2017-07-19 14:17:23 +02:00
Raphael Michel
baabbfb1ea Check for required invoice information in other steps 2017-07-19 14:10:04 +02:00
Raphael Michel
28e676ac9a Allow to only require the name in the invoice address 2017-07-19 14:07:18 +02:00
Raphael Michel
21fac9ec7a Auto-toggle form fields for business/individual customers 2017-07-19 12:26:50 +02:00
Raphael Michel
335955820b Fix a counting bug 2017-07-19 12:10:35 +02:00
Raphael Michel
d2b0e7209f Use a choice field for invoice address countries 2017-07-19 12:08:18 +02:00
Johan von Forstner
f8ed21c819 Fix #130 -- Mark required question fields at checkout (#564)
* Fix #130: Mark required question fields at checkout

* Add legend
2017-07-19 11:02:07 +02:00
Tobias Kunze
9c2143effe Clarify mail subject prefix. (#520) 2017-07-19 10:42:34 +02:00
Raphael Michel
8a3fa6aff6 Show category descriptions if redeeming a voucher 2017-07-18 16:47:18 +02:00
Daniel
7e304bb231 Fix #565 -- OrderList ordering request handling (#566)
Fix missed out ordering get request handling.
2017-07-18 16:16:51 +02:00
Raphael Michel
99d614289e Link order from change pages, compat for old logs 2017-07-18 11:46:21 +02:00
Daniel
b90894c20f Fix #543 -- Allow send mail from order (#550)
- Add send email directly for order
- Add email history (from mass and custom) to each specific order
2017-07-18 11:45:30 +02:00
Johan von Forstner
921834c917 Fix os.chown call on Windows (#563) 2017-07-18 11:42:30 +02:00
Raphael Michel
83df4451e6 Fix #491 -- Search order by invoice number 2017-07-18 11:14:51 +02:00
Raphael Michel
2ad9e1bb43 Change API to accomodate for invoice numbers 2017-07-18 10:37:32 +02:00
Raphael Michel
c9990e5ca4 Update translation 2017-07-18 10:04:24 +02:00
Raphael Michel
c95e61db09 Add new external plugins to list 2017-07-17 23:32:41 +02:00
Raphael Michel
7bb12ff0ec Configurable invoice prefixes 2017-07-17 23:03:20 +02:00
Raphael Michel
670bfa18de Slug widget 2017-07-17 20:54:39 +02:00
Raphael Michel
130f619b05 Fix raven integration 2017-07-17 20:29:56 +02:00
Raphael Michel
f900c842cb Improve "copy from above" 2017-07-17 16:12:33 +02:00
Tobias Kunze
c2844a8f35 Add custom_header block to <head> (#561) 2017-07-17 14:52:20 +02:00
Raphael Michel
a864dabbaf Refs #552 -- Expand all panels that contain validation errors 2017-07-17 12:49:45 +02:00
Daniel
02786f4801 Fix #552 -- Add icon to expansible panels (#560)
* Ref #552 -- Add icon to expansible panels

Offer informative feedback to user by showing icons to indicate
expansible panels.

* Fix wrongly removed JavaScript line
2017-07-17 12:29:47 +02:00
Raphael Michel
5a4fe266c6 Fix usage of wrong settings key 2017-07-16 22:17:02 +02:00
Raphael Michel
b30a4db0b8 Update translation 2017-07-16 20:15:48 +02:00
Raphael Michel
2e76e07764 Show end date on front page 2017-07-16 20:11:59 +02:00
Raphael Michel
1be92f5078 Really fix 2050c0b7 issue 2017-07-16 19:37:18 +02:00
Raphael Michel
8afff29cd4 Make microdata XSS-safe and subevent-aware 2017-07-16 17:52:08 +02:00
Raphael Michel
9c6090a355 Fix translation sytax error 2017-07-16 17:49:31 +02:00
Joepriesto
27b73227ed Fix #30 -- Event microdata information (#546)
* Addition of all basics required for microdata

Microdata for event added to the head of the base event html template.

* Fixed flake8 errors & included settings checks

Fixed the flake8 too many blank line errors and changed the date and time section to refer to settings before including the time and/or the date_to.

* Typo in last commit

Fixed typo in last commit

* Fixed bracket mistake and flake8 error

Fixed tab between { and % in template tag. Removed extra lines. Removed unnecessary str() call.
Corrected binary & to [and] operator.

* Added testing for microdata

Testing added to the presale.test_event tests to cover the 4 branches and basic microdata functionality.
2017-07-16 17:42:46 +02:00
Raphael Michel
9582f8380f Fix translation syntax 2017-07-16 17:31:35 +02:00
Raphael Michel
bfeac7e70b Fix bogus translation 2017-07-16 17:21:07 +02:00
Raphael Michel
2050c0b7a8 Fix TypeError 2017-07-16 17:05:46 +02:00
Raphael Michel
144f9bed69 Fix URL in iCal test 2017-07-15 16:38:14 +02:00
Raphael Michel
f2b642d944 Fix iCal URL 2017-07-14 18:26:18 +02:00
Raphael Michel
bcdc75953e Add SubEvent.frontpage_text 2017-07-14 18:14:24 +02:00
Raphael Michel
1fabe5a7cf Disable button "add to cart" with no selection 2017-07-14 18:10:49 +02:00
Raphael Michel
fe9a4b7aa3 Allow word wrapping inside .dl-horizontal dt 2017-07-14 18:10:18 +02:00
Raphael Michel
56735dc1c6 Make PDF ticket output subevent-compatible 2017-07-14 18:09:48 +02:00
Raphael Michel
42287b92f1 Allow ticket output methods to opt-out from ZIP generation 2017-07-14 17:32:16 +02:00
Raphael Michel
7d9e642f24 Useful default date for admission field 2017-07-14 17:21:41 +02:00
Raphael Michel
b20e10585f Include Event.is_public in Event.copy_data_from 2017-07-14 17:11:52 +02:00
Raphael Michel
8438b211a6 Fix iCal test failing due to new UID format 2017-07-14 14:55:08 +02:00
Raphael Michel
f94314afec Generate organizer-level iCal files 2017-07-14 14:25:05 +02:00
Martin Gross
4584d23434 Fix #556 - Max. redemptions instead of amount of vouchers (#558) 2017-07-14 12:11:34 +02:00
Raphael Michel
2791501781 Fix #557 -- Drop unused CSS file 2017-07-14 12:11:20 +02:00
Raphael Michel
6ea798e55b Allow to exclude free products from invoices 2017-07-14 12:11:20 +02:00
Martin Gross
0ab6ac569e Fix #553 -- Order by position first in order_overview (#554) 2017-07-14 10:22:26 +02:00
Raphael Michel
f91d7352a4 Fix AttributeError in test 2017-07-13 22:04:34 +02:00
Raphael Michel
d644b8fe01 Fix detection of core modules after ae910eb7 2017-07-13 20:33:08 +02:00
Raphael Michel
5a8042cc10 Fix relative dates in payment method availability field 2017-07-13 17:37:07 +02:00
Raphael Michel
ae910eb731 Fix EventPluginSignal to recognize Django apps with custom app labels 2017-07-12 19:14:28 +02:00
Raphael Michel
c1158c3175 Allow to mark add-ons as "included" in price 2017-07-12 18:42:09 +02:00
Raphael Michel
79562e7ad9 Add logdisplay for charge.pending 2017-07-12 18:02:56 +02:00
Raphael Michel
e3388bea96 Fix Stripe webhooks 2017-07-12 17:37:05 +02:00
Daniel
a3b4a7ef1d Fix #516 -- Prevent gaps in plugin list (#551)
Use flexbox to fix misalignment when column height is imbalanced.
2017-07-12 17:30:01 +02:00
Raphael Michel
947b06cd61 Update translation 2017-07-12 17:20:09 +02:00
Raphael Michel
a3f3561f02 Fix import in stripe tests 2017-07-12 16:56:01 +02:00
Raphael Michel
48095d38be Fix #407 -- Integrate more Stripe payment methods 2017-07-12 16:42:44 +02:00
Raphael Michel
1c6858653a Avoid relative URLs in stripe plugin 2017-07-11 22:53:15 +02:00
Raphael Michel
648797325e Allow to copy across events with and w/o subevents 2017-07-11 22:45:41 +02:00
Raphael Michel
af3fa88d67 Typos and translation 2017-07-11 18:47:39 +02:00
Raphael Michel
8123effa65 Add sub-events and relative date settings (#503)
* Data model

* little crud

* SubEventItemForm etc

* Drop SubEventItem.active, quota editor

* Fix failing tests

* First frontend stuff

* Addons form stuff

* Quota calculation

* net price display on EventIndex

* Add tests, solve some bugs

* Correct quota selection in more places, consolidate pricing logic

* Fix failing quota tests

* Fix TypeError

* Add tests for checkout

* Fixed a bug in QuotaForm

* Prevent immutable cart if a quota was removed from an item

* Add tests for pricing

* Handle waiting list

* Filter in check-in list

* Fixed import lost in rebase

* Fix waiting list widget

* Voucher management

* Voucher redemption

* Fix broken tests

* Add subevents to OrderChangeManager

* Create a subevent during event creation

* Fix bulk voucher creation

* Introduce subevent.active

* Copy from for subevents

* Show active in list

* ICal download for subevents

* Check start and end of presale

* Failing tests / show cart logic

* Test

* Rebase migrations

* REST API integration of sub-events

* Integrate quota calculation into the traditional quota form

* Make subevent argument to add_position optional

* Log-display foo

* pretixdroid and subevents

* Filter by subevent

* Add more tests

* Some mor tests

* Rebase fixes

* More tests

* Relative dates

* Restrict selection in relative datetime widgets

* Filter subevent list

* Re-label has_subevents

* Rebase fixes, subevents in calendar view

* Performance and caching issues

* Refactor calendar templates

* Permission tests

* Calendar fixes and month selection

* subevent selection

* Rename subevents to dates

* Add tests for calendar views
2017-07-11 13:56:00 +02:00
Raphael Michel
554800c06f Add 3DS support to Stripe plugin 2017-07-10 21:36:59 +02:00
Raphael Michel
687ce29366 Fix #547 -- Switch to Stripe Elements 2017-07-10 20:00:03 +02:00
Raphael Michel
c70301572c Fix bug in invoice rendering 2017-07-10 14:43:37 +02:00
Raphael Michel
714f58e2c5 Update translation 2017-07-10 14:29:53 +02:00
Raphael Michel
9bf9dca88a Invoice exporter: open in binary mode 2017-07-10 14:25:26 +02:00
Daniel
0663f25208 Fix #541 -- Allow delete waitinglist entries (#544)
Add new action "delete" for each waiting list entries
2017-07-09 15:19:30 +02:00
Raphael Michel
275d162b81 List of events: Optional calendar view 2017-07-08 22:50:48 +02:00
Raphael Michel
675956a7c4 Update translations 2017-07-07 11:37:09 +02:00
Raphael Michel
cf1602c0af Add short form to event list 2017-07-07 11:32:53 +02:00
Raphael Michel
35979ed332 Add internal comment to events 2017-07-07 11:31:33 +02:00
Raphael Michel
6e65ae5306 Swappable invoice renderers 2017-07-07 11:16:07 +02:00
Raphael Michel
95e716b8ce Improve help texts 2017-07-07 10:11:39 +02:00
Raphael Michel
554284ac67 Link new plugins 2017-07-06 18:07:03 +02:00
Raphael Michel
fa6102bbad Allow to override sender for mail 2017-07-06 18:06:07 +02:00
Raphael Michel
f28d5f19a7 Ignore spaces in bank transfer references 2017-07-06 17:39:32 +02:00
Raphael Michel
34d59c7741 Add "Go to shop" in mobile view 2017-07-06 17:36:01 +02:00
Raphael Michel
21d432a3ca Make cancel notifications optional 2017-07-06 12:04:35 +02:00
Daniel
d444935140 Fix #391 -- Send email on cancelled order (#540)
Send a notification email to user when
order is cancelled.
2017-07-06 11:59:48 +02:00
Raphael Michel
9de9d96e35 Do not expose "Deutsch (Du)" in name-local 2017-07-06 09:26:26 +02:00
Michele Fattoruso
5932558ca2 Fix #300 - Feature/remove flag as language (#542)
* don't use flags to indicate languages #300

* cleaned code

* removed the filter and moved the logic to context.py

* cleaned code

* show the text in the local language

* cleaned code

* changed loop for list comprehension

* fix indentation

* ordered import
2017-07-06 09:13:57 +02:00
Raphael Michel
7b22adb72e Allow to manually generate invoice if invoice mode is "on payment" 2017-07-05 13:34:07 +02:00
Raphael Michel
0db5d062be Add a file upload type to questions (#534)
* Initial stuff

* More features
2017-07-03 14:22:31 +02:00
Sanket Dasgupta
678d510e29 config.rst: Include environment variable in list of paths (#539)
Closes https://github.com/pretix/pretix/issues/528
2017-07-03 12:52:36 +02:00
Raphael Michel
1fc3307d22 Add signal question_form_fields 2017-07-02 19:45:26 +02:00
Raphael Michel
45c17ba949 Fix syntax error in setup.py 2017-07-02 18:57:55 +02:00
Raphael Michel
15bd1d9006 Fix missing dependencies in setup.py 2017-07-02 18:44:25 +02:00
Tydon K
b4715f0931 Fix #517 -- Changed navpills to navtabs in email template editor (#533)
* Changed navpills to navtabs in email template editor

* Issue 517 - Added border radius to email template tabs and removed excess bottom margin.
2017-07-02 17:51:41 +02:00
Raphael Michel
6f24a2a88c Fix import in unit test 2017-07-02 17:36:12 +02:00
Raphael Michel
fcbecf895a Bump version to 1.6.0.dev0 2017-07-02 17:32:02 +02:00
388 changed files with 28871 additions and 7323 deletions

View File

@@ -8,6 +8,8 @@ tests:
- XDG_CACHE_HOME=/cache bash .travis.sh tests
tags:
- python3
except:
- pypi
pypi:
stage: release
script:
@@ -22,7 +24,7 @@ pypi:
tags:
- python3
only:
- release
- pypi
artifacts:
paths:
- src/dist/

View File

@@ -12,29 +12,29 @@ services:
- postgresql
matrix:
include:
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/sqlite.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/sqlite.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/sqlite.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=style
- python: 3.6
env: JOB=plugins
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.6
env: JOB=tests-cov
- python: 3.6
env: JOB=style
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.4
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=plugins
addons:
postgresql: "9.4"

View File

@@ -6063,3 +6063,39 @@ url('../opensans_regular_macroman/OpenSans-Regular-webfont.svg#open_sansregular'
white-space: normal;
}
}
img.screenshot, a.screenshot img {
box-shadow: 0 4px 18px 0 rgba(0,0,0,0.1), 0 6px 20px 0 rgba(0,0,0,0.09);
}
/* Changes */
.versionchanged {
background: #e7f2fa;
padding: 12px;
line-height: 24px;
margin-bottom: 24px;
-webkit-font-smoothing: antialiased;
}
.versionmodified {
background: #6ab0de;
font-weight: bold;
display: block;
color: #fff;
margin: -12px;
padding: 6px 12px;
margin-bottom: 12px;
font-family: inherit;
}
.versionmodified:before {
font-family: "FontAwesome";
display: inline-block;
font-style: normal;
font-weight: normal;
line-height: 1;
text-decoration: inherit;
content: "";
margin-right: 4px;
}
.versionchanged p:last-child {
margin-bottom: 0;
}

View File

@@ -10,9 +10,10 @@ at the following locations. It will try to read the file from the specified path
the following order. The file that is found *last* will override the settings from
the files found before.
1. ``/etc/pretix/pretix.cfg``
2. ``~/.pretix.cfg``
3. ``pretix.cfg`` in the current working directory
1. ``PREFIX_CONFIG_FILE`` environment variable
2. ``/etc/pretix/pretix.cfg``
3. ``~/.pretix.cfg``
4. ``pretix.cfg`` in the current working directory
The file is expected to be in the INI format as specified in the `Python documentation`_.
@@ -59,6 +60,14 @@ Example::
``password_reset``
Enables or disables password reset. Defaults to ``on``.
``long_sessions``
Enables or disables the "keep me logged in" button. Defaults to ``on``.
``ecb_rates``
By default, pretix periodically downloads a XML file from the European Central Bank to retrieve exchange rates
that are used to print tax amounts in the customer currency on invoices for some currencies. Set to ``off`` to
disable this feature. Defaults to ``on``.
Locale settings
---------------
@@ -163,14 +172,9 @@ Django settings
Example::
[django]
hosts=localhost
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
debug=off
``hosts``
Comma-separated list of allowed host names for this installation.
Default: ``localhost``
``secret``
The secret to be used by Django for signing and verification purposes. If this
setting is not provided, pretix will generate a random secret on the first start

View File

@@ -4,6 +4,8 @@ Basic concepts
This page describes basic concepts and definition that you need to know to interact
with pretix' REST API, such as authentication, pagination and similar definitions.
.. _`rest-auth`:
Obtaining an API token
----------------------
@@ -13,12 +15,14 @@ or choose an existing team that has the level of permissions the token should ha
create a new token using the form below the list of team members:
.. image:: img/token_form.png
:class: screenshot
You can enter a description for the token to distinguish from other tokens later on.
Once you click "Add", you will be provided with an API token in the success message.
Copy this token, as you won't be able to retrieve it again.
.. image:: img/token_success.png
:class: screenshot
Authentication
--------------

View File

@@ -24,8 +24,14 @@ is_public boolean If ``true``, th
presale_start datetime The date at which the ticket shop opens (or ``null``)
presale_end datetime The date at which the ticket shop closes (or ``null``)
location multi-lingual string The event location (or ``null``)
has_subevents boolean ``True`` if the event series feature is active for this
event
meta_data dict Values set for organizer-specific meta data parameters.
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The ``meta_data`` field has been added.
Endpoints
---------
@@ -67,6 +73,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"has_subevents": false,
"meta_data": {}
}
]
}
@@ -109,6 +117,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"has_subevents": false,
"meta_data": {}
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -6,6 +6,8 @@ Resources and endpoints
organizers
events
subevents
taxrules
categories
items
questions

View File

@@ -11,7 +11,7 @@ The invoice resource contains the following public fields:
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
invoice_no string Invoice number (without prefix)
number string Invoice number (with prefix)
order string Order code of the order this invoice belongs to
is_cancellation boolean ``True``, if this invoice is the cancellation of a
different invoice.
@@ -29,12 +29,34 @@ payment_provider_text string Text to be prin
footer_text string Text to be printed in the page footer area
lines list of objects The actual invoice contents
├ description string Text representing the invoice line (e.g. product name)
├ gross_value money (string) Price including VAT
├ tax_value money (string) VAT amount
tax_rate decimal (string) Used VAT rate
├ gross_value money (string) Price including taxes
├ tax_value money (string) Tax amount included
tax_name string Name of used tax rate (e.g. "VAT")
└ tax_rate decimal (string) Used tax rate
foreign_currency_display string If the invoice should also show the total and tax
amount in a different currency, this contains the
currency code (``null`` otherwise).
foreign_currency_rate decimal (string) If ``foreign_currency_rate`` is set and the system
knows the exchange rate to the event currency at
invoicing time, it is stored here.
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
date at which the currency rate was obtained.
===================================== ========================== =======================================================
.. versionchanged:: 1.6
The attribute ``invoice_no`` has been dropped in favor of ``number`` which includes the number including the prefix,
since the prefix can now vary. Also, invoices now need to be identified by their ``number`` instead of the raw
number.
.. versionchanged:: 1.7
The attributes ``lines.tax_name``, ``foreign_currency_display``, ``foreign_currency_rate``, and
``foreign_currency_rate_date`` have been added.
Endpoints
---------
@@ -64,7 +86,7 @@ Endpoints
"previous": null,
"results": [
{
"invoice_no": "00001",
"number": "SAMPLECONF-00001",
"order": "ABC12",
"is_cancellation": false,
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
@@ -81,9 +103,13 @@ Endpoints
"description": "Budget Ticket",
"gross_value": "23.00",
"tax_value": "0.00",
"tax_name": "VAT",
"tax_rate": "0.00"
}
]
],
"foreign_currency_display": "PLN",
"foreign_currency_rate": "4.2408",
"foreign_currency_rate_date": "2017-07-24"
}
]
}
@@ -95,14 +121,14 @@ Endpoints
:query string refers: If set, only invoices refering to the given invoice will be returned.
:query string locale: If set, only invoices with the given locale will be returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
``invoice_no``. Default: ``invoice_no``
``nr`` (equals to ``number``). Default: ``nr``
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/
Returns information on one invoice, identified by its invoice number.
@@ -110,7 +136,7 @@ Endpoints
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/ HTTP/1.1
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/SAMPLECONF-00001/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
@@ -123,7 +149,7 @@ Endpoints
Content-Type: text/javascript
{
"invoice_no": "00001",
"number": "SAMPLECONF-00001",
"order": "ABC12",
"is_cancellation": false,
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
@@ -140,9 +166,13 @@ Endpoints
"description": "Budget Ticket",
"gross_value": "23.00",
"tax_value": "0.00",
"tax_name": "VAT",
"tax_rate": "0.00"
}
]
],
"foreign_currency_display": "PLN",
"foreign_currency_rate": "4.2408",
"foreign_currency_rate_date": "2017-07-24"
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -27,6 +27,7 @@ free_price boolean If ``True``, cu
lower than the price defined by ``default_price`` or
otherwise).
tax_rate decimal (string) The VAT rate to be applied for this item.
tax_rule integer The internal ID of the applied tax rule (or ``null``).
admission boolean ``True`` for items that grant admission to the event
(such as primary tickets) and ``False`` for others
(such as add-ons or merchandise).
@@ -49,6 +50,9 @@ min_per_order integer This product ca
max_per_order integer This product can only be bought if it is included at
most this many times in the order (or ``null`` for no
limitation).
checkin_attention boolean If ``True``, the check-in app should show a warning
that this ticket requires special attention if such
a product is being scanned.
has_variations boolean Shows whether or not this item has variations
(read-only).
variations list of objects A list with one object for each variation of this item.
@@ -70,6 +74,11 @@ addons list of objects Definition of a
└ position integer An integer, used for sorting
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The attribute ``tax_rule`` has been added. ``tax_rate`` is kept for compatibility. The attribute
``checkin_attention`` has been added.
Endpoints
---------
@@ -108,6 +117,7 @@ Endpoints
"description": null,
"free_price": false,
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"position": 0,
"picture": null,
@@ -118,6 +128,7 @@ Endpoints
"allow_cancel": true,
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"has_variations": false,
"variations": [
{
@@ -188,6 +199,7 @@ Endpoints
"description": null,
"free_price": false,
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"position": 0,
"picture": null,
@@ -198,6 +210,7 @@ Endpoints
"allow_cancel": true,
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"has_variations": false,
"variations": [
{

View File

@@ -27,20 +27,38 @@ expires datetime The order will
payment_date date Date of payment receival
payment_provider string Payment provider used for this order
payment_fee money (string) Payment fee included in this order's total
payment_fee_tax_rate decimal (string) VAT rate applied to the payment fee
payment_fee_tax_value money (string) VAT value included in the payment fee
payment_fee_tax_rate decimal (string) Tax rate applied to the payment fee
payment_fee_tax_value money (string) Tax value included in the payment fee
payment_fee_tax_rule integer The ID of the used tax rule (or ``null``)
total money (string) Total value of this order
comment string Internal comment on this order
invoice_address object Invoice address information (can be ``null``)
├ last_modified datetime Last modification date of the address
├ company string Customer company name
├ is_business boolean Business or individual customers (always ``False``
for orders created before pretix 1.7, do not rely on
it).
├ name string Customer name
├ street string Customer street
├ zipcode string Customer ZIP code
├ city string Customer city
├ country string Customer country
vat_id string Customer VAT ID
vat_id string Customer VAT ID
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
EU VAT service and validation was successful. This only
happens in rare cases.
position list of objects List of order positions (see below)
fees list of objects List of fees included in the order total (i.e.
payment fees)
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
``other``)
├ value money (string) Fee amount
├ description string Human-readable string with more details (can be empty)
├ internal_type string Internal string (i.e. ID of the payment provider),
can be empty
├ tax_rate decimal (string) VAT rate applied for this fee
├ tax_value money (string) VAT included in this fee
└ tax_rule integer The ID of the used tax rule (or ``null``)
downloads list of objects List of ticket download options for order-wise ticket
downloading. This might be a multi-page PDF or a ZIP
file of tickets for outputs that do not support
@@ -50,6 +68,19 @@ downloads list of objects List of ticket
└ url string Download URL
===================================== ========================== =======================================================
.. versionchanged:: 1.6
The ``invoice_address.country`` attribute contains a two-letter country code for all new orders. For old orders,
a custom text might still be returned.
.. versionchanged:: 1.7
The attributes ``invoice_address.vat_id_validated`` and ``invoice_address.is_business`` have been added.
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate`` and ``order.payment_fee_tax_value`` have been
deprecated in favour of the new ``fees`` attribute but will still be served and removed in 1.9.
Order position resource
-----------------------
@@ -69,8 +100,10 @@ attendee_email string Specified atten
voucher integer Internal ID of the voucher used for this position (or ``null``)
tax_rate decimal (string) VAT rate applied for this position
tax_value money (string) VAT included in this position
tax_rule integer The ID of the used tax rule (or ``null``)
secret string Secret code printed on the tickets for validation
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
checkins list of objects List of check-ins with this ticket
└ datetime datetime Time of check-in
downloads list of objects List of ticket download options
@@ -82,6 +115,10 @@ answers list of objects Answers to user
└ options list of integers Internal IDs of selected option(s)s (only for choice types)
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The attribute ``tax_rule`` has been added.
Order endpoints
---------------
@@ -121,20 +158,20 @@ Order endpoints
"expires": "2017-12-10T10:00:00Z",
"payment_date": "2017-12-05",
"payment_provider": "banktransfer",
"payment_fee": "0.00",
"payment_fee_tax_rate": "0.00",
"payment_fee_tax_value": "0.00",
"fees": [],
"total": "23.00",
"comment": "",
"invoice_address": {
"last_modified": "2017-12-01T10:00:00Z",
"is_business": True,
"company": "Sample company",
"name": "John Doe",
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"vat_id": "EU123456789"
"vat_id": "EU123456789",
"vat_id_validated": False
},
"positions": [
{
@@ -149,8 +186,10 @@ Order endpoints
"voucher": null,
"tax_rate": "0.00",
"tax_value": "0.00",
"tax_rule": null,
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"checkins": [
{
"datetime": "2017-12-25T12:45:23Z"
@@ -224,20 +263,20 @@ Order endpoints
"expires": "2017-12-10T10:00:00Z",
"payment_date": "2017-12-05",
"payment_provider": "banktransfer",
"payment_fee": "0.00",
"payment_fee_tax_rate": "0.00",
"payment_fee_tax_value": "0.00",
"fees": [],
"total": "23.00",
"comment": "",
"invoice_address": {
"last_modified": "2017-12-01T10:00:00Z",
"company": "Sample company",
"is_business": True,
"name": "John Doe",
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"vat_id": "EU123456789"
"vat_id": "EU123456789",
"vat_id_validated": False
},
"positions": [
{
@@ -251,9 +290,11 @@ Order endpoints
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"checkins": [
{
"datetime": "2017-12-25T12:45:23Z"
@@ -369,9 +410,11 @@ Order position endpoints
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"checkins": [
{
"datetime": "2017-12-25T12:45:23Z"
@@ -407,6 +450,7 @@ Order position endpoints
:query string order__status: Only return positions with the given order status.
:query bollean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
checked in already.
:query integer subevent: Only return positions of the sub-event with the given ID
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
@@ -445,9 +489,11 @@ Order position endpoints
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"checkins": [
{
"datetime": "2017-12-25T12:45:23Z"

View File

@@ -22,6 +22,7 @@ type string The expected ty
* ``B`` boolean
* ``C`` choice from a list
* ``M`` multiple choice from a list
* ``F`` file upload
required boolean If ``True``, the question needs to be filled out.
position integer An integer, used for sorting
items list of integers List of item IDs this question is assigned to.

View File

@@ -17,6 +17,7 @@ name string The internal na
size integer The size of the quota or ``null`` for unlimited
items list of integers List of item IDs this quota acts on.
variations list of integers List of item variation IDs this quota acts on.
subevent integer ID of the date inside an event series this quota belongs to (or ``null``).
===================================== ========================== =======================================================
@@ -53,7 +54,8 @@ Endpoints
"name": "Ticket Quota",
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7]
"variations": [1, 4, 5, 7],
"subevent": null
}
]
}
@@ -61,6 +63,7 @@ Endpoints
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
Default: ``position``
:query integer subevent: Only return quotas of the sub-event with the given ID
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
@@ -92,7 +95,8 @@ Endpoints
"name": "Ticket Quota",
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7]
"variations": [1, 4, 5, 7],
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -0,0 +1,144 @@
Event series dates / Sub-events
===============================
Resource description
--------------------
Events can represent whole event series if the ``has_subevents`` property of the event is active.
In this case, many other resources are additionally connected to an event date (also called sub-event).
The sub-event resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the sub-event
name multi-lingual string The sub-event's full name
active boolean If ``true``, the sub-event ticket shop is publicly
available.
date_from datetime The sub-event's start date
date_to datetime The sub-event's end date (or ``null``)
date_admission datetime The sub-event's admission date (or ``null``)
presale_start datetime The sub-date at which the ticket shop opens (or ``null``)
presale_end datetime The sub-date at which the ticket shop closes (or ``null``)
location multi-lingual string The sub-event location (or ``null``)
item_price_overrides list of objects List of items for which this sub-event overrides the
default price
├ item integer The internal item ID
└ price money (string) The price or ``null`` for the default price
variation_price_overrides list of objects List of variations for which this sub-event overrides
the default price
├ variation integer The internal variation ID
└ price money (string) The price or ``null`` for the default price
meta_data dict Values set for organizer-specific meta data parameters.
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The ``meta_data`` field has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
Returns a list of all sub-events of an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": {"en": "First Sample Conference"},
"active": false,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"item_price_overrides": [
{
"item": 2,
"price": "12.00"
}
],
"variation_price_overrides": [],
"meta_data": {}
}
]
}
: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 does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
Returns information on one sub-event, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"name": {"en": "First Sample Conference"},
"active": false,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"item_price_overrides": [
{
"item": 2,
"price": "12.00"
}
],
"variation_price_overrides": [],
"meta_data": {}
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``slug`` field of the sub-event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.

View File

@@ -0,0 +1,109 @@
Tax rules
=========
Resource description
--------------------
Tax rules specify how tax should be calculated for specific products.
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the tax rule
name multi-lingual string The tax rules' name
rate decimal (string) Tax rate in percent
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
the specified product price
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied
home_country string Merchant country (required for reverse charge), can be
``null`` or empty string
===================================== ========================== =======================================================
.. versionchanged:: 1.7
This resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/taxrules/
Returns a list of all tax rules configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/taxrules/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": {"en": "VAT"},
"rate": "19.00",
"price_includes_tax": true,
"eu_reverse_charge": false,
"home_country": "DE"
}
]
}
: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 does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/taxrules/(id)/
Returns information on one tax rule, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/taxrules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"name": {"en": "VAT"},
"rate": "19.00",
"price_includes_tax": true,
"eu_reverse_charge": false,
"home_country": "DE"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``slug`` field of the sub-event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.

View File

@@ -40,6 +40,7 @@ quota integer An ID of a quot
for all items without restriction.
tag string A string that is used for grouping vouchers
comment string An internal comment on the voucher
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
===================================== ========================== =======================================================
@@ -85,7 +86,8 @@ Endpoints
"variation": null,
"quota": null,
"tag": "testvoucher",
"comment": ""
"comment": "",
"subevent": null
}
]
}
@@ -107,6 +109,7 @@ Endpoints
:query integer variation: If set, only vouchers attached to the variation with the given ID will be shown.
:query integer quota: If set, only vouchers attached to the quota with the given ID will be shown.
:query string tag: If set, only vouchers with the given tag will be shown.
:query integer subevent: Only return vouchers of the sub-event with the given ID
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``code``,
``max_usages``, ``valid_until``, and ``value``. Default: ``id``
:param organizer: The ``slug`` field of the organizer to fetch
@@ -149,7 +152,8 @@ Endpoints
"variation": null,
"quota": null,
"tag": "testvoucher",
"comment": ""
"comment": "",
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -23,6 +23,7 @@ item integer An ID of an ite
variation integer An ID of a variation the user is waiting to be
available again (or ``null``)
locale string Locale of the waiting user
subevent integer ID of the date inside an event series this entry belongs to (or ``null``).
===================================== ========================== =======================================================
@@ -61,7 +62,8 @@ Endpoints
"voucher": null,
"item": 2,
"variation": null,
"locale": "en"
"locale": "en",
"subevent": null
}
]
}
@@ -73,6 +75,7 @@ Endpoints
have not been sent a voucher.
:query integer item: If set, only entries of users waiting for the item with the given ID will be shown.
:query integer variation: If set, only entries of users waiting for the variation with the given ID will be shown.
:query integer subevent: Only return entries of the sub-event with the given ID
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``created``,
``email``, ``item``. Default: ``created``
:param organizer: The ``slug`` field of the organizer to fetch
@@ -108,7 +111,8 @@ Endpoints
"voucher": null,
"item": 2,
"variation": null,
"locale": "en"
"locale": "en",
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -13,6 +13,10 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
from docutils.parsers.rst.directives.admonitions import BaseAdmonition
from sphinx.util import compat
compat.make_admonition = BaseAdmonition # See https://github.com/spinus/sphinxcontrib-images/issues/41
import sys
import os
@@ -38,9 +42,9 @@ django.setup()
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinxcontrib.httpdomain',
'sphinxcontrib.images',
]
# Add any paths that contain templates here, relative to this directory.
@@ -281,3 +285,8 @@ texinfo_documents = [
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
images_config = {
'default_image_width': '250px'
}

View File

@@ -60,7 +60,85 @@ your views::
def admin_view(request, organizer, event):
...
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``.
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``. In case of
event-related views, there is also a signal that allows you to add the view to the event navigation like this::
from django.core.urlresolvers import resolve, reverse
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.control.signals import nav_event
@receiver(nav_event, dispatch_uid='friends_tickets_nav')
def navbar_info(sender, request, **kwargs):
url = resolve(request.path_info)
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers'):
return []
return [{
'label': _('My plugin view'),
'icon': 'heart',
'url': reverse('plugins:myplugin:index', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
}),
'active': url.namespace == 'plugins:myplugin' and url.url_name == 'review',
}]
Event settings view
-------------------
A special case of a control panel view is a view hooked into the event settings page. For this case, there is a
special navigation signal::
@receiver(nav_event_settings, dispatch_uid='friends_tickets_nav_settings')
def navbar_settings(sender, request, **kwargs):
url = resolve(request.path_info)
return [{
'label': _('My settings'),
'url': reverse('plugins:myplugin:settings', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
}),
'active': url.namespace == 'plugins:myplugin' and url.url_name == 'settings',
}]
Also, your view should inherit from ``EventSettingsViewMixin`` and your template from ``pretixcontrol/event/settings_base.html``
for good integration. If you just want to display a form, you could do it like the following::
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
model = Event
permission = 'can_change_settings'
form_class = MySettingsForm
template_name = 'my_plugin/settings.html'
def get_success_url(self, **kwargs):
return reverse('plugins:myplugin:settings', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
With this template::
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %} {% trans "Friends Tickets Settings" %} {% endblock %}
{% block inside %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<fieldset>
<legend>{% trans "Friends Tickets Settings" %}</legend>
{% bootstrap_form form layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
Frontend views
--------------
@@ -68,35 +146,34 @@ Frontend views
Including a custom view into the participant-facing frontend is a little bit different as there is
no path prefix like ``control/``.
First, define your URL in your ``urls.py``, but this time in the ``event_patterns`` section::
First, define your URL in your ``urls.py``, but this time in the ``event_patterns`` section and wrapped by
``event_url``::
from django.conf.urls import url
from pretix.multidomain import event_url
from . import views
event_patterns = [
url(r'^mypluginname/', views.frontend_view, name='frontend'),
event_url(r'^mypluginname/', views.frontend_view, name='frontend'),
]
You can then implement a view as you would normally do, but you need to apply a decorator to your
view if you want pretix's default behavior::
from pretix.presale.utils import event_view
@event_view
def some_event_view(request, *args, **kwargs):
...
This decorator will check the URL arguments for their ``event`` and ``organizer`` parameters and
correctly ensure that:
You can then implement a view as you would normally do. It will be automatically ensured that:
* The requested event exists
* The requested event is activated (can be overridden by decorating with ``@event_view(require_live=False)``)
* The requested event is active (you can disable this check using ``event_url(…, require_live=True)``)
* The event is accessed via the domain it should be accessed
* The ``request.event`` attribute contains the correct ``Event`` object
* The ``request.organizer`` attribute contains the correct ``Organizer`` object
* Your plugin is enabled
* The locale is set correctly
.. versionchanged:: 1.7
The ``event_url()`` wrapper has been added in 1.7 to replace the former ``@event_view`` decorator. The
``event_url()`` wrapper is optional and using ``url()`` still works, but you will not be able to set the
``require_live`` setting any more via the decorator. The ``@event_view`` decorator is now deprecated and
does nothing.
REST API viewsets
-----------------

View File

@@ -19,13 +19,13 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: validate_cart, order_paid, order_placed
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_fee_type_name, allow_ticket_download
Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, contact_form_fields, checkout_confirm_messages
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content
.. automodule:: pretix.presale.signals
@@ -47,20 +47,26 @@ Backend
-------
.. automodule:: pretix.control.signals
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, order_info
.. automodule:: pretix.base.signals
:members: logentry_display, requiredaction_display
:members: logentry_display, logentry_object_link, requiredaction_display
Vouchers
""""""""
.. automodule:: pretix.control.signals
:members: voucher_form_class, voucher_form_html
:members: voucher_form_class, voucher_form_html, voucher_form_validation
Dashboards
""""""""""
.. automodule:: pretix.control.signals
:members: event_dashboard_widgets, user_dashboard_widgets
Ticket designs
""""""""""""""
.. automodule:: pretix.plugins.ticketoutputpdf.signals
:members: layout_text_variables

View File

@@ -10,5 +10,6 @@ Contents:
exporter
ticketoutput
payment
invoice
customview
general

View File

@@ -0,0 +1,95 @@
.. highlight:: python
:linenothreshold: 5
Writing an invoice renderer plugin
==================================
An invoice renderer controls how invoice files are built.
The creation of such a plugin is very similar to creating an export output.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
Output registration
-------------------
The invoice renderer API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available ticket outputs. Your plugin
should listen for this signal and return the subclass of ``pretix.base.invoice.BaseInvoiceRenderer``
that we'll provide in this plugin::
from django.dispatch import receiver
from pretix.base.signals import register_invoice_renderers
@receiver(register_invoice_renderers, dispatch_uid="output_custom")
def register_infoice_renderers(sender, **kwargs):
from .invoice import MyInvoiceRenderer
return MyInvoiceRenderer
The renderer class
------------------
.. class:: pretix.base.invoice.BaseInvoiceRenderer
The central object of each invoice renderer is the subclass of ``BaseInvoiceRenderer``.
.. py:attribute:: BaseInvoiceRenderer.event
The default constructor sets this property to the event we are currently
working for.
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: verbose_name
This is an abstract attribute, you **must** override this!
.. automethod:: generate
Helper class for reportlab-base renderers
-----------------------------------------
All PDF rendering that ships with pretix is based on reportlab. We recommend to read the
`reportlab User Guide`_ to understand all the concepts used here.
If you want to implement a renderer that also uses report lab, this helper class might be
convenient to you:
.. class:: pretix.base.invoice.BaseReportlabInvoiceRenderer
.. py:attribute:: BaseReportlabInvoiceRenderer.pagesize
.. py:attribute:: BaseReportlabInvoiceRenderer.left_margin
.. py:attribute:: BaseReportlabInvoiceRenderer.right_margin
.. py:attribute:: BaseReportlabInvoiceRenderer.top_margin
.. py:attribute:: BaseReportlabInvoiceRenderer.bottom_margin
.. py:attribute:: BaseReportlabInvoiceRenderer.doc_template_class
.. py:attribute:: BaseReportlabInvoiceRenderer.invoice
.. automethod:: _init
.. automethod:: _get_stylesheet
.. automethod:: _register_fonts
.. automethod:: _on_first_page
.. automethod:: _on_other_page
.. automethod:: _get_first_page_frames
.. automethod:: _get_other_page_frames
.. automethod:: _build_doc
.. _reportlab User Guide: https://www.reportlab.com/docs/reportlab-userguide.pdf

View File

@@ -114,6 +114,19 @@ method to make your receivers available::
def ready(self):
from . import signals # NOQA
You can optionally specify code that is executed when your plugin is activated for an event
in the ``installed`` method::
class PaypalApp(AppConfig):
def installed(self, event):
pass # Your code here
Note that ``installed`` will *not* be called if the plugin in indirectly activated for an event
because the event is created with settings copied from another event.
Views
-----

View File

@@ -58,6 +58,8 @@ The output class
.. autoattribute:: is_enabled
.. autoattribute:: multi_download_enabled
.. autoattribute:: settings_form_fields
.. automethod:: settings_content_render

View File

@@ -59,7 +59,7 @@ If an item is assigned to multiple quotas, it can only be bought if *all of them
If multiple items are assigned to the same quota, the quota will be counted as sold out as soon as the
*sum* of the two items exceeds the quota limit.
The availability of a quota is currently calculated by substracting the following numbers from the quota
The availability of a quota is currently calculated by subtracting the following numbers from the quota
limit:
* The number of orders placed for an item that are either already paid or within their granted payment period

View File

@@ -14,7 +14,7 @@ Implementing a task
A common pattern for implementing asynchronous tasks can be seen a lot in ``pretix.base.services``
and looks like this::
from pretix.celery import app
from pretix.celery_app import app
@app.task
def my_task(argument1, argument2):

View File

@@ -2,6 +2,9 @@ Sending Email
=============
pretix allows event organizers to configure how they want to send emails to their users in multiple ways.
Therefore, all emails should be sent through the following function:
Therefore, all emails should be sent through the following function.
If the email you send is related to an order, you should also take a look at the
:py:meth:`~pretix.base.models.Order.send_mail` of the order model.
.. autofunction:: pretix.base.services.mail.mail

View File

@@ -21,7 +21,10 @@ Organizers and events
:members:
.. autoclass:: pretix.base.models.Event
:members:
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, get_cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, active_subevents, invoice_renderer, settings
.. autoclass:: pretix.base.models.SubEvent
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running
.. autoclass:: pretix.base.models.Team
:members:
@@ -29,6 +32,15 @@ Organizers and events
.. autoclass:: pretix.base.models.RequiredAction
:members:
.. autoclass:: pretix.base.models.EventMetaProperty
:members:
.. autoclass:: pretix.base.models.EventMetaValue
:members:
.. autoclass:: pretix.base.models.SubEventMetaValue
:members:
Items
-----
@@ -42,6 +54,15 @@ Items
.. autoclass:: pretix.base.models.ItemVariation
:members:
.. autoclass:: pretix.base.models.SubEventItem
:members:
.. autoclass:: pretix.base.models.SubEventItemVariation
:members:
.. autoclass:: pretix.base.models.ItemAddOn
:members:
.. autoclass:: pretix.base.models.Question
:members:

View File

@@ -10,6 +10,3 @@ Developer documentation
implementation/index
api/index
structure
.. TODO::
Document settings objects, ItemVariation objects, form fields.

View File

@@ -20,7 +20,6 @@ Your should install the following on your system:
* Python 3.4 or newer
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
* ``pyvenv`` for Python 3 (Debian package: ``python3-venv``)
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
* ``libffi`` (Debian package: ``libffi-dev``)
* ``libssl`` (Debian package: ``libssl-dev``)
@@ -37,7 +36,7 @@ Please execute ``python -V`` or ``python3 -V`` to make sure you have Python 3.4
execute ``pip3 -V`` to check. Then use Python's internal tools to create a virtual
environment and activate it for your current session::
pyvenv env
python3 -m venv env
source env/bin/activate
You should now see a ``(env)`` prepended to your shell prompt. You have to do this

View File

@@ -19,10 +19,12 @@ same team. We update them regularly to make them compatible with the latest
pretix releases:
* `SEPA direct debit`_
* `Wirecard payment`_
* `Pages`_
* `Passbook/Wallet ticket output`_
* `Cartshare`_
* `Fontpack Free fonts`_
* `Mailing list subscription`_
The following closed-source plugins are available to customers of the hosted pretix.eu platform.
Please get in touch with the pretix team if you want to have them for your self-hosted
@@ -34,10 +36,12 @@ pretix installation:
* Integration with MailChimp
The following plugins are from independent third-party authors, so we can make
no statements about their stability or compatibility:
no statements about their functionality, security, stability or compatibility:
* `esPass ticket output`_
* `IcePay integration`_
* `Average price chart`_
* `Pay in cash upon arrival`_
.. _SEPA direct debit: https://github.com/pretix/pretix-sepadebit
.. _Passbook/Wallet ticket output: https://github.com/pretix/pretix-passbook
@@ -46,3 +50,7 @@ no statements about their stability or compatibility:
.. _esPass ticket output: https://github.com/esPass/pretix-espass
.. _IcePay integration: https://github.com/chotee/pretix-icepay
.. _Fontpack Free fonts: https://github.com/pretix/pretix-fontpack-free
.. _Wirecard payment: https://github.com/pretix/pretix-wirecard
.. _Mailing list subscription: https://github.com/pretix/pretix-newsletter-ml
.. _Average price chart: https://github.com/rixx/pretix-avgchart
.. _Pay in cash upon arrival: https://github.com/pc-coholic/pretix-cashpayment

View File

@@ -99,6 +99,7 @@ uses to communicate with the pretix server.
"variation": null,
"attendee_name": "Peter Higgs",
"redeemed": false,
"attention": false,
"paid": true
},
...
@@ -107,10 +108,10 @@ uses to communicate with the pretix server.
}
:query query: Search query
:query key: Secret API key
:statuscode 200: Valid request
:statuscode 404: Unknown organizer or event
:statuscode 403: Invalid authorization key
:query key: Secret API key
:statuscode 200: Valid request
:statuscode 404: Unknown organizer or event
:statuscode 403: Invalid authorization key
.. http:get:: /pretixdroid/api/(organizer)/(event)/download/
@@ -140,6 +141,7 @@ uses to communicate with the pretix server.
"variation": null,
"attendee_name": "Peter Higgs",
"redeemed": false,
"attention": false,
"paid": true
},
...

View File

@@ -2,3 +2,4 @@
sphinx
sphinx-rtd-theme
sphinxcontrib-httpdomain
sphinxcontrib-images

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,97 @@
Creating an event
=================
After you have created an organizer account, the next step is to create your event. An event is the basic object in
pretix that everything is organized around. One event corresponds to one ticket shop with all its products, quotas,
orders and settings.
To create an event, you can click the "Create a new event" tile on your dashboard or the button above the list of
events. You will then be presented with the first step of event creation:
.. thumbnail:: ../../screens/event/create_step1.png
:align: center
:class: screenshot
Here, you first need to decide for the organizer the event belongs to. You will not be able to change this
association later. This will determine default settings for the event, as well as access control to the event's
settings.
Second, you need to select the languages that the ticket shop should be available in. You can change this setting
later, but if you select it correctly now, it will automatically ask you for all descriptions in the respective
languages starting from the next step.
Last on this page, you can decide if this event represents an event series. In this cases, the event will turn into
multiple events included in once, meaning that you will get one combined ticket shop for multiple actual events. This
is useful if you have a large number of events that are very similar to each other and that should be sold together
(i.e. users should be able to buy tickets for multiple events at the same time). Those single events can differ in
available products, quotas, prices and some meta information, but most settings need to be the same for all of them.
We recommend to use this feature only if you really know that you need it and if you really run a lot of events, not if
you run e.g. a yearly conference.
Once you set these values, you can procede to the next step:
.. thumbnail:: ../../screens/event/create_step2.png
:align: center
:class: screenshot
In this step, you will be asked more detailled questions about your event. In particular, you can fill in the
following fields:
Name
This is the public name of your event. It should be descriptive and tell both you and the user which event you are
dealing with, but should still be concise. You probably know how your event is named already ;)
Short form
This will be used in multiple places. For example, the URL of your ticket shop will include this short form of
your event name, but it will also be the default prefix e.g. for invoice numbers. We recommend to use some natural
abbreviation of your event name, maybe together with a date, of no more than 10 characters. This is the only value
on this page that can't be changed later.
Event start time
The date and time that your event starts at. You can later configure settings to hide the time, if you don't want
to show that.
Event end time
The date and time your event ends at. You can later configure settings to hide this value completely -- or you can
just leave it empty. It's optional!
Location
This is the location of your event in a human-readable format. We will show this on the ticket shop frontpage, but
it might also be used e.g. in Wallet tickets.
Event currency
This is the currency all prices and payments in your shop will be handled in.
Sales tax rate
If you need to pay a form of sales tax (also known as VAT in many countries) on your products, you can set a tax rate
in percent here that will be used as a default later. After creating your event, you can also create multiple tax
rates or fine-tune the tax settings.
Default language
If you selected multiple supported languages in the previous step, you can now decide which one should be
displayed by default.
Start of presale
If you set this date, no ticket will be sold before this date. We normally recommend not to set this date during
event creation because it will make testing your shop harder.
End of presale
If you set this date, no ticket will be sold after this date.
If all of this is set, you can proceed to the next step. If this is your first event, there will not be a next step
and you are done! If you have already created events before, you will be asked if you want to copy settings from one
of them:
.. thumbnail:: ../../screens/event/create_step3.png
:align: center
:class: screenshot
If you do so, all products, categories, quotas and most settings of the other event will be taken over. You should
still review them if they make sense for your new event, but it could save you a lot of work. After this step, your
event is created successfully:
.. thumbnail:: ../../screens/event/create_step4.png
:align: center
:class: screenshot
You can now fine-tune all settings to your liking, publish your event and start selling tickets!

109
doc/user/events/taxes.rst Normal file
View File

@@ -0,0 +1,109 @@
Tax rules
=========
In most countries, you will be required to pay some form of sales tax for your event tickets. If you don't know about
the exact rules, you should consult a professional tax consultant right now.
To implement those taxes in pretix, you can create one or multiple "tax rules". A tax rule specifies when and at what
rate should be calculated on a product price. Taxes will then be correctly displayed in the product list, order
details and on invoices.
At the time of this writing, every product can be assigned exactly one tax rule. To view and change the tax rules of
your event, go to the respective section in your event's settings:
.. thumbnail:: ../../screens/event/tax_list.png
:align: center
:class: screenshot
On this page, you can create, edit and delete your tax rules. Clicking on the name of a tax rule will take you to its
detailled settings:
.. thumbnail:: ../../screens/event/tax_detail.png
:align: center
:class: screenshot
Here, you can tune the following parameters:
Name
What is the (short) name of this tax? This is probably "VAT" in English and should be very short as it will be
displayed in lots of places.
Rate
This is the tax rate in percent.
The configured product prices include the tax amount
If this setting is enabled (the default), then a product configured to a price of 10.00 EUR will, at a tax rate of
19.00 %, be interpreted as a product with a total gross price of 10.00 EUR including 1.60 EUR taxes, leading to a
net price of 8.40 EUR. If you disable this setting, the price will be interpreted as a net price of 10.00 EUR,
leading to a total price to pay of 11.90 EUR.
Use EU reverse charge taxation rules
This enables reverse charge taxation (see section below).
Merchant country
This is probably your country of residence, but in some cases it could also be the country your event is
located in. This is the place of taxation in the sense of EU reverse charge rules (see section below).
EU reverse charge
-----------------
.. warning:: Everything contained in this section is not legal advice. Please consult a tax consultant
before making decisions. We are not responsible for the correct handling of taxes in your
ticket shop.
"Reverse charge" is a rule in European VAT legislation that specifies how taxes are paid
if you provide goods to a buyer in a different European country than you reside in yourself.
If the buyer is a VAT-paying business in their country, you charge them only the net price without
taxes and state that the buyer is responsible for paying the correct taxes themselves.
.. warning:: We firmly believe that reverse charge rules are **not applicable** for most events handled
with pretix and therefore **strongly recommend not to enable this feature** if you do not have
a specific reason to do so. The reasoning behind this is that according to article 52 of the
`VAT directive`_ (page 17), the place of supply is always the location of your event and
therefore the tax rate of the event country always has to be paid regardless of the location
of the visitor.
If you enable the reverse charge feature and specify your merchant country, then the following process
will be performed during order creation:
* The user will first be presented with the "normal" prices (net or gross, as configured).
* The user adds a product to their cart. The cart will at this point always show gross prices *with*
taxes.
* In the next step, the user can enter an invoice address. Tax will be removed from the price if one of the
following statements is true:
* The invoice address is in a non-EU country.
* The invoice address is a business address in an EU-country different from the merchant country and has a valid VAT ID.
* In the second case, a reverse charge note will be added to the invoice.
VAT IDs are validated against the EUs validation web service. Should that service be unavailable, the user
needs to pay VAT tax and reclaim the taxes at a later point in time with their government.
If you and the buyer are residing in EU countries that use different currencies, the invoice will show
the total and VAT amount also in the local currency of the buyer, if the system was able to obtain a
conversion rate from the European Central Bank's webservice within the last 7 days.
For existing orders, a change of the invoice address will not result in a change of taxes automatically.
You can trigger this manually in the backend by going to the order's detail view. There, first click
the "Check" button next to the VAT ID. Then, go to "Change products" and select the option "Recalculate
taxes" at the end of the page.
.. note:: In the invoicing settings, you should turn the setting "Ask for VAT ID" on for this to work.
.. note:: During back-and-forth modification of taxation status, unfortunately there can be rounding
errors of usually up to one cent from the intended price. This is unavoidable due to the
flexible nature in which prices are being calculated.
Taxation of payment fees
------------------------
In the payment part of your event settings, you can choose the tax rule that needs to be applied for
payment method fees. This works in the same way as product prices, with the small difference that the
"configured product prices include the tax amount" settings is ignored and payment fees will always be
treated as gross values.
.. _VAT directive: http://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32006L0112&from=EN

View File

@@ -1,7 +1,13 @@
User Guide
==========
This section of our documentation is dedicated to show you the way around pretix if you are an event organizer
wanting to use pretix to sell tickets.
.. toctree::
:maxdepth: 2
organizers/index
events/create
events/taxes
payments/index

View File

@@ -0,0 +1,112 @@
Organizer accounts and teams
============================
Organizer account
-----------------
The basis of all your operations within pretix is your organizer account. It represents an entity that is running
events, for example a company, yourself or any other institution.
Every event belongs to one organizer account and events within the same organizer account are assumed to belong together
in some sense, whereas events in different organizer accounts are completely isolated.
If you want to use the hosted pretix service, you can create an organizer account on our `Get started`_ page. Otherwise,
ask your pretix administrator for access to an organizer account.
You can find out all organizer accounts you have access to by going to your global dashboard (click on the pretix logo
in the top-left corner) and then select "Organizers" from the navigation bar on the left side. Then, choose one of the
organizer accounts presented, if there are multiple of them:
.. thumbnail:: ../../screens/organizer/list.png
:align: center
:class: screenshot
This overview shows you all event that belong to the organizer and you have access to:
.. thumbnail:: ../../screens/organizer/event_list.png
:align: center
:class: screenshot
With the "Edit" button at the top, next to the organizer account name, you can modify properties of the organizer
account such as its name and display settings for the public profile page of the organizer account:
.. thumbnail:: ../../screens/organizer/edit.png
:align: center
:class: screenshot
.. tip::
The profile page will be shown as ``https://pretix.eu/slug/`` where ``slug`` is to be replaced by the short form of
the organizer name that you entered during account creation and ``pretix.eu`` is to be replaced by your
installation's domain name if you are not using our hosted service.
Instead, you can also use a custom domain for the profile page and your events, for example
``https://tickets.example.com/`` if ``example.com`` is a domain that you own. In this case, please contact the pretix
hosted support or your system administrator to set up the custom domain.
Teams
-----
We don't expect you to work on your events all by yourself and therefore, pretix comes with ways to invite your fellow
team members to access your pretix organizer account. To manage teams, click on the "Teams" link on your organizer
settings page (see above how to find it). This shows you a list of teams that should contain at least one team already:
.. thumbnail:: ../../screens/organizer/team_list.png
:align: center
:class: screenshot
If you click on a team name, you get to a page that shows you the current members of the team:
.. thumbnail:: ../../screens/organizer/team_detail.png
:align: center
:class: screenshot
You see that there is a list of pretix user accounts (i.e. email addresses), who are part of the team. To add a user to
the team, just enter their email address in the text box next to the "Add" button. If the user already has an account
in the pretix system they will instantly get access to the team. Otherwise, they will be sent an email with an invitation
link that can be used to create an account. This account will then instantly have access to the team. Users can be part
of as many teams as you want.
In the section below, you can also create access tokens for our :ref:`rest-api`. You can read more on this topic in the
section :ref:`rest-auth` of the API documentation.
Next to the team name, you again see a button called "Edit" that allows you to modify the permissions of the team.
Permissions separate into two areas:
* **Organizer permissions** allow actions on the level of an organizer account, in particular:
* Can create events To create a new event under this organizer account, users need to have this permission
* Can change teams and permissions This permission is required to perform the kind of action you are doing right now.
Anyone with this permission can assign arbitrary other permissions to themselves, so this is the most powerful
permission there is to give.
* Can change organizer settings This permission is required to perform changes to the settings of the organizer
account, e.g. its name or display settings.
* **Event permissions** allow actions on the level of an event. You can give the team access to all events of the
organizer (including future ones that are not yet created) or just a selected set of events. The specific permissions to choose from are:
* Can change event settings This permission gives access to most areas of the control panel that are not controlled
by one of the other event permissions, especially those that are related to setting up and configuring the event.
* Can change product settings This permission allows to create and modify products and objects that are closely
related to products, such as product categories, quotas, and questions.
* Can view orders This permission allows viewing the list of orders and allindividual order details, but not
changing anything about it. This also includes the various exports offered.
* Can change orders This permission allows all actions that involve changing an order, such as changing the products
in an order, marking an order as paid or refunden, importing banking data, etc. This only works properly if the
same users also have the "Can view orders" permission.
* Can view vouchers This permission allows viewing the list of vouchers including the voucher codes themselves and
their redemption status.
* Can change vouchers This permission allows to create and modify vouchers in all their details. It only works
properly if the same users also have the "Can view vouchers" permission.
.. thumbnail:: ../../screens/organizer/team_edit.png
:align: center
:class: screenshot
.. _Get started: https://pretix.eu/about/en/setup

View File

@@ -3,7 +3,7 @@
Bank transfer
=============
To accept payments with bank transfer, you only need to fill one important field in pretix' settings: In "Bank
To accept payments with bank transfer, you only need to fill out one important field in pretix' settings: In "Bank
account details" you should specify everything one needs to know to transfer money to you, e.g. your IBAN and BIC,
the name of your bank and for international transfers, preferably also your address and the bank's address.
@@ -17,6 +17,7 @@ The easiest way to import payment data is to download a CSV file from your onlin
export of some sort. You can go to "Import bank data" in pretix to upload a new file:
.. image:: img/bank1.png
:class: screenshot
If you upload a file for the first time, pretix will not know what information is contained in which column as every
bank builds completely different CSV files. Therefore, pretix will ask you for that information. It will show you the

View File

@@ -8,41 +8,49 @@ PayPal account, you can create one on `paypal.com`_.
If you look into pretix' settings, you are required to fill in two keys:
.. image:: img/paypal_pretix.png
:class: screenshot
Unfortunately, it is not straightforward how to get those keys from PayPal's website. In order to do so, you
need to go to `developer.paypal.com`_ to link the account to your pretix event.
Click on "Log In" in the top-right corner and log in with your PayPal account.
.. image:: img/paypal2.png
:class: screenshot
Then, click on "Dashboard" in the top-right corner.
.. image:: img/paypal3.png
:class: screenshot
In the dashboard, scroll down until you see the headline "REST API Apps". Click "Create App".
.. image:: img/paypal4.png
:class: screenshot
Enter any name for the application that helps you to identify it later. Then confirm with "Create App".
.. image:: img/paypal5.png
:class: screenshot
On the next page, before you do anything else, switch the mode on the right to "Live" to get the correct keys.
Then, copy the "Client ID" and the "Secret" and enter them into the appropriate fields in the payment settings in
pretix.
.. image:: img/paypal6.png
:class: screenshot
Finally, we need to create a webhook. The webhook tells PayPal to notify pretix e.g. if a payment gets cancelled so
pretix can cancel the ticket as well. If you have multiple events connected to your PayPal account, you need multiple
webhooks. To create one, scroll a bit down and click "Add Webhook".
.. image:: img/paypal7.png
:class: screenshot
Then, enter the webhook URL that you find on the pretix settings page. It should look similar to the one in the
screenshot but contain your event name. Tick the box "All events" and save.
.. image:: img/paypal8.png
:class: screenshot
That's it, you are ready to go!

View File

@@ -9,6 +9,7 @@ Dashboard. As you can see in the following screenshot, you will be presented wit
and one for live payments. In each set, there is a secret and a publishable keys.
.. image:: img/stripe1.png
:class: screenshot
Choose one of the two sets and copy the two keys to the appropriate fields in pretix' settings. To perform actual
payments, you will need to use the live keys, but you can use the test keys to test the payment flow before you go live.
@@ -21,6 +22,7 @@ that you are currently on. Then, click "Add endpoint" and enter the URL that you
configuration in pretix' settings.
.. image:: img/stripe2.png
:class: screenshot
Again, you can choose between live mode and test mode here.

View File

@@ -47,14 +47,15 @@ question = Question.objects.create(
event=event, question='Age',
type=Question.TYPE_NUMBER, required=False
)
tr19 = event.tax_rules.create(rate=19)
item_ticket = Item.objects.create(
event=event, category=cat_tickets, name='Ticket',
default_price=23, tax_rate=19, admission=True
default_price=23, tax_rule=tr19, admission=True
)
item_ticket.questions.add(question)
item_shirt = Item.objects.create(
event=event, category=cat_merch, name='T-Shirt',
default_price=15, tax_rate=19
default_price=15, tax_rule=tr19
)
var_s = ItemVariation.objects.create(item=item_shirt, value='S')
var_m = ItemVariation.objects.create(item=item_shirt, value='M')

View File

@@ -1 +1 @@
__version__ = "1.5.0"
__version__ = "1.8.1"

View File

@@ -1,3 +1,7 @@
import time
from django.conf import settings
from django.contrib.auth import logout
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.base.models import Event
@@ -13,6 +17,18 @@ class EventPermission(BasePermission):
return True
return False
if request.user.is_authenticated:
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
last_used = request.session.get('pretix_auth_last_used', time.time())
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
logout(request)
request.session['pretix_auth_login_time'] = 0
return False
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
return False
request.session['pretix_auth_last_used'] = int(time.time())
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
else request.user)
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:

View File

@@ -1,10 +1,55 @@
from django_countries.serializers import CountryFieldMixin
from rest_framework.fields import Field
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Event
from pretix.base.models import Event, TaxRule
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
class MetaDataField(Field):
def to_representation(self, value):
return {
v.property.name: v.value for v in value.meta_values.all()
}
class EventSerializer(I18nAwareModelSerializer):
meta_data = MetaDataField(source='*')
class Meta:
model = Event
fields = ('name', 'slug', 'live', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location')
'presale_end', 'location', 'has_subevents', 'meta_data')
class SubEventItemSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItem
fields = ('item', 'price')
class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItemVariation
fields = ('variation', 'price')
class SubEventSerializer(I18nAwareModelSerializer):
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
meta_data = MetaDataField(source='*')
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location',
'item_price_overrides', 'variation_price_overrides', 'meta_data')
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
class Meta:
model = TaxRule
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')

View File

@@ -1,3 +1,5 @@
from decimal import Decimal
from rest_framework import serializers
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -21,17 +23,26 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
'position')
class ItemTaxRateField(serializers.Field):
def to_representation(self, i):
if i.tax_rule:
return str(Decimal(i.tax_rule.rate))
else:
return str(Decimal('0.00'))
class ItemSerializer(I18nAwareModelSerializer):
addons = InlineItemAddOnSerializer(many=True)
variations = InlineItemVariationSerializer(many=True)
tax_rate = ItemTaxRateField(source='*', read_only=True)
class Meta:
model = Item
fields = ('id', 'category', 'name', 'active', 'description',
'default_price', 'free_price', 'tax_rate', 'admission',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel',
'min_per_order', 'max_per_order', 'has_variations',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
'variations', 'addons')
@@ -61,4 +72,4 @@ class QuotaSerializer(I18nAwareModelSerializer):
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations')
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent')

View File

@@ -1,3 +1,5 @@
from decimal import Decimal
from rest_framework import serializers
from rest_framework.reverse import reverse
@@ -6,13 +8,25 @@ from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
QuestionAnswer,
)
from pretix.base.models.orders import OrderFee
from pretix.base.signals import register_ticket_outputs
class CompatibleCountryField(serializers.Field):
def to_representation(self, instance: InvoiceAddress):
if instance.country:
return str(instance.country)
else:
return instance.country_old
class InvoiceAdddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
class Meta:
model = InvoiceAddress
fields = ('last_modified', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id')
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
'vat_id_validated')
class AnswerSerializer(I18nAwareModelSerializer):
@@ -86,25 +100,48 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'checkins', 'downloads', 'answers')
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
'answers', 'tax_rule')
class OrderFeeSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderFee
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
class PaymentFeeLegacyField(serializers.Field):
def __init__(self, *args, **kwargs):
self.attr = kwargs.pop('attribute')
super().__init__(*args, **kwargs)
def to_representation(self, instance: Order):
return str(
sum([getattr(f, self.attr) for f in instance.fees.all() if f.fee_type == OrderFee.FEE_TYPE_PAYMENT],
Decimal('0.00'))
)
class OrderSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAdddressSerializer()
positions = OrderPositionSerializer(many=True)
fees = OrderFeeSerializer(many=True)
downloads = OrderDownloadsField(source='*')
payment_fee = PaymentFeeLegacyField(source='*', attribute='value') # TODO: Remove in 1.9
payment_fee_tax_rate = PaymentFeeLegacyField(source='*', attribute='tax_rate') # TODO: Remove in 1.9
payment_fee_tax_value = PaymentFeeLegacyField(source='*', attribute='tax_value') # TODO: Remove in 1.9
class Meta:
model = Order
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value',
'total', 'comment', 'invoice_address', 'positions', 'downloads')
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value')
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceLine
fields = ('description', 'gross_value', 'tax_value', 'tax_rate')
fields = ('description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
class InvoiceSerializer(I18nAwareModelSerializer):
@@ -114,5 +151,6 @@ class InvoiceSerializer(I18nAwareModelSerializer):
class Meta:
model = Invoice
fields = ('order', 'invoice_no', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines')
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date')

View File

@@ -7,4 +7,4 @@ class VoucherSerializer(I18nAwareModelSerializer):
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment')
'tag', 'comment', 'subevent')

View File

@@ -6,4 +6,4 @@ class WaitingListSerializer(I18nAwareModelSerializer):
class Meta:
model = WaitingListEntry
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale')
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent')

View File

@@ -13,6 +13,7 @@ orga_router = routers.DefaultRouter()
orga_router.register(r'events', event.EventViewSet)
event_router = routers.DefaultRouter()
event_router.register(r'subevents', event.SubEventViewSet)
event_router.register(r'items', item.ItemViewSet)
event_router.register(r'categories', item.ItemCategoryViewSet)
event_router.register(r'questions', item.QuestionViewSet)
@@ -21,6 +22,7 @@ event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.OrderViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'taxrules', event.TaxRuleViewSet)
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
# Force import of all plugins to give them a chance to register URLs with the router

View File

@@ -1,7 +1,11 @@
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import filters, viewsets
from pretix.api.serializers.event import EventSerializer
from pretix.base.models import Event
from pretix.api.serializers.event import (
EventSerializer, SubEventSerializer, TaxRuleSerializer,
)
from pretix.base.models import Event, ItemCategory, TaxRule
from pretix.base.models.event import SubEvent
class EventViewSet(viewsets.ReadOnlyModelViewSet):
@@ -11,4 +15,30 @@ class EventViewSet(viewsets.ReadOnlyModelViewSet):
lookup_url_kwarg = 'event'
def get_queryset(self):
return self.request.organizer.events.all()
return self.request.organizer.events.prefetch_related('meta_values', 'meta_values__property')
class SubEventFilter(FilterSet):
class Meta:
model = SubEvent
fields = ['active']
class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = SubEventSerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filter_class = SubEventFilter
def get_queryset(self):
return self.request.event.subevents.prefetch_related(
'subeventitem_set', 'subeventitemvariation_set'
)
class TaxRuleViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = TaxRuleSerializer
queryset = TaxRule.objects.none()
def get_queryset(self):
return self.request.event.tax_rules.all()

View File

@@ -1,3 +1,5 @@
import django_filters
from django.db.models import Q
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from rest_framework.decorators import detail_route
@@ -12,6 +14,14 @@ from pretix.base.models import Item, ItemCategory, Question, Quota
class ItemFilter(FilterSet):
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
def tax_rate_qs(self, queryset, name, value):
if value in ("0", "None", "0.00"):
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
else:
return queryset.filter(tax_rule__rate=value)
class Meta:
model = Item
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
@@ -26,7 +36,7 @@ class ItemViewSet(viewsets.ReadOnlyModelViewSet):
filter_class = ItemFilter
def get_queryset(self):
return self.request.event.items.prefetch_related('variations', 'addons').all()
return self.request.event.items.select_related('tax_rule').prefetch_related('variations', 'addons').all()
class ItemCategoryFilter(FilterSet):
@@ -58,10 +68,17 @@ class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
return self.request.event.questions.prefetch_related('options').all()
class QuotaFilter(FilterSet):
class Meta:
model = Quota
fields = ['subevent']
class QuotaViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = QuotaSerializer
queryset = Quota.objects.none()
filter_backends = (OrderingFilter,)
filter_backends = (DjangoFilterBackend, OrderingFilter,)
filter_class = QuotaFilter
ordering_fields = ('id', 'size')
ordering = ('id',)

View File

@@ -1,5 +1,6 @@
import django_filters
from django.db.models import Q
from django.db.models.functions import Concat
from django.http import FileResponse
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
@@ -36,7 +37,8 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options'
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
'fees'
).select_related(
'invoice_address'
)
@@ -84,7 +86,7 @@ class OrderPositionFilter(FilterSet):
class Meta:
model = OrderPosition
fields = ['item', 'variation', 'attendee_name', 'secret', 'order', 'order__status', 'has_checkin',
'addon_to']
'addon_to', 'subevent']
class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
@@ -137,12 +139,21 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(name='refers', lookup_expr='invoice_no__iexact')
refers = django_filters.CharFilter(method='refers_qs')
number = django_filters.CharFilter(method='nr_qs')
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')
def refers_qs(self, queryset, name, value):
return queryset.annotate(
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
).filter(refers_nr__iexact=value)
def nr_qs(self, queryset, name, value):
return queryset.filter(nr__iexact=value)
class Meta:
model = Invoice
fields = ['order', 'invoice_no', 'is_cancellation', 'refers', 'locale']
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
class RetryException(APIException):
@@ -155,15 +166,17 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = InvoiceSerializer
queryset = Invoice.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('invoice_no',)
ordering_fields = ('invoice_no', 'date')
ordering = ('nr',)
ordering_fields = ('nr', 'date')
filter_class = InvoiceFilter
lookup_field = 'invoice_no'
lookup_url_kwarg = 'invoice_no'
permission = 'can_view_orders'
lookup_url_kwarg = 'number'
lookup_field = 'nr'
def get_queryset(self):
return self.request.event.invoices.prefetch_related('lines').select_related('order')
return self.request.event.invoices.prefetch_related('lines').select_related('order', 'refers').annotate(
nr=Concat('prefix', 'invoice_no')
)
@detail_route()
def download(self, request, **kwargs):

View File

@@ -16,7 +16,7 @@ class VoucherFilter(FilterSet):
class Meta:
model = Voucher
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
'price_mode', 'value', 'item', 'variation', 'quota', 'tag']
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
def filter_active(self, queryset, name, value):
if value:

View File

@@ -15,7 +15,7 @@ class WaitingListFilter(FilterSet):
class Meta:
model = WaitingListEntry
fields = ['item', 'variation', 'email', 'locale', 'has_voucher']
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
class WaitingListViewSet(viewsets.ReadOnlyModelViewSet):

View File

@@ -1,4 +1,5 @@
from django.apps import AppConfig
from django.conf import settings
class PretixBaseConfig(AppConfig):
@@ -9,13 +10,18 @@ class PretixBaseConfig(AppConfig):
from . import exporter # NOQA
from . import payment # NOQA
from . import exporters # NOQA
from .services import export, mail, tickets, cart, orders, cleanup, update_check # NOQA
from . import invoice # NOQA
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas # NOQA
try:
from .celery_app import app as celery_app # NOQA
except ImportError:
pass
if hasattr(settings, 'RAVEN_CONFIG'):
from ..sentry import initialize
initialize()
default_app_config = 'pretix.base.PretixBaseConfig'
try:

View File

@@ -1,3 +1,4 @@
from .answers import * # noqa
from .invoices import * # noqa
from .json import * # noqa
from .mail import * # noqa

View File

@@ -0,0 +1,61 @@
import os
import tempfile
from collections import OrderedDict
from zipfile import ZipFile
from django import forms
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import QuestionAnswer
from ..exporter import BaseExporter
from ..signals import register_data_exporters
class AnswerFilesExporter(BaseExporter):
identifier = 'answerfiles'
verbose_name = _('Answers to file upload questions')
@property
def export_form_fields(self):
return OrderedDict(
[
('questions',
forms.ModelMultipleChoiceField(
queryset=self.event.questions.filter(type='F'),
label=_('Questions'),
widget=forms.CheckboxSelectMultiple,
required=False
)),
]
)
def render(self, form_data: dict):
qs = QuestionAnswer.objects.filter(
orderposition__order__event=self.event,
).select_related('orderposition', 'orderposition__order', 'question')
if form_data.get('questions'):
qs = qs.filter(question__in=form_data['questions'])
with tempfile.TemporaryDirectory() as d:
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
if i.file:
i.file.open('rb')
fname = '{}-{}-{}-q{}-{}'.format(
self.event.slug.upper(),
i.orderposition.order.code,
i.orderposition.positionid,
i.question.pk,
os.path.basename(i.file.name).split('.', 1)[1]
)
zipf.writestr(fname, i.file.read())
i.file.close()
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return 'answers.zip', 'application/zip', zipf.read()
@receiver(register_data_exporters, dispatch_uid="exporter_answers")
def register_anwers_export(sender, **kwargs):
return AnswerFilesExporter

View File

@@ -21,7 +21,7 @@ class InvoiceExporter(BaseExporter):
if not i.file:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('r')
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
i.file.close()

View File

@@ -1,4 +1,5 @@
import json
from decimal import Decimal
from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver
@@ -32,7 +33,8 @@ class JSONExporter(BaseExporter):
'name': str(item.name),
'category': item.category_id,
'price': item.default_price,
'tax_rate': item.tax_rate,
'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'),
'tax_name': str(item.tax_rule.name) if item.tax_rule else None,
'admission': item.admission,
'active': item.active,
'variations': [
@@ -44,7 +46,7 @@ class JSONExporter(BaseExporter):
'name': str(variation)
} for variation in item.variations.all()
]
} for item in self.event.items.all().prefetch_related('variations')
} for item in self.event.items.select_related('tax_rule').prefetch_related('variations')
],
'questions': [
{
@@ -59,7 +61,13 @@ class JSONExporter(BaseExporter):
'status': order.status,
'user': order.email,
'datetime': order.datetime,
'payment_fee': order.payment_fee,
'fees': [
{
'type': fee.fee_type,
'description': fee.description,
'value': fee.value,
} for fee in order.fees.all()
],
'total': order.total,
'positions': [
{
@@ -80,7 +88,7 @@ class JSONExporter(BaseExporter):
} for position in order.positions.all()
]
} for order in
self.event.orders.all().prefetch_related('positions', 'positions__answers')
self.event.orders.all().prefetch_related('positions', 'positions__answers', 'fees')
],
'quotas': [
{

View File

@@ -1,9 +1,9 @@
import csv
import io
from collections import OrderedDict
from decimal import Decimal
import pytz
from defusedcsv import csv
from django import forms
from django.db.models import Sum
from django.dispatch import receiver
@@ -11,6 +11,7 @@ from django.utils.formats import localize
from django.utils.translation import ugettext as _, ugettext_lazy
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models.orders import OrderFee
from ..exporter import BaseExporter
from ..signals import register_data_exporters
@@ -35,7 +36,10 @@ class OrderListExporter(BaseExporter):
def _get_all_tax_rates(self, qs):
tax_rates = set(
qs.exclude(payment_fee=0).values_list('payment_fee_tax_rate', flat=True).distinct().order_by()
a for a
in OrderFee.objects.filter(
order__event=self.event
).values_list('tax_rate', flat=True).distinct().order_by()
)
tax_rates |= set(
a for a
@@ -59,7 +63,7 @@ class OrderListExporter(BaseExporter):
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Payment date'), _('Payment type'), _('Payment method fee'),
_('Payment date'), _('Payment type'), _('Fees'),
]
for tr in tax_rates:
@@ -78,6 +82,16 @@ class OrderListExporter(BaseExporter):
for k, v in self.event.get_payment_providers().items()
}
full_fee_sum_cache = {
o['order__id']: o['grosssum'] for o in
OrderFee.objects.values('tax_rate', 'order__id').order_by().annotate(grosssum=Sum('value'))
}
fee_sum_cache = {
(o['order__id'], o['tax_rate']): o for o in
OrderFee.objects.values('tax_rate', 'order__id').order_by().annotate(
taxsum=Sum('tax_value'), grosssum=Sum('value')
)
}
sum_cache = {
(o['order__id'], o['tax_rate']): o for o in
OrderPosition.objects.values('tax_rate', 'order__id').order_by().annotate(
@@ -100,7 +114,7 @@ class OrderListExporter(BaseExporter):
order.invoice_address.street,
order.invoice_address.zipcode,
order.invoice_address.city,
order.invoice_address.country,
order.invoice_address.country if order.invoice_address.country else order.invoice_address.country_old,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
@@ -109,19 +123,18 @@ class OrderListExporter(BaseExporter):
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
provider_names.get(order.payment_provider, order.payment_provider),
localize(order.payment_fee)
localize(full_fee_sum_cache.get(order.id) or Decimal('0.00'))
]
for tr in tax_rates:
taxrate_values = sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
if tr == order.payment_fee_tax_rate and order.payment_fee_tax_value:
taxrate_values['grosssum'] += order.payment_fee
taxrate_values['taxsum'] += order.payment_fee_tax_value
fee_taxrate_values = fee_sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
row += [
localize(taxrate_values['grosssum']),
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']),
localize(taxrate_values['taxsum']),
localize(taxrate_values['grosssum'] + fee_taxrate_values['grosssum']),
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']
+ fee_taxrate_values['grosssum'] - fee_taxrate_values['taxsum']),
localize(taxrate_values['taxsum'] + fee_taxrate_values['taxsum']),
]
row.append(', '.join([i.number for i in order.invoices.all()]))

View File

@@ -7,6 +7,7 @@ from django.utils.crypto import get_random_string
from hierarkey.forms import HierarkeyForm
from pretix.base.models import Event
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from .validators import PlaceholderValidator # NOQA
@@ -56,6 +57,9 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
kwargs['locales'] = self.locales
kwargs['initial'] = self.obj.settings.freeze()
super().__init__(*args, **kwargs)
for f in self.fields.values():
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
f.set_event(self.obj)
def get_new_filename(self, name: str) -> str:
nonce = get_random_string(length=8)

View File

@@ -1,4 +1,5 @@
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
@@ -15,6 +16,7 @@ class LoginForm(forms.Form):
"""
email = forms.EmailField(label=_("E-mail"), max_length=254)
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
error_messages = {
'invalid_login': _("Please enter a correct email address and password."),
@@ -29,6 +31,8 @@ class LoginForm(forms.Form):
self.request = request
self.user_cache = None
super().__init__(*args, **kwargs)
if not settings.PRETIX_LONG_SESSIONS:
del self.fields['keep_logged_in']
def clean(self):
email = self.cleaned_data.get('email')
@@ -90,6 +94,12 @@ class RegistrationForm(forms.Form):
}),
required=True
)
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not settings.PRETIX_LONG_SESSIONS:
del self.fields['keep_logged_in']
def clean(self):
password1 = self.cleaned_data.get('password', '')

478
src/pretix/base/invoice.py Normal file
View File

@@ -0,0 +1,478 @@
from collections import defaultdict
from decimal import Decimal
from io import BytesIO
from typing import Tuple
import vat_moss.exchange_rates
from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import pgettext
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import (
BaseDocTemplate, Frame, KeepTogether, NextPageTemplate, PageTemplate,
Paragraph, Spacer, Table, TableStyle,
)
from pretix.base.decimal import round_decimal
from pretix.base.models import Event, Invoice
from pretix.base.signals import register_invoice_renderers
class BaseInvoiceRenderer:
"""
This is the base class for all invoice renderers.
"""
def __init__(self, event: Event):
self.event = event
def __str__(self):
return self.identifier
def generate(self, invoice: Invoice) -> Tuple[str, str, str]:
"""
This method should generate the invoice file and return a tuple consisting of a
filename, a file type and file content. The extension will be taken from the filename
which is otherwise ignored.
"""
raise NotImplementedError()
@property
def verbose_name(self) -> str:
"""
A human-readable name for this renderer. This should be short but
self-explanatory. Good examples include 'German DIN 5008' or 'Italian invoice'.
"""
raise NotImplementedError() # NOQA
@property
def identifier(self) -> str:
"""
A short and unique identifier for this renderer.
This should only contain lowercase letters and in most
cases will be the same as your package name.
"""
raise NotImplementedError() # NOQA
class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
This is a convenience class to avoid duplicate code when implementing invoice renderers
that are based on reportlab.
"""
pagesize = pagesizes.A4
left_margin = 25 * mm
right_margin = 20 * mm
top_margin = 20 * mm
bottom_margin = 15 * mm
doc_template_class = BaseDocTemplate
def _init(self):
"""
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
"""
self.stylesheet = self._get_stylesheet()
self._register_fonts()
def _get_stylesheet(self):
"""
Get a stylesheet. By default, this contains the "Normal" and "Heading1" styles.
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName='OpenSans', fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='Heading1', fontName='OpenSansBd', fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName='OpenSansBd', fontSize=8, leading=12))
stylesheet.add(ParagraphStyle(name='Fineprint', fontName='OpenSans', fontSize=8, leading=10))
return stylesheet
def _register_fonts(self):
"""
Register fonts with reportlab. By default, this registers the OpenSans font family
"""
pdfmetrics.registerFont(TTFont('OpenSans', finders.find('fonts/OpenSans-Regular.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansBI', finders.find('fonts/OpenSans-BoldItalic.ttf')))
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
def _on_other_page(self, canvas: Canvas, doc):
"""
Called when a new page is rendered that is *not* the first page.
"""
pass
def _on_first_page(self, canvas: Canvas, doc):
"""
Called when a new page is rendered that is the first page.
"""
pass
def _get_story(self, doc):
"""
Called to create the story to be inserted into the main frames.
"""
raise NotImplementedError()
def _get_first_page_frames(self, doc):
"""
Called to create a list of frames for the first page.
"""
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 75 * mm,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0,
id='normal')
]
def _get_other_page_frames(self, doc):
"""
Called to create a list of frames for the other pages.
"""
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0,
id='normal')
]
def _build_doc(self, fhandle):
"""
Build a PDF document in a given file handle
"""
self._init()
doc = self.doc_template_class(fhandle, pagesize=self.pagesize,
leftMargin=self.left_margin, rightMargin=self.right_margin,
topMargin=self.top_margin, bottomMargin=self.bottom_margin)
doc.addPageTemplates([
PageTemplate(
id='FirstPage',
frames=self._get_first_page_frames(doc),
onPage=self._on_first_page,
pagesize=self.pagesize
),
PageTemplate(
id='OtherPages',
frames=self._get_other_page_frames(doc),
onPage=self._on_other_page,
pagesize=self.pagesize
)
])
story = self._get_story(doc)
doc.build(story)
return doc
def generate(self, invoice: Invoice):
self.invoice = invoice
buffer = BytesIO()
self._build_doc(buffer)
buffer.seek(0)
return 'invoice.pdf', 'application/pdf', buffer.read()
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
identifier = 'classic'
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
def _on_other_page(self, canvas: Canvas, doc):
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(self.pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d") % (doc.page,))
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
canvas.restoreState()
def _on_first_page(self, canvas: Canvas, doc):
canvas.setCreator('pretix.eu')
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=self.invoice.number))
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(self.pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d") % (doc.page,))
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
canvas.drawText(textobject)
p = Paragraph(self.invoice.invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 70 * mm, 50 * mm)
p_size = p.wrap(70 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
canvas.drawText(textobject)
p = Paragraph(self.invoice.invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 85 * mm, 50 * mm)
p_size = p.wrap(85 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Order code').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(self.invoice.order.full_code)
canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
if self.invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(self.invoice.number)
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Original invoice').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(self.invoice.refers.number)
else:
textobject.textLine(pgettext('invoice', 'Invoice number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(self.invoice.number)
textobject.moveCursor(0, 5)
if self.invoice.is_cancellation:
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Cancellation date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Original invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
else:
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
canvas.drawText(textobject)
if self.invoice.event.settings.invoice_logo_image:
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
canvas.drawImage(ImageReader(logo_file),
95 * mm, (297 - 38) * mm,
width=25 * mm, height=25 * mm,
preserveAspectRatio=True, anchor='n',
mask='auto')
if self.invoice.event.settings.show_date_to:
p_str = (
str(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
from_date=self.invoice.event.get_date_from_display(),
to_date=self.invoice.event.get_date_to_display())
)
else:
p_str = (
str(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
)
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 65 * mm, 50 * mm)
p_size = p.wrap(65 * mm, 50 * mm)
p.drawOn(canvas, 125 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Event').upper())
canvas.drawText(textobject)
canvas.restoreState()
def _get_first_page_frames(self, doc):
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 75 * mm,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
def _get_other_page_frames(self, doc):
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
def _get_story(self, doc):
story = [
NextPageTemplate('FirstPage'),
Paragraph(pgettext('invoice', 'Invoice')
if not self.invoice.is_cancellation
else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1']),
Spacer(1, 5 * mm),
NextPageTemplate('OtherPages'),
]
if self.invoice.introductory_text:
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
story.append(Spacer(1, 10 * mm))
taxvalue_map = defaultdict(Decimal)
grossvalue_map = defaultdict(Decimal)
tstyledata = [
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
]
tdata = [(
pgettext('invoice', 'Description'),
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net'),
pgettext('invoice', 'Gross'),
)]
total = Decimal('0.00')
for line in self.invoice.lines.all():
tdata.append((
Paragraph(line.description, self.stylesheet['Normal']),
localize(line.tax_rate) + " %",
localize(line.net_value) + " " + self.invoice.event.currency,
localize(line.gross_value) + " " + self.invoice.event.currency,
))
taxvalue_map[line.tax_rate, line.tax_name] += line.tax_value
grossvalue_map[line.tax_rate, line.tax_name] += line.gross_value
total += line.gross_value
tdata.append([
pgettext('invoice', 'Invoice total'), '', '', localize(total) + " " + self.invoice.event.currency
])
colwidths = [a * doc.width for a in (.55, .15, .15, .15)]
table = Table(tdata, colWidths=colwidths, repeatRows=1)
table.setStyle(TableStyle(tstyledata))
story.append(table)
story.append(Spacer(1, 15 * mm))
if self.invoice.payment_provider_text:
story.append(Paragraph(self.invoice.payment_provider_text, self.stylesheet['Normal']))
if self.invoice.additional_text:
story.append(Paragraph(self.invoice.additional_text, self.stylesheet['Normal']))
story.append(Spacer(1, 15 * mm))
tstyledata = [
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
('FONTSIZE', (0, 0), (-1, -1), 8),
('FONTNAME', (0, 0), (-1, -1), 'OpenSans'),
]
thead = [
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net value'),
pgettext('invoice', 'Gross value'),
pgettext('invoice', 'Tax'),
''
]
tdata = [thead]
for idx, gross in grossvalue_map.items():
rate, name = idx
if rate == 0:
continue
tax = taxvalue_map[idx]
tdata.append([
localize(rate) + " % " + name,
localize(gross - tax) + " " + self.invoice.event.currency,
localize(gross) + " " + self.invoice.event.currency,
localize(tax) + " " + self.invoice.event.currency,
''
])
def fmt(val):
try:
return vat_moss.exchange_rates.format(val, self.invoice.foreign_currency_display)
except ValueError:
return localize(val) + ' ' + self.invoice.foreign_currency_display
if len(tdata) > 1:
colwidths = [a * doc.width for a in (.25, .15, .15, .15, .3)]
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
table.setStyle(TableStyle(tstyledata))
story.append(KeepTogether([
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table
]))
if self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
tdata = [thead]
for idx, gross in grossvalue_map.items():
rate, name = idx
if rate == 0:
continue
tax = taxvalue_map[idx]
gross = round_decimal(gross * self.invoice.foreign_currency_rate)
tax = round_decimal(tax * self.invoice.foreign_currency_rate)
net = gross - tax
tdata.append([
localize(rate) + " % " + name,
fmt(net), fmt(gross), fmt(tax), ''
])
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
table.setStyle(TableStyle(tstyledata))
story.append(KeepTogether([
Spacer(1, height=2 * mm),
Paragraph(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the European Central Bank on '
'{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
self.stylesheet['Fineprint']
),
Spacer(1, height=3 * mm),
table
]))
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
story.append(Spacer(1, 5 * mm))
story.append(Paragraph(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the European Central Bank on '
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
total=fmt(total)),
self.stylesheet['Fineprint']
))
return story
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
def recv_classic(sender, **kwargs):
return ClassicInvoiceRenderer

View File

@@ -7,6 +7,7 @@ from django.core.urlresolvers import get_script_prefix
from django.http import HttpRequest, HttpResponse
from django.utils import timezone, translation
from django.utils.cache import patch_vary_headers
from django.utils.crypto import get_random_string
from django.utils.deprecation import MiddlewareMixin
from django.utils.translation import LANGUAGE_SESSION_KEY
from django.utils.translation.trans_real import (
@@ -132,8 +133,8 @@ def get_language_from_request(request: HttpRequest) -> str:
return (
get_language_from_user_settings(request)
or get_language_from_session_or_cookie(request)
or get_language_from_event(request)
or get_language_from_browser(request)
or get_language_from_event(request)
or get_default_language()
)
@@ -165,6 +166,9 @@ class SecurityMiddleware(MiddlewareMixin):
'/api/v1/docs/',
)
def process_request(self, request):
request.csp_nonce = get_random_string(length=32)
def process_response(self, request, resp):
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
@@ -179,20 +183,25 @@ class SecurityMiddleware(MiddlewareMixin):
# frame-src is deprecated but kept for compatibility with CSP 1.0 browsers, e.g. Safari 9
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'style-src': ["{static}"],
'connect-src': ["{dynamic}", "https://checkout.stripe.com"],
'img-src': ["{static}", "data:", "https://*.stripe.com"],
'style-src': ["{static}", "{media}", "'nonce-{nonce}'"],
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
'font-src': ["{static}"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
# single-sign-on this can be nearly anything so we cannot really restrict
# this. However, we'll restrict it to HTTPS.
'form-action': ["{dynamic}", "https:"],
'report-uri': ["/csp_report/"],
}
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
staticdomain = "'self'"
dynamicdomain = "'self'"
mediadomain = "'self'"
if settings.MEDIA_URL.startswith('http'):
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
if settings.STATIC_URL.startswith('http'):
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
if settings.SITE_URL.startswith('http'):
@@ -211,6 +220,14 @@ class SecurityMiddleware(MiddlewareMixin):
domain = '%s:%d' % (domain, siteurlsplit.port)
dynamicdomain += " " + domain
if request.path not in self.CSP_EXEMPT:
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain)
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
media=mediadomain, nonce=request.csp_nonce)
for k, v in h.items():
h[k] = ' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain,
nonce=request.csp_nonce).split(' ')
resp['Content-Security-Policy'] = _render_csp(h)
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp

View File

@@ -0,0 +1,573 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-30 17:47
from __future__ import unicode_literals
import django.core.validators
import django.db.migrations.operations.special
import django.db.models.deletion
import django_countries.fields
import i18nfield.fields
from django.conf import settings
from django.core.cache import cache
from django.db import migrations, models
import pretix.base.models.base
import pretix.base.models.event
import pretix.base.models.invoices
import pretix.base.models.orders
import pretix.base.models.organizer
import pretix.base.validators
def create_teams(apps, schema_editor):
Organizer = apps.get_model('pretixbase', 'Organizer')
Team = apps.get_model('pretixbase', 'Team')
for o in Organizer.objects.prefetch_related('events'):
for e in o.events.all():
teams = {}
for p in e.user_perms.all():
pkey = (p.can_change_settings, p.can_change_items, p.can_view_orders,
p.can_change_permissions, p.can_change_orders, p.can_view_vouchers,
p.can_change_vouchers)
if pkey not in teams:
team = Team()
team.can_change_event_settings = p.can_change_settings
team.can_change_items = p.can_change_items
team.can_view_orders = p.can_view_orders
team.can_change_orders = p.can_change_orders
team.can_view_vouchers = p.can_view_vouchers
team.can_change_vouchers = p.can_change_vouchers
team.organizer = o
team.name = '{} Team {}'.format(
str(e.name), len(teams) + 1
)
team.save()
team.limit_events.add(e)
teams[pkey] = team
if p.user:
teams[pkey].members.add(p.user)
else:
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
teams = {}
for p in o.user_perms.all():
pkey = (p.can_create_events, p.can_change_permissions)
if pkey not in teams:
team = Team()
team.can_change_organizer_settings = True
team.can_create_events = p.can_create_events
team.can_change_teams = p.can_change_permissions
team.organizer = o
team.name = '{} Team {}'.format(
str(o.name), len(teams) + 1
)
team.save()
teams[pkey] = team
if p.user:
teams[pkey].members.add(p.user)
else:
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
def rename_placeholder(app, schema_editor):
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
for setting in EventSettingsStore.objects.all():
if setting.key == 'mail_text_order_placed':
new_value = setting.value.replace('{paymentinfo}', '{payment_info}')
setting.value = new_value
cache.delete('hierarkey_{}_{}'.format('event', setting.object_id))
setting.save()
def fwd69(app, schema_editor):
Event = app.get_model('pretixbase', 'Event')
for e in Event.objects.select_related('organizer').all():
e.invoices.all().update(prefix=e.slug.upper() + '-', organizer=e.organizer)
def fwd70(app, schema_editor):
InvoiceAddress = app.get_model('pretixbase', 'InvoiceAddress')
for ia in InvoiceAddress.objects.all():
if ia.company or ia.vat_id:
ia.is_business = True
ia.save()
class Migration(migrations.Migration):
replaces = [('pretixbase', '0052_team_teaminvite'), ('pretixbase', '0058_auto_20170429_1020'),
('pretixbase', '0059_checkin_nonce'), ('pretixbase', '0060_auto_20170510_1027'),
('pretixbase', '0061_auto_20170521_0942'), ('pretixbase', '0062_auto_20170602_0948'),
('pretixbase', '0063_auto_20170702_1711'), ('pretixbase', '0064_auto_20170703_0912'),
('pretixbase', '0065_auto_20170707_0920'), ('pretixbase', '0066_auto_20170708_2102'),
('pretixbase', '0067_auto_20170712_1610'), ('pretixbase', '0068_subevent_frontpage_text'),
('pretixbase', '0069_invoice_prefix'), ('pretixbase', '0070_auto_20170719_0910')]
dependencies = [
('pretixbase', '0051_auto_20170206_2027_squashed_0057_auto_20170501_2116'),
]
operations = [
migrations.CreateModel(
name='Team',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=190, verbose_name='Team name')),
('all_events',
models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
('can_create_events', models.BooleanField(default=False, verbose_name='Can create events')),
('can_change_teams', models.BooleanField(default=False, verbose_name='Can change permissions')),
('can_change_organizer_settings',
models.BooleanField(default=False, verbose_name='Can change organizer settings')),
('can_change_event_settings',
models.BooleanField(default=False, verbose_name='Can change event settings')),
('can_change_items', models.BooleanField(default=False, verbose_name='Can change product settings')),
('can_view_orders', models.BooleanField(default=False, verbose_name='Can view orders')),
('can_change_orders', models.BooleanField(default=False, verbose_name='Can change orders')),
('can_view_vouchers', models.BooleanField(default=False, verbose_name='Can view vouchers')),
('can_change_vouchers', models.BooleanField(default=False, verbose_name='Can change vouchers')),
('limit_events', models.ManyToManyField(to='pretixbase.Event', verbose_name='Limit to events')),
('members', models.ManyToManyField(related_name='teams', to=settings.AUTH_USER_MODEL,
verbose_name='Team members')),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams',
to='pretixbase.Organizer')),
],
options={
'verbose_name_plural': 'Teams',
'verbose_name': 'Team',
},
),
migrations.CreateModel(
name='TeamInvite',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('token',
models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64,
null=True)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites',
to='pretixbase.Team')),
],
),
migrations.RunPython(
code=create_teams,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.RemoveField(
model_name='eventpermission',
name='event',
),
migrations.RemoveField(
model_name='eventpermission',
name='user',
),
migrations.RemoveField(
model_name='organizerpermission',
name='organizer',
),
migrations.RemoveField(
model_name='organizerpermission',
name='user',
),
migrations.RemoveField(
model_name='event',
name='permitted',
),
migrations.RemoveField(
model_name='organizer',
name='permitted',
),
migrations.AlterField(
model_name='team',
name='can_change_teams',
field=models.BooleanField(default=False, verbose_name='Can change teams and permissions'),
),
migrations.AlterField(
model_name='team',
name='limit_events',
field=models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events'),
),
migrations.DeleteModel(
name='EventPermission',
),
migrations.DeleteModel(
name='OrganizerPermission',
),
migrations.AddField(
model_name='checkin',
name='nonce',
field=models.CharField(blank=True, max_length=190, null=True),
),
migrations.AddField(
model_name='event',
name='date_admission',
field=models.DateTimeField(blank=True, null=True, verbose_name='Admission time'),
),
migrations.AlterField(
model_name='event',
name='location',
field=i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location'),
),
migrations.RunPython(
code=rename_placeholder,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.CreateModel(
name='TeamAPIToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=190)),
('active', models.BooleanField(default=True)),
('token', models.CharField(default=pretix.base.models.organizer.generate_api_token, max_length=64)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens',
to='pretixbase.Team')),
],
),
migrations.AlterModelOptions(
name='voucher',
options={'ordering': ('code',), 'verbose_name': 'Voucher', 'verbose_name_plural': 'Vouchers'},
),
migrations.AddField(
model_name='cartposition',
name='meta_info',
field=models.TextField(blank=True, null=True, verbose_name='Meta information'),
),
migrations.AddField(
model_name='orderposition',
name='meta_info',
field=models.TextField(blank=True, null=True, verbose_name='Meta information'),
),
migrations.AlterField(
model_name='event',
name='currency',
field=models.CharField(choices=[('AED', 'AED - UAE Dirham'), ('AFN', 'AFN - Afghani'), ('ALL', 'ALL - Lek'),
('AMD', 'AMD - Armenian Dram'),
('ANG', 'ANG - Netherlands Antillean Guilder'),
('AOA', 'AOA - Kwanza'), ('ARS', 'ARS - Argentine Peso'),
('AUD', 'AUD - Australian Dollar'), ('AWG', 'AWG - Aruban Florin'),
('AZN', 'AZN - Azerbaijanian Manat'), ('BAM', 'BAM - Convertible Mark'),
('BBD', 'BBD - Barbados Dollar'), ('BDT', 'BDT - Taka'),
('BGN', 'BGN - Bulgarian Lev'), ('BHD', 'BHD - Bahraini Dinar'),
('BIF', 'BIF - Burundi Franc'), ('BMD', 'BMD - Bermudian Dollar'),
('BND', 'BND - Brunei Dollar'), ('BOB', 'BOB - Boliviano'),
('BRL', 'BRL - Brazilian Real'), ('BSD', 'BSD - Bahamian Dollar'),
('BTN', 'BTN - Ngultrum'), ('BWP', 'BWP - Pula'),
('BYN', 'BYN - Belarusian Ruble'), ('BZD', 'BZD - Belize Dollar'),
('CAD', 'CAD - Canadian Dollar'), ('CDF', 'CDF - Congolese Franc'),
('CHF', 'CHF - Swiss Franc'), ('CLP', 'CLP - Chilean Peso'),
('CNY', 'CNY - Yuan Renminbi'), ('COP', 'COP - Colombian Peso'),
('CRC', 'CRC - Costa Rican Colon'), ('CUC', 'CUC - Peso Convertible'),
('CUP', 'CUP - Cuban Peso'), ('CVE', 'CVE - Cabo Verde Escudo'),
('CZK', 'CZK - Czech Koruna'), ('DJF', 'DJF - Djibouti Franc'),
('DKK', 'DKK - Danish Krone'), ('DOP', 'DOP - Dominican Peso'),
('DZD', 'DZD - Algerian Dinar'), ('EGP', 'EGP - Egyptian Pound'),
('ERN', 'ERN - Nakfa'), ('ETB', 'ETB - Ethiopian Birr'),
('EUR', 'EUR - Euro'),
('FJD', 'FJD - Fiji Dollar'), ('FKP', 'FKP - Falkland Islands Pound'),
('GBP', 'GBP - Pound Sterling'), ('GEL', 'GEL - Lari'),
('GHS', 'GHS - Ghana Cedi'), ('GIP', 'GIP - Gibraltar Pound'),
('GMD', 'GMD - Dalasi'), ('GNF', 'GNF - Guinea Franc'),
('GTQ', 'GTQ - Quetzal'), ('GYD', 'GYD - Guyana Dollar'),
('HKD', 'HKD - Hong Kong Dollar'), ('HNL', 'HNL - Lempira'),
('HRK', 'HRK - Kuna'), ('HTG', 'HTG - Gourde'), ('HUF', 'HUF - Forint'),
('IDR', 'IDR - Rupiah'), ('ILS', 'ILS - New Israeli Sheqel'),
('INR', 'INR - Indian Rupee'), ('IQD', 'IQD - Iraqi Dinar'),
('IRR', 'IRR - Iranian Rial'), ('ISK', 'ISK - Iceland Krona'),
('JMD', 'JMD - Jamaican Dollar'), ('JOD', 'JOD - Jordanian Dinar'),
('JPY', 'JPY - Yen'), ('KES', 'KES - Kenyan Shilling'),
('KGS', 'KGS - Som'),
('KHR', 'KHR - Riel'), ('KMF', 'KMF - Comoro Franc'),
('KPW', 'KPW - North Korean Won'), ('KRW', 'KRW - Won'),
('KWD', 'KWD - Kuwaiti Dinar'), ('KYD', 'KYD - Cayman Islands Dollar'),
('KZT', 'KZT - Tenge'), ('LAK', 'LAK - Kip'),
('LBP', 'LBP - Lebanese Pound'),
('LKR', 'LKR - Sri Lanka Rupee'), ('LRD', 'LRD - Liberian Dollar'),
('LSL', 'LSL - Loti'), ('LYD', 'LYD - Libyan Dinar'),
('MAD', 'MAD - Moroccan Dirham'), ('MDL', 'MDL - Moldovan Leu'),
('MGA', 'MGA - Malagasy Ariary'), ('MKD', 'MKD - Denar'),
('MMK', 'MMK - Kyat'),
('MNT', 'MNT - Tugrik'), ('MOP', 'MOP - Pataca'), ('MRO', 'MRO - Ouguiya'),
('MUR', 'MUR - Mauritius Rupee'), ('MVR', 'MVR - Rufiyaa'),
('MWK', 'MWK - Malawi Kwacha'), ('MXN', 'MXN - Mexican Peso'),
('MYR', 'MYR - Malaysian Ringgit'), ('MZN', 'MZN - Mozambique Metical'),
('NAD', 'NAD - Namibia Dollar'), ('NGN', 'NGN - Naira'),
('NIO', 'NIO - Cordoba Oro'), ('NOK', 'NOK - Norwegian Krone'),
('NPR', 'NPR - Nepalese Rupee'), ('NZD', 'NZD - New Zealand Dollar'),
('OMR', 'OMR - Rial Omani'), ('PAB', 'PAB - Balboa'), ('PEN', 'PEN - Sol'),
('PGK', 'PGK - Kina'), ('PHP', 'PHP - Philippine Peso'),
('PKR', 'PKR - Pakistan Rupee'), ('PLN', 'PLN - Zloty'),
('PYG', 'PYG - Guarani'), ('QAR', 'QAR - Qatari Rial'),
('RON', 'RON - Romanian Leu'), ('RSD', 'RSD - Serbian Dinar'),
('RUB', 'RUB - Russian Ruble'), ('RWF', 'RWF - Rwanda Franc'),
('SAR', 'SAR - Saudi Riyal'), ('SBD', 'SBD - Solomon Islands Dollar'),
('SCR', 'SCR - Seychelles Rupee'), ('SDG', 'SDG - Sudanese Pound'),
('SEK', 'SEK - Swedish Krona'), ('SGD', 'SGD - Singapore Dollar'),
('SHP', 'SHP - Saint Helena Pound'), ('SLL', 'SLL - Leone'),
('SOS', 'SOS - Somali Shilling'), ('SRD', 'SRD - Surinam Dollar'),
('SSP', 'SSP - South Sudanese Pound'), ('STD', 'STD - Dobra'),
('SVC', 'SVC - El Salvador Colon'), ('SYP', 'SYP - Syrian Pound'),
('SZL', 'SZL - Lilangeni'), ('THB', 'THB - Baht'), ('TJS', 'TJS - Somoni'),
('TMT', 'TMT - Turkmenistan New Manat'), ('TND', 'TND - Tunisian Dinar'),
('TOP', 'TOP - Paanga'), ('TRY', 'TRY - Turkish Lira'),
('TTD', 'TTD - Trinidad and Tobago Dollar'),
('TWD', 'TWD - New Taiwan Dollar'),
('TZS', 'TZS - Tanzanian Shilling'), ('UAH', 'UAH - Hryvnia'),
('UGX', 'UGX - Uganda Shilling'), ('USD', 'USD - US Dollar'),
('UYU', 'UYU - Peso Uruguayo'), ('UZS', 'UZS - Uzbekistan Sum'),
('VEF', 'VEF - Bolívar'), ('VND', 'VND - Dong'), ('VUV', 'VUV - Vatu'),
('WST', 'WST - Tala'), ('XAF', 'XAF - CFA Franc BEAC'),
('XAG', 'XAG - Silver'),
('XAU', 'XAU - Gold'),
('XBA', 'XBA - Bond Markets Unit European Composite Unit (EURCO)'),
('XBB', 'XBB - Bond Markets Unit European Monetary Unit (E.M.U.-6)'),
('XBC', 'XBC - Bond Markets Unit European Unit of Account 9 (E.U.A.-9)'),
('XBD', 'XBD - Bond Markets Unit European Unit of Account 17 (E.U.A.-17)'),
('XCD', 'XCD - East Caribbean Dollar'),
('XDR', 'XDR - SDR (Special Drawing Right)'),
('XOF', 'XOF - CFA Franc BCEAO'),
('XPD', 'XPD - Palladium'), ('XPF', 'XPF - CFP Franc'),
('XPT', 'XPT - Platinum'), ('XSU', 'XSU - Sucre'),
('XTS', 'XTS - Codes specifically reserved for testing purposes'),
('XUA', 'XUA - ADB Unit of Account'), ('XXX',
'XXX - The codes assigned for transactions where no currency is involved'),
('YER', 'YER - Yemeni Rial'), ('ZAR', 'ZAR - Rand'),
('ZMW', 'ZMW - Zambian Kwacha'), ('ZWL', 'ZWL - Zimbabwe Dollar')],
default='EUR', max_length=10, verbose_name='Default currency'),
),
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(
help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='invoice',
name='date',
field=models.DateField(default=pretix.base.models.invoices.today),
),
migrations.AlterField(
model_name='item',
name='allow_cancel',
field=models.BooleanField(default=True,
help_text='If this is active and the general event settings allow it, orders containing this product can be canceled by the user until the order is paid for. Users cannot cancel paid orders on their own and you can cancel orders at all times, regardless of this setting',
verbose_name='Allow product to be canceled'),
),
migrations.AddField(
model_name='questionanswer',
name='file',
field=models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.answerfile_name),
),
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(
choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'),
('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload')],
max_length=5, verbose_name='Question type'),
),
migrations.AddField(
model_name='event',
name='comment',
field=models.TextField(blank=True, null=True, verbose_name='Internal comment'),
),
migrations.AlterField(
model_name='event',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.',
null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='event',
name='presale_start',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.',
null=True, verbose_name='Start of presale'),
),
migrations.CreateModel(
name='SubEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=False,
help_text='Only with this checkbox enabled, this sub-event is visible in the frontend to users.',
verbose_name='Active')),
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
('date_admission', models.DateTimeField(blank=True, null=True, verbose_name='Admission time')),
('presale_end',
models.DateTimeField(blank=True, help_text='No products will be sold after this date.', null=True,
verbose_name='End of presale')),
('presale_start',
models.DateTimeField(blank=True, help_text='No products will be sold before this date.', null=True,
verbose_name='Start of presale')),
(
'location',
i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location')),
],
options={
'verbose_name': 'Sub-Event',
'verbose_name_plural': 'Sub-Events',
'ordering': ('date_from', 'name'),
},
bases=(pretix.base.models.event.EventMixin, models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='SubEventItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Item')),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
],
),
migrations.CreateModel(
name='SubEventItemVariation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
(
'variation',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.ItemVariation')),
],
),
migrations.AddField(
model_name='event',
name='has_subevents',
field=models.BooleanField(default=False, verbose_name='Event series'),
),
migrations.AddField(
model_name='subevent',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subevents',
to='pretixbase.Event'),
),
migrations.AddField(
model_name='subevent',
name='items',
field=models.ManyToManyField(through='pretixbase.SubEventItem', to='pretixbase.Item'),
),
migrations.AddField(
model_name='subevent',
name='variations',
field=models.ManyToManyField(through='pretixbase.SubEventItemVariation', to='pretixbase.ItemVariation'),
),
migrations.AddField(
model_name='cartposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AddField(
model_name='orderposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AddField(
model_name='quota',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='quotas', to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AddField(
model_name='voucher',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AddField(
model_name='waitinglistentry',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterModelOptions(
name='subevent',
options={'ordering': ('date_from', 'name'), 'verbose_name': 'Date in event series',
'verbose_name_plural': 'Dates in event series'},
),
migrations.AddField(
model_name='itemaddon',
name='price_included',
field=models.BooleanField(default=False,
help_text='If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost money individually.',
verbose_name='Add-Ons are included in the price'),
),
migrations.AlterField(
model_name='subevent',
name='active',
field=models.BooleanField(default=False,
help_text='Only with this checkbox enabled, this date is visible in the frontend to users.',
verbose_name='Active'),
),
migrations.AlterField(
model_name='subevent',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.',
null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='subevent',
name='presale_start',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.',
null=True, verbose_name='Start of presale'),
),
migrations.AddField(
model_name='subevent',
name='frontpage_text',
field=i18nfield.fields.I18nTextField(blank=True, null=True, verbose_name='Frontpage text'),
),
migrations.AddField(
model_name='invoice',
name='prefix',
field=models.CharField(db_index=True, default='', max_length=160),
preserve_default=False,
),
migrations.AddField(
model_name='invoice',
name='organizer',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='invoices',
to='pretixbase.Organizer'),
preserve_default=False,
),
migrations.RunPython(
code=fwd69, reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterUniqueTogether(
name='invoice',
unique_together=set([('organizer', 'prefix', 'invoice_no')]),
),
migrations.AlterField(
model_name='invoice',
name='organizer',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='invoices',
to='pretixbase.Organizer'),
),
migrations.RenameField(
model_name='invoiceaddress',
old_name='country',
new_name='country_old',
),
migrations.AddField(
model_name='invoiceaddress',
name='country',
field=django_countries.fields.CountryField(default='', max_length=2, verbose_name='Country'),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceaddress',
name='is_business',
field=models.BooleanField(default=False, verbose_name='Business customer'),
),
migrations.RunPython(
code=fwd70, reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-03 09:12
from __future__ import unicode_literals
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0063_auto_20170702_1711'),
]
operations = [
migrations.AddField(
model_name='questionanswer',
name='file',
field=models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.answerfile_name),
),
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'), ('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload')], max_length=5, verbose_name='Question type'),
),
]

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-07 09:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0064_auto_20170703_0912'),
]
operations = [
migrations.AddField(
model_name='event',
name='comment',
field=models.TextField(blank=True, null=True, verbose_name='Internal comment'),
),
migrations.AlterField(
model_name='event',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.', null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='event',
name='presale_start',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.', null=True, verbose_name='Start of presale'),
),
]

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-08 21:02
from __future__ import unicode_literals
import django.db.models.deletion
import i18nfield.fields
from django.db import migrations, models
import pretix.base.models.base
import pretix.base.models.event
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0065_auto_20170707_0920'),
]
operations = [
migrations.CreateModel(
name='SubEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=False, help_text='Only with this checkbox enabled, this sub-event is visible in the frontend to users.', verbose_name='Active')),
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
('date_admission', models.DateTimeField(blank=True, null=True, verbose_name='Admission time')),
('presale_end', models.DateTimeField(blank=True, help_text='No products will be sold after this date.', null=True, verbose_name='End of presale')),
('presale_start', models.DateTimeField(blank=True, help_text='No products will be sold before this date.', null=True, verbose_name='Start of presale')),
('location', i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location')),
],
options={
'verbose_name': 'Sub-Event',
'verbose_name_plural': 'Sub-Events',
'ordering': ('date_from', 'name'),
},
bases=(pretix.base.models.event.EventMixin, models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='SubEventItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Item')),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
],
),
migrations.CreateModel(
name='SubEventItemVariation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
('variation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.ItemVariation')),
],
),
migrations.AddField(
model_name='event',
name='has_subevents',
field=models.BooleanField(default=False, help_text='Only recommended for advanced users. If this feature is enabled, this will not only be a single event but a series of very similar events that are handled within a single shop. The single events inside the series can only differ in prices and quotas, not in other settings, and buying tickets across multiple of these events at the same time is possible.', verbose_name='Event series'),
),
migrations.AddField(
model_name='subevent',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subevents', to='pretixbase.Event'),
),
migrations.AddField(
model_name='subevent',
name='items',
field=models.ManyToManyField(through='pretixbase.SubEventItem', to='pretixbase.Item'),
),
migrations.AddField(
model_name='subevent',
name='variations',
field=models.ManyToManyField(through='pretixbase.SubEventItemVariation', to='pretixbase.ItemVariation'),
),
migrations.AddField(
model_name='cartposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
migrations.AddField(
model_name='orderposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
migrations.AddField(
model_name='quota',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
migrations.AddField(
model_name='voucher',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
migrations.AddField(
model_name='waitinglistentry',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
]

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-12 16:10
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0066_auto_20170708_2102'),
]
operations = [
migrations.AlterModelOptions(
name='subevent',
options={'ordering': ('date_from', 'name'), 'verbose_name': 'Date in event series', 'verbose_name_plural': 'Dates in event series'},
),
migrations.AddField(
model_name='itemaddon',
name='price_included',
field=models.BooleanField(default=False, help_text='If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost money individually.', verbose_name='Add-Ons are included in the price'),
),
migrations.AlterField(
model_name='cartposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterField(
model_name='event',
name='has_subevents',
field=models.BooleanField(default=False, verbose_name='Event series'),
),
migrations.AlterField(
model_name='orderposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterField(
model_name='quota',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterField(
model_name='subevent',
name='active',
field=models.BooleanField(default=False, help_text='Only with this checkbox enabled, this date is visible in the frontend to users.', verbose_name='Active'),
),
migrations.AlterField(
model_name='subevent',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.', null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='subevent',
name='presale_start',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.', null=True, verbose_name='Start of presale'),
),
migrations.AlterField(
model_name='voucher',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterField(
model_name='waitinglistentry',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-14 16:11
from __future__ import unicode_literals
import i18nfield.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0067_auto_20170712_1610'),
]
operations = [
migrations.AddField(
model_name='subevent',
name='frontpage_text',
field=i18nfield.fields.I18nTextField(blank=True, null=True, verbose_name='Frontpage text'),
),
]

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-17 19:29
from __future__ import unicode_literals
from django.db import migrations, models
from django.db.models import deletion
def fwd(app, schema_editor):
Event = app.get_model('pretixbase', 'Event')
for e in Event.objects.select_related('organizer').all():
e.invoices.all().update(prefix=e.slug.upper() + '-', organizer=e.organizer)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0068_subevent_frontpage_text'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='prefix',
field=models.CharField(db_index=True, default='', max_length=160),
preserve_default=False,
),
migrations.AddField(
model_name='invoice',
name='organizer',
field=models.ForeignKey(null=True,
on_delete=deletion.PROTECT,
related_name='invoices', to='pretixbase.Organizer'),
preserve_default=False,
),
migrations.RunPython(
fwd, migrations.RunPython.noop
),
migrations.AlterUniqueTogether(
name='invoice',
unique_together=set([('organizer', 'prefix', 'invoice_no')]),
),
migrations.AlterField(
model_name='invoice',
name='organizer',
field=models.ForeignKey(on_delete=deletion.PROTECT, related_name='invoices', to='pretixbase.Organizer'),
),
]

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-19 09:10
from __future__ import unicode_literals
import django_countries
from django.db import migrations, models
def fwd(app, schema_editor):
InvoiceAddress = app.get_model('pretixbase', 'InvoiceAddress')
for ia in InvoiceAddress.objects.all():
if ia.company or ia.vat_id:
ia.is_business = True
ia.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0069_invoice_prefix'),
]
operations = [
migrations.RenameField(
model_name='invoiceaddress',
old_name='country',
new_name='country_old',
),
migrations.AddField(
model_name='invoiceaddress',
name='country',
field=django_countries.fields.CountryField(default='', max_length=2, verbose_name='Country'),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceaddress',
name='is_business',
field=models.BooleanField(default=False, verbose_name='Business customer'),
),
migrations.RunPython(
fwd, migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-29 16:16
from __future__ import unicode_literals
import i18nfield.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0070_auto_20170719_0910'),
]
operations = [
migrations.AddField(
model_name='question',
name='help_text',
field=i18nfield.fields.I18nTextField(blank=True, help_text='If the question needs to be explained or clarified, do it here!', null=True, verbose_name='Help text'),
),
migrations.AlterField(
model_name='invoiceaddress',
name='vat_id',
field=models.CharField(blank=True, help_text='Only for business customers within the EU.', max_length=255, verbose_name='VAT ID'),
),
]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-04 13:42
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0071_auto_20170729_1616'),
]
operations = [
migrations.AddField(
model_name='order',
name='download_reminder_sent',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-16 13:33
from __future__ import unicode_literals
import django.db.models.deletion
import django_countries.fields
import i18nfield.fields
from django.core.cache import cache
from django.db import migrations, models
from i18nfield.strings import LazyI18nString
def tax_rate_converter(app, schema_editor):
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
Item = app.get_model('pretixbase', 'Item')
TaxRule = app.get_model('pretixbase', 'TaxRule')
Order = app.get_model('pretixbase', 'Order')
OrderPosition = app.get_model('pretixbase', 'OrderPosition')
InvoiceLine = app.get_model('pretixbase', 'InvoiceLine')
n = LazyI18nString({
'en': 'VAT',
'de': 'MwSt.',
'de-informal': 'MwSt.'
})
for i in Item.objects.select_related('event').exclude(tax_rate=0):
try:
i.tax_rule = i.event.tax_rules.get(rate=i.tax_rate)
except TaxRule.DoesNotExist:
tr = i.event.tax_rules.create(rate=i.tax_rate, name=n)
i.tax_rule = tr
i.save()
for o in Order.objects.select_related('event').exclude(payment_fee_tax_rate=0):
try:
o.payment_fee_tax_rule = o.event.tax_rules.get(rate=o.payment_fee_tax_rate)
except TaxRule.DoesNotExist:
tr = o.event.tax_rules.create(rate=o.payment_fee_tax_rate, name=n)
o.tax_rule = tr
o.save()
for op in OrderPosition.objects.select_related('order', 'order__event').exclude(tax_rate=0):
try:
op.tax_rule = op.order.event.tax_rules.get(rate=op.tax_rate)
except TaxRule.DoesNotExist:
tr = op.order.event.tax_rules.create(rate=op.tax_rate, name=n)
op.tax_rule = tr
op.save()
for il in InvoiceLine.objects.select_related('invoice', 'invoice__event').exclude(tax_rate=0):
try:
il.tax_name = il.invoice.event.tax_rules.get(rate=op.tax_rate).name
except TaxRule.DoesNotExist:
tr = il.invoice.event.tax_rules.create(rate=op.tax_rate, name=n)
il.tax_name = tr.name
il.save()
for setting in EventSettingsStore.objects.filter(key='tax_rate_default'):
try:
tr = setting.object.tax_rules.get(rate=setting.value)
except TaxRule.DoesNotExist:
tr = setting.object.tax_rules.create(rate=setting.value, name=n)
setting.value = tr.pk
setting.save()
cache.delete('hierarkey_{}_{}'.format('event', setting.object.pk))
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0072_order_download_reminder_sent'),
]
operations = [
migrations.CreateModel(
name='TaxRule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', i18nfield.fields.I18nCharField(help_text='Should be short, e.g. "VAT"', max_length=190,
verbose_name='Name')),
('rate', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tax rate')),
('price_includes_tax', models.BooleanField(default=True,
verbose_name='The configured product prices includes the '
'tax amount')),
('eu_reverse_charge',
models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be '
'qualified for reverse charge since the place of '
'taxation is the location of the event. This option '
'only enables reverse charge for business customers who '
'entered a valid EU VAT ID. Only enable this option '
'after consulting a tax counsel. No warranty given for '
'correct tax calculation.',
verbose_name='Use EU reverse charge taxation')),
('home_country', models.CharField(blank=True,
choices=[('AT', 'Austria'), ('BE', 'Belgium'), ('BG', 'Bulgaria'),
('HR', 'Croatia'), ('CY', 'Cyprus'),
('CZ', 'Czech Republic'), ('DK', 'Denmark'),
('EE', 'Estonia'), ('FI', 'Finland'), ('FR', 'France'),
('DE', 'Germany'), ('GR', 'Greece'), ('HU', 'Hungary'),
('IE', 'Ireland'), ('IT', 'Italy'), ('LV', 'Latvia'),
('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MT', 'Malta'),
('NL', 'Netherlands'), ('PL', 'Poland'), ('PT', 'Portugal'),
('RO', 'Romania'), ('SK', 'Slovakia'), ('SI', 'Slovenia'),
('ES', 'Spain'), ('SE', 'Sweden'), ('UJ', 'United Kingdom')],
help_text='Your country. Only relevant for EU reverse charge.',
max_length=2, verbose_name='Merchant country')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tax_rules',
to='pretixbase.Event')),
],
),
migrations.AddField(
model_name='item',
name='tax_rule',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to='pretixbase.TaxRule', verbose_name='Sales tax'),
),
migrations.AddField(
model_name='order',
name='payment_fee_tax_rule',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to='pretixbase.TaxRule'),
),
migrations.AddField(
model_name='orderposition',
name='tax_rule',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to='pretixbase.TaxRule'),
),
migrations.RunPython(
tax_rate_converter, migrations.RunPython.noop
),
migrations.RemoveField(
model_name='item',
name='tax_rate',
),
migrations.AddField(
model_name='invoiceaddress',
name='vat_id_validated',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='invoiceaddress',
name='vat_id',
field=models.CharField(blank=True, help_text='Only for business customers within the EU.', max_length=255, verbose_name='VAT ID'),
),
migrations.AlterField(
model_name='taxrule',
name='home_country',
field=django_countries.fields.CountryField(blank=True, help_text='Your country of residence. This is the country the EU reverse charge rule will not apply in, if configured above.', max_length=2, verbose_name='Merchant country'),
),
migrations.AddField(
model_name='cartposition',
name='includes_tax',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='invoiceline',
name='tax_name',
field=models.CharField(default='', max_length=190),
preserve_default=False,
),
migrations.AlterField(
model_name='taxrule',
name='eu_reverse_charge',
field=models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be qualified for reverse charge since the place of taxation is the location of the event. This option disables charging VAT for all customers outside the EU and for business customers in different EU countries that do not customers who entered a valid EU VAT ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax calculation. USE AT YOUR OWN RISK.', verbose_name='Use EU reverse charge taxation rules'),
),
migrations.AddField(
model_name='invoice',
name='foreign_currency_display',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name='invoice',
name='foreign_currency_rate',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
),
migrations.AddField(
model_name='invoice',
name='foreign_currency_rate_date',
field=models.DateField(blank=True, null=True),
),
]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-28 09:01
from __future__ import unicode_literals
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.base
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0074_auto_20170825_1258'),
]
operations = [
migrations.CreateModel(
name='EventMetaProperty',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, help_text='Can not contain spaces or special characters execpt underscores', max_length=50, validators=[django.core.validators.RegexValidator(message='The property name may only contain letters, numbers and underscores.', regex='^[a-zA-Z0-9_]+$')], verbose_name='Name')),
('default', models.TextField()),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_properties', to='pretixbase.Organizer')),
],
options={
'abstract': False,
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='EventMetaValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField()),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.Event')),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_values', to='pretixbase.EventMetaProperty')),
],
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='SubEventMetaValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField()),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subevent_values', to='pretixbase.EventMetaProperty')),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.SubEvent')),
],
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.AlterUniqueTogether(
name='subeventmetavalue',
unique_together=set([('subevent', 'property')]),
),
migrations.AlterUniqueTogether(
name='eventmetavalue',
unique_together=set([('event', 'property')]),
),
]

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-28 14:35
from __future__ import unicode_literals
from decimal import Decimal
import django.db.models.deletion
from django.db import migrations, models
def fee_converter(app, schema_editor):
OrderFee = app.get_model('pretixbase', 'OrderFee')
Order = app.get_model('pretixbase', 'Order')
of = []
for o in Order.objects.exclude(payment_fee=Decimal('0.00')).iterator():
of.append(OrderFee(
order=o,
value=o.payment_fee,
fee_type='payment',
tax_rate=o.payment_fee_tax_rate,
tax_rule=o.payment_fee_tax_rule,
tax_value=o.payment_fee_tax_value,
internal_type=o.payment_provider
))
if len(of) > 900:
OrderFee.objects.bulk_create(of)
of = []
OrderFee.objects.bulk_create(of)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0075_auto_20170828_0901'),
]
operations = [
migrations.CreateModel(
name='OrderFee',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Value')),
('description', models.CharField(blank=True, max_length=190)),
('internal_type', models.CharField(blank=True, max_length=255)),
('fee_type', models.CharField(choices=[('payment', 'Payment method fee'), ('shipping', 'Shipping fee')], max_length=100)),
('tax_rate', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='Tax rate')),
('tax_value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tax value')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='fees', to='pretixbase.Order', verbose_name='Order')),
('tax_rule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.TaxRule')),
],
),
migrations.RunPython(
fee_converter, migrations.RunPython.noop
),
migrations.RemoveField(
model_name='order',
name='payment_fee',
),
migrations.RemoveField(
model_name='order',
name='payment_fee_tax_rate',
),
migrations.RemoveField(
model_name='order',
name='payment_fee_tax_rule',
),
migrations.RemoveField(
model_name='order',
name='payment_fee_tax_value',
),
]

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 11:26
from __future__ import unicode_literals
from django.db import migrations, models
def assign_positions(app, schema_editor):
Invoice = app.get_model('pretixbase', 'Invoice')
for i in Invoice.objects.iterator():
for j, l in enumerate(i.lines.all()):
l.position = j
l.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0076_orderfee'),
]
operations = [
migrations.AddField(
model_name='invoiceline',
name='position',
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name='orderfee',
name='fee_type',
field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('other', 'Other fees')], max_length=100),
),
migrations.RunPython(
assign_positions, migrations.RunPython.noop
),
migrations.AlterModelOptions(
name='invoiceline',
options={'ordering': ('position', 'pk')},
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-10-03 16:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0077_auto_20170829_1126'),
]
operations = [
migrations.AddField(
model_name='quota',
name='cached_availability_number',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='quota',
name='cached_availability_state',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='quota',
name='cached_availability_time',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='eventmetaproperty',
name='default',
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name='taxrule',
name='eu_reverse_charge',
field=models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be qualified for reverse charge since the place of taxation is the location of the event. This option disables charging VAT for all customers outside the EU and for business customers in different EU countries who entered a valid EU VAT ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax calculation. USE AT YOUR OWN RISK.', verbose_name='Use EU reverse charge taxation rules'),
),
]

View File

@@ -3,13 +3,13 @@ from .auth import U2FDevice, User
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin
from .event import (
Event, Event_SettingsStore, EventLock, RequiredAction,
Event, Event_SettingsStore, EventLock, RequiredAction, SubEvent,
generate_invite_token,
)
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota, itempicture_upload_to,
Quota, SubEventItem, SubEventItemVariation, itempicture_upload_to,
)
from .log import LogEntry
from .orders import (
@@ -19,5 +19,6 @@ from .orders import (
generate_secret,
)
from .organizer import Organizer, Organizer_SettingsStore, Team, TeamInvite
from .tax import TaxRule
from .vouchers import Voucher
from .waitinglist import WaitingListEntry

View File

@@ -6,7 +6,8 @@ from django.db import models
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.utils.crypto import get_random_string
from i18nfield.utils import I18nJSONEncoder
from pretix.helpers.json import CustomJSONEncoder
def cachedfile_name(instance, filename: str) -> str:
@@ -16,7 +17,7 @@ def cachedfile_name(instance, filename: str) -> str:
class CachedFile(models.Model):
"""
A cached file (e.g. pre-generated ticket PDF)
An uploaded file, with an optional expiry date.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
expires = models.DateTimeField(null=True, blank=True)
@@ -54,7 +55,7 @@ class LoggingMixin:
event = self.event
l = LogEntry(content_object=self, user=user, action_type=action, event=event)
if data:
l.data = json.dumps(data, cls=I18nJSONEncoder)
l.data = json.dumps(data, cls=CustomJSONEncoder)
l.save()

View File

@@ -1,6 +1,7 @@
import string
import uuid
from datetime import date, datetime, time
from collections import OrderedDict
from datetime import datetime, time
import pytz
from django.conf import settings
@@ -9,122 +10,26 @@ from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Q
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.email import CustomSMTPBackend
from pretix.base.models.base import LoggedModel
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.validators import EventSlugBlacklistValidator
from pretix.helpers.daterange import daterange
from pretix.helpers.json import safe_string
from ..settings import settings_hierarkey
from .organizer import Organizer
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
class Event(LoggedModel):
"""
This model represents an event. An event is anything you can buy
tickets for.
:param organizer: The organizer this event belongs to
:type organizer: Organizer
:param name: This event's full title
:type name: str
:param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
be unique among the events of the same organizer.
:type slug: str
:param live: Whether or not the shop is publicly accessible
:type live: bool
:param currency: The currency of all prices and payments of this event
:type currency: str
:param date_from: The datetime this event starts
:type date_from: datetime
:param date_to: The datetime this event ends
:type date_to: datetime
:param presale_start: No tickets will be sold before this date.
:type presale_start: datetime
:param presale_end: No tickets will be sold after this date.
:type presale_end: datetime
:param location: venue
:type location: str
:param plugins: A comma-separated list of plugin names that are active for this
event.
:type plugins: str
"""
settings_namespace = 'event'
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
name = I18nCharField(
max_length=200,
verbose_name=_("Name"),
)
slug = models.SlugField(
max_length=50, db_index=True,
help_text=_(
"Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
"We recommend some kind of abbreviation or a date with less than 10 characters that can be easily "
"remembered, but you can also choose to use a random value. "
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
validators=[
RegexValidator(
regex="^[a-zA-Z0-9.-]+$",
message=_("The slug may only contain letters, numbers, dots and dashes."),
),
EventSlugBlacklistValidator()
],
verbose_name=_("Short form"),
)
live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
currency = models.CharField(max_length=10,
verbose_name=_("Default currency"),
choices=CURRENCY_CHOICES,
default=settings.DEFAULT_CURRENCY)
date_from = models.DateTimeField(verbose_name=_("Event start time"))
date_to = models.DateTimeField(null=True, blank=True,
verbose_name=_("Event end time"))
date_admission = models.DateTimeField(null=True, blank=True,
verbose_name=_("Admission time"))
is_public = models.BooleanField(default=False,
verbose_name=_("Visible in public lists"),
help_text=_("If selected, this event may show up on the ticket system's start page "
"or an organization profile."))
presale_end = models.DateTimeField(
null=True, blank=True,
verbose_name=_("End of presale"),
help_text=_("No products will be sold after this date."),
)
presale_start = models.DateTimeField(
null=True, blank=True,
verbose_name=_("Start of presale"),
help_text=_("No products will be sold before this date."),
)
location = I18nTextField(
null=True, blank=True,
max_length=200,
verbose_name=_("Location"),
)
plugins = models.TextField(
null=True, blank=True,
verbose_name=_("Plugins"),
)
class Meta:
verbose_name = _("Event")
verbose_name_plural = _("Events")
ordering = ("date_from", "name")
def __str__(self):
return str(self.name)
def save(self, *args, **kwargs):
obj = super().save(*args, **kwargs)
self.get_cache().clear()
return obj
class EventMixin:
def clean(self):
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
@@ -133,13 +38,30 @@ class Event(LoggedModel):
raise ValidationError({'date_to': _('The end of the event has to be later than its start.')})
super().clean()
def get_plugins(self) -> "list[str]":
def get_short_date_from_display(self, tz=None, show_times=True) -> str:
"""
Returns the names of the plugins activated for this event as a list.
Returns a shorter formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting.
"""
if self.plugins is None:
return []
return self.plugins.split(",")
tz = tz or pytz.timezone(self.settings.timezone)
return _date(
self.date_from.astimezone(tz),
"SHORT_DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
)
def get_short_date_to_display(self, tz=None) -> str:
"""
Returns a shorter formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting. Returns an empty string
if ``show_date_to`` is ``False``.
"""
tz = tz or pytz.timezone(self.settings.timezone)
if not self.settings.show_date_to or not self.date_to:
return ""
return _date(
self.date_to.astimezone(tz),
"SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
)
def get_date_from_display(self, tz=None, show_times=True) -> str:
"""
@@ -177,11 +99,182 @@ class Event(LoggedModel):
)
def get_date_range_display(self, tz=None) -> str:
"""
Returns a formatted string containing the start date and the event date
of the event with respect to the current locale and to the ``show_times`` and
``show_date_to`` settings.
"""
tz = tz or pytz.timezone(self.settings.timezone)
if not self.settings.show_date_to or not self.date_to:
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz))
@property
def presale_has_ended(self):
"""
Is true, when ``presale_end`` is set and in the past.
"""
if self.presale_end and now() > self.presale_end:
return True
return False
@property
def presale_is_running(self):
"""
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
set or in the past.
"""
if self.presale_start and now() < self.presale_start:
return False
if self.presale_end and now() > self.presale_end:
return False
return True
@property
def event_microdata(self):
import json
eventdict = {
"@context": "http://schema.org",
"@type": "Event", "location": {
"@type": "Place",
"address": str(self.location)
},
"name": str(self.name)
}
if self.settings.show_times:
eventdict["startDate"] = self.date_from.isoformat()
if self.settings.show_date_to and self.date_to is not None:
eventdict["endDate"] = self.date_to.isoformat()
else:
eventdict["startDate"] = self.date_from.date().isoformat()
if self.settings.show_date_to and self.date_to is not None:
eventdict["endDate"] = self.date_to.date().isoformat()
return safe_string(json.dumps(eventdict))
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
class Event(EventMixin, LoggedModel):
"""
This model represents an event. An event is anything you can buy
tickets for.
:param organizer: The organizer this event belongs to
:type organizer: Organizer
:param name: This event's full title
:type name: str
:param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
be unique among the events of the same organizer.
:type slug: str
:param live: Whether or not the shop is publicly accessible
:type live: bool
:param currency: The currency of all prices and payments of this event
:type currency: str
:param date_from: The datetime this event starts
:type date_from: datetime
:param date_to: The datetime this event ends
:type date_to: datetime
:param presale_start: No tickets will be sold before this date.
:type presale_start: datetime
:param presale_end: No tickets will be sold after this date.
:type presale_end: datetime
:param location: venue
:type location: str
:param plugins: A comma-separated list of plugin names that are active for this
event.
:type plugins: str
:param has_subevents: Enable event series functionality
:type has_subevents: bool
"""
settings_namespace = 'event'
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
name = I18nCharField(
max_length=200,
verbose_name=_("Event name"),
)
slug = models.SlugField(
max_length=50, db_index=True,
help_text=_(
"Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
"We recommend some kind of abbreviation or a date with less than 10 characters that can be easily "
"remembered, but you can also choose to use a random value. "
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
validators=[
RegexValidator(
regex="^[a-zA-Z0-9.-]+$",
message=_("The slug may only contain letters, numbers, dots and dashes."),
),
EventSlugBlacklistValidator()
],
verbose_name=_("Short form"),
)
live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
currency = models.CharField(max_length=10,
verbose_name=_("Event currency"),
choices=CURRENCY_CHOICES,
default=settings.DEFAULT_CURRENCY)
date_from = models.DateTimeField(verbose_name=_("Event start time"))
date_to = models.DateTimeField(null=True, blank=True,
verbose_name=_("Event end time"))
date_admission = models.DateTimeField(null=True, blank=True,
verbose_name=_("Admission time"))
is_public = models.BooleanField(default=False,
verbose_name=_("Visible in public lists"),
help_text=_("If selected, this event may show up on the ticket system's start page "
"or an organization profile."))
presale_end = models.DateTimeField(
null=True, blank=True,
verbose_name=_("End of presale"),
help_text=_("Optional. No products will be sold after this date."),
)
presale_start = models.DateTimeField(
null=True, blank=True,
verbose_name=_("Start of presale"),
help_text=_("Optional. No products will be sold before this date."),
)
location = I18nTextField(
null=True, blank=True,
max_length=200,
verbose_name=_("Location"),
)
plugins = models.TextField(
null=True, blank=True,
verbose_name=_("Plugins"),
)
comment = models.TextField(
verbose_name=_("Internal comment"),
null=True, blank=True
)
has_subevents = models.BooleanField(
verbose_name=_('Event series'),
default=False
)
class Meta:
verbose_name = _("Event")
verbose_name_plural = _("Events")
ordering = ("date_from", "name")
def __str__(self):
return str(self.name)
def save(self, *args, **kwargs):
obj = super().save(*args, **kwargs)
self.get_cache().clear()
return obj
def get_plugins(self) -> "list[str]":
"""
Returns the names of the plugins activated for this event as a list.
"""
if self.plugins is None:
return []
return self.plugins.split(",")
def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
"""
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
@@ -193,20 +286,6 @@ class Event(LoggedModel):
return ObjectRelatedCache(self)
@property
def presale_has_ended(self):
if self.presale_end and now() > self.presale_end:
return True
return False
@property
def presale_is_running(self):
if self.presale_start and now() < self.presale_start:
return False
if self.presale_end and now() > self.presale_end:
return False
return True
def lock(self):
"""
Returns a contextmanager that can be used to lock an event for bookings.
@@ -216,6 +295,10 @@ class Event(LoggedModel):
return locking.LockManager(self)
def get_mail_backend(self, force_custom=False):
"""
Returns an email server connection, either by using the system-wide connection
or by returning a custom one based on the event's settings.
"""
if self.settings.smtp_use_custom or force_custom:
return CustomSMTPBackend(host=self.settings.smtp_host,
port=self.settings.smtp_port,
@@ -229,9 +312,12 @@ class Event(LoggedModel):
@property
def payment_term_last(self):
"""
The last datetime of payments for this event.
"""
tz = pytz.timezone(self.settings.timezone)
return make_aware(datetime.combine(
self.settings.get('payment_term_last', as_type=date),
self.settings.get('payment_term_last', as_type=RelativeDateWrapper).datetime(self).date(),
time(hour=23, minute=59, second=59)
), tz)
@@ -240,8 +326,16 @@ class Event(LoggedModel):
from ..signals import event_copy_data
self.plugins = other.plugins
self.is_public = other.is_public
self.save()
tax_map = {}
for t in other.tax_rules.all():
tax_map[t.pk] = t
t.pk = None
t.event = self
t.save()
category_map = {}
for c in ItemCategory.objects.filter(event=other):
category_map[c.pk] = c
@@ -260,6 +354,8 @@ class Event(LoggedModel):
i.picture.save(i.picture.name, i.picture)
if i.category_id:
i.category = category_map[i.category_id]
if i.tax_rule_id:
i.tax_rule = tax_map[i.tax_rule_id]
i.save()
for v in vars:
variation_map[v.pk] = v
@@ -273,7 +369,7 @@ class Event(LoggedModel):
ia.addon_category = category_map[ia.addon_category.pk]
ia.save()
for q in Quota.objects.filter(event=other).prefetch_related('items', 'variations'):
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
items = list(q.items.all())
vars = list(q.variations.all())
q.pk = None
@@ -309,11 +405,25 @@ class Event(LoggedModel):
)
newname = default_storage.save(fname, fi)
s.value = 'file://' + newname
s.save()
s.save()
elif s.key == 'tax_rate_default':
try:
if int(s.value) in tax_map:
s.value = tax_map.get(int(s.value)).pk
s.save()
else:
s.delete()
except ValueError:
s.delete()
else:
s.save()
event_copy_data.send(sender=self, other=other)
def get_payment_providers(self) -> dict:
"""
Returns a dictionary of initialized payment providers mapped by their identifiers.
"""
from ..signals import register_payment_providers
responses = register_payment_providers.send(self)
@@ -324,7 +434,149 @@ class Event(LoggedModel):
for p in response:
pp = p(self)
providers[pp.identifier] = pp
return providers
return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
def get_invoice_renderers(self) -> dict:
"""
Returns a dictionary of initialized invoice renderers mapped by their identifiers.
"""
from ..signals import register_invoice_renderers
responses = register_invoice_renderers.send(self)
renderers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
renderers[pp.identifier] = pp
return renderers
@property
def invoice_renderer(self):
"""
Returns the currently configured invoice renderer.
"""
irs = self.get_invoice_renderers()
return irs[self.settings.invoice_renderer]
@property
def active_subevents(self):
"""
Returns a queryset of active subevents.
"""
return self.subevents.filter(active=True).order_by('-date_from', 'name')
@property
def active_future_subevents(self):
return self.subevents.filter(
Q(active=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(date_to__gte=now())
)
).order_by('date_from', 'name')
@property
def meta_data(self):
data = {p.name: p.default for p in self.organizer.meta_properties.all()}
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
return data
class SubEvent(EventMixin, LoggedModel):
"""
This model represents a date within an event series.
:param event: The event this belongs to
:type event: Event
:param active: Whether to show the subevent
:type active: bool
:param name: This event's full title
:type name: str
:param date_from: The datetime this event starts
:type date_from: datetime
:param date_to: The datetime this event ends
:type date_to: datetime
:param presale_start: No tickets will be sold before this date.
:type presale_start: datetime
:param presale_end: No tickets will be sold after this date.
:type presale_end: datetime
:param location: venue
:type location: str
"""
event = models.ForeignKey(Event, related_name="subevents", on_delete=models.PROTECT)
active = models.BooleanField(default=False, verbose_name=_("Active"),
help_text=_("Only with this checkbox enabled, this date is visible in the "
"frontend to users."))
name = I18nCharField(
max_length=200,
verbose_name=_("Name"),
)
date_from = models.DateTimeField(verbose_name=_("Event start time"))
date_to = models.DateTimeField(null=True, blank=True,
verbose_name=_("Event end time"))
date_admission = models.DateTimeField(null=True, blank=True,
verbose_name=_("Admission time"))
presale_end = models.DateTimeField(
null=True, blank=True,
verbose_name=_("End of presale"),
help_text=_("Optional. No products will be sold after this date."),
)
presale_start = models.DateTimeField(
null=True, blank=True,
verbose_name=_("Start of presale"),
help_text=_("Optional. No products will be sold before this date."),
)
location = I18nTextField(
null=True, blank=True,
max_length=200,
verbose_name=_("Location"),
)
frontpage_text = I18nTextField(
null=True, blank=True,
verbose_name=_("Frontpage text")
)
items = models.ManyToManyField('Item', through='SubEventItem')
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
class Meta:
verbose_name = _("Date in event series")
verbose_name_plural = _("Dates in event series")
ordering = ("date_from", "name")
def __str__(self):
return '{} - {}'.format(self.name, self.get_date_range_display())
@cached_property
def settings(self):
return self.event.settings
@cached_property
def item_price_overrides(self):
from .items import SubEventItem
return {
si.item_id: si.price
for si in SubEventItem.objects.filter(subevent=self, price__isnull=False)
}
@cached_property
def var_price_overrides(self):
from .items import SubEventItemVariation
return {
si.variation_id: si.price
for si in SubEventItemVariation.objects.filter(subevent=self, price__isnull=False)
}
@property
def meta_data(self):
data = self.event.meta_data
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
return data
def generate_invite_token():
@@ -374,3 +626,74 @@ class RequiredAction(models.Model):
if response:
return response
return self.action_type
class EventMetaProperty(LoggedModel):
"""
An organizer account can have EventMetaProperty objects attached to define meta information fields
for its events. This information can be re-used for example in ticket layouts.
:param organizer: The organizer this property is defined for.
:type organizer: Organizer
:param name: Name
:type name: Name of the property, used in various places
:param default: Default value
:type default: str
"""
organizer = models.ForeignKey(Organizer, related_name="meta_properties", on_delete=models.CASCADE)
name = models.CharField(
max_length=50, db_index=True,
help_text=_(
"Can not contain spaces or special characters execpt 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 EventMetaValue(LoggedModel):
"""
A meta-data value assigned to an event.
:param event: The event this metadata is valid for
:type event: Event
:param property: The property this value belongs to
:type property: EventMetaProperty
:param value: The actual value
:type value: str
"""
event = models.ForeignKey('Event', on_delete=models.CASCADE,
related_name='meta_values')
property = models.ForeignKey('EventMetaProperty', on_delete=models.CASCADE,
related_name='event_values')
value = models.TextField()
class Meta:
unique_together = ('event', 'property')
class SubEventMetaValue(LoggedModel):
"""
A meta-data value assigned to a sub-event.
:param event: The event this metadata is valid for
:type event: Event
:param property: The property this value belongs to
:type property: EventMetaProperty
:param value: The actual value
:type value: str
"""
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE,
related_name='meta_values')
property = models.ForeignKey('EventMetaProperty', on_delete=models.CASCADE,
related_name='subevent_values')
value = models.TextField()
class Meta:
unique_together = ('subevent', 'property')

View File

@@ -9,9 +9,10 @@ from django.utils.functional import cached_property
def invoice_filename(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
return 'invoices/{org}/{ev}/{no}-{code}-{secret}.pdf'.format(
return 'invoices/{org}/{ev}/{no}-{code}-{secret}.{ext}'.format(
org=instance.event.organizer.slug, ev=instance.event.slug,
no=instance.number, code=instance.order.code, secret=secret
no=instance.number, code=instance.order.code, secret=secret,
ext=filename.split('.')[-1]
)
@@ -28,6 +29,8 @@ class Invoice(models.Model):
:type order: Order
:param event: The event this belongs to (for convenience)
:type event: Event
:param organizer: The organizer this belongs to (redundant, for enforcing uniqueness)
:type organizer: Organizer
:param invoice_no: The human-readable, event-unique invoice number
:type invoice_no: int
:param is_cancellation: Whether or not this is a cancellation instead of an invoice
@@ -50,11 +53,19 @@ class Invoice(models.Model):
:type payment_provider_text: str
:param footer_text: A footer text, displayed smaller and centered on every page
:type footer_text: str
:param foreign_currency_display: A different currency that taxes should also be displayed in.
:type foreign_currency_display: str
:param foreign_currency_rate: The rate of a forein currency that the taxes should be displayed in.
:type foreign_currency_rate: Decimal
:param foreign_currency_rate_date: The date of the forein currency exchange rates.
:type foreign_currency_rate_date: date
:param file: The filename of the rendered invoice
:type file: File
"""
order = models.ForeignKey('Order', related_name='invoices', db_index=True)
organizer = models.ForeignKey('Organizer', related_name='invoices', db_index=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
prefix = models.CharField(max_length=160, db_index=True)
invoice_no = models.CharField(max_length=19, db_index=True)
is_cancellation = models.BooleanField(default=False)
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
@@ -66,6 +77,9 @@ class Invoice(models.Model):
additional_text = models.TextField(blank=True)
payment_provider_text = models.TextField(blank=True)
footer_text = models.TextField(blank=True)
foreign_currency_display = models.CharField(max_length=50, null=True, blank=True)
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
foreign_currency_rate_date = models.DateField(null=True, blank=True)
file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
@staticmethod
@@ -73,7 +87,10 @@ class Invoice(models.Model):
return '{:05d}'.format(int(number))
def _get_numeric_invoice_number(self):
numeric_invoices = Invoice.objects.filter(event=self.event).exclude(invoice_no__contains='-')
numeric_invoices = Invoice.objects.filter(
event__organizer=self.event.organizer,
prefix=self.prefix,
).exclude(invoice_no__contains='-')
return self._to_numeric_invoice_number(numeric_invoices.count() + 1)
def _get_invoice_number_from_order(self):
@@ -87,6 +104,10 @@ class Invoice(models.Model):
raise ValueError('Every invoice needs to be connected to an order')
if not self.event:
self.event = self.order.event
if not self.organizer:
self.organizer = self.order.event.organizer
if not self.prefix:
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
if not self.invoice_no:
for i in range(10):
if self.event.settings.get('invoice_numbers_consecutive'):
@@ -115,8 +136,8 @@ class Invoice(models.Model):
"""
Returns the invoice number in a human-readable string with the event slug prepended.
"""
return '{event}-{code}'.format(
event=self.event.slug.upper(),
return '{prefix}{code}'.format(
prefix=self.prefix,
code=self.invoice_no
)
@@ -125,7 +146,7 @@ class Invoice(models.Model):
return self.refered.filter(is_cancellation=True).exists()
class Meta:
unique_together = ('event', 'invoice_no')
unique_together = ('organizer', 'prefix', 'invoice_no')
ordering = ('invoice_no',)
@@ -143,13 +164,20 @@ class InvoiceLine(models.Model):
:type tax_value: decimal.Decimal
:param tax_rate: The applied tax rate in percent
:type tax_rate: decimal.Decimal
:param tax_name: The name of the applied tax rate
:type tax_name: str
"""
invoice = models.ForeignKey('Invoice', related_name='lines')
position = models.PositiveIntegerField(default=0)
description = models.TextField()
gross_value = models.DecimalField(max_digits=10, decimal_places=2)
tax_value = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
tax_name = models.CharField(max_length=190)
@property
def net_value(self):
return self.gross_value - self.tax_value
class Meta:
ordering = ('position', 'pk')

View File

@@ -10,13 +10,13 @@ from django.db import models
from django.db.models import F, Func, Q, Sum
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
from pretix.base.models.tax import TaxedPrice
from .event import Event
from .event import Event, SubEvent
class ItemCategory(LoggedModel):
@@ -88,6 +88,40 @@ def itempicture_upload_to(instance, filename: str) -> str:
)
class SubEventItem(models.Model):
"""
This model can be used to change the price of a product for a single subevent (i.e. a
date in an event series).
:param subevent: The date this belongs to
:type subevent: SubEvent
:param item: The item to modify the price for
:type item: Item
:param price: The modified price (or ``None`` for the original price)
:type price: Decimal
"""
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
item = models.ForeignKey('Item', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
class SubEventItemVariation(models.Model):
"""
This model can be used to change the price of a product variation for a single
subevent (i.e. a date in an event series).
:param subevent: The date this belongs to
:type subevent: SubEvent
:param variation: The variation to modify the price for
:type variation: ItemVariation
:param price: The modified price (or ``None`` for the original price)
:type price: Decimal
"""
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
class Item(LoggedModel):
"""
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
@@ -125,6 +159,8 @@ class Item(LoggedModel):
:type max_per_order: int
:param min_per_order: Minimum number of times this item needs to be in an order if bought at all. None for unlimited.
:type min_per_order: int
:param checkin_attention: Requires special attention at checkin
:type checkin_attention: bool
"""
event = models.ForeignKey(
@@ -139,6 +175,7 @@ class Item(LoggedModel):
related_name="items",
blank=True, null=True,
verbose_name=_("Category"),
help_text=_("If you have many products, you can optionally sort them into categories to keep things organized.")
)
name = I18nCharField(
max_length=255,
@@ -168,10 +205,11 @@ class Item(LoggedModel):
"additional donations for your event. This is currently not supported for products that are "
"bought as an add-on to other products.")
)
tax_rate = models.DecimalField(
verbose_name=_("Taxes included in percent"),
max_digits=7, decimal_places=2,
default=Decimal('0.00')
tax_rule = models.ForeignKey(
'TaxRule',
verbose_name=_('Sales tax'),
on_delete=models.PROTECT,
null=True, blank=True
)
admission = models.BooleanField(
verbose_name=_("Is an admission ticket"),
@@ -231,6 +269,13 @@ class Item(LoggedModel):
'empty or set it to 0, there is no special limit for this product. The limit for the maximum '
'number of items in the whole order applies regardless.')
)
checkin_attention = models.BooleanField(
verbose_name=_('Requires special attention'),
default=False,
help_text=_('If you set this, the check-in app will show a visible warning that this ticket requires special '
'attention. You can use this for example for student tickets to indicate to the person at '
'check-in that the student ID card still needs to be checked.')
)
# !!! Attention: If you add new fields here, also add them to the copying code in
# pretix/control/views/item.py if applicable.
@@ -252,10 +297,12 @@ class Item(LoggedModel):
if self.event:
self.event.get_cache().clear()
@property
def default_price_net(self):
tax_value = round_decimal(self.default_price * (1 - 100 / (100 + self.tax_rate)))
return self.default_price - tax_value
def tax(self, price=None, base_price_is='auto'):
price = price if price is not None else self.default_price
if not self.tax_rule:
return TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
rate=Decimal('0.00'), name='')
return self.tax_rule.tax(price, base_price_is=base_price_is)
def is_available(self, now_dt: datetime=None) -> bool:
"""
@@ -271,7 +318,7 @@ class Item(LoggedModel):
return False
return True
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None):
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None):
"""
This method is used to determine whether this Item is currently available
for sale.
@@ -285,12 +332,18 @@ class Item(LoggedModel):
:raises ValueError: if you call this on an item which has variations associated with it.
Please use the method on the ItemVariation object you are interested in.
"""
check_quotas = set(self.quotas.all())
check_quotas = set(getattr(
self, '_subevent_quotas', # Utilize cache in product list
self.quotas.select_related('subevent').filter(subevent=subevent)
if subevent else self.quotas.all()
))
if not subevent and self.event.has_subevents:
raise TypeError('You need to supply a subevent.')
if ignored_quotas:
check_quotas -= set(ignored_quotas)
if not check_quotas:
return Quota.AVAILABILITY_OK, sys.maxsize
if self.variations.count() > 0: # NOQA
if self.has_variations: # NOQA
raise ValueError('Do not call this directly on items which have variations '
'but call this on their ItemVariation objects')
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
@@ -356,10 +409,11 @@ class ItemVariation(models.Model):
def price(self):
return self.default_price if self.default_price is not None else self.item.default_price
@property
def net_price(self):
tax_value = round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate)))
return self.price - tax_value
def tax(self, price=None):
price = price or self.price
if not self.item.tax_rule:
return TaxedPrice(gross=price, net=price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
return self.item.tax_rule.tax(price)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -371,7 +425,7 @@ class ItemVariation(models.Model):
if self.item:
self.item.event.get_cache().clear()
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None) -> Tuple[int, int]:
"""
This method is used to determine whether this ItemVariation is currently
available for sale in terms of quotas.
@@ -383,9 +437,15 @@ class ItemVariation(models.Model):
:param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation.
:returns: any of the return codes of :py:meth:`Quota.availability()`.
"""
check_quotas = set(self.quotas.all())
check_quotas = set(getattr(
self, '_subevent_quotas', # Utilize cache in product list
self.quotas.filter(subevent=subevent).select_related('subevent')
if subevent else self.quotas.all()
))
if ignored_quotas:
check_quotas -= set(ignored_quotas)
if not subevent and self.item.event.has_subevents: # NOQA
raise TypeError('You need to supply a subevent.')
if not check_quotas:
return Quota.AVAILABILITY_OK, sys.maxsize
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
@@ -402,6 +462,17 @@ class ItemAddOn(models.Model):
An instance of this model indicates that buying a ticket of the time ``base_item``
allows you to add up to ``max_count`` items from the category ``addon_category``
to your order that will be associated with the base item.
:param base_item: The base item the add-ons are attached to
:type base_item: Item
:param addon_category: The category the add-on can be chosen from
:type addon_category: ItemCategory
:param min_count: The minimal number of add-ons to be chosen
:type min_count: int
:param max_count: The maximal number of add-ons to be chosen
:type max_count: int
:param position: An integer used for sorting
:type position: int
"""
base_item = models.ForeignKey(
Item,
@@ -420,6 +491,12 @@ class ItemAddOn(models.Model):
default=1,
verbose_name=_('Maximum number')
)
price_included = models.BooleanField(
default=False,
verbose_name=_('Add-Ons are included in the price'),
help_text=_('If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost '
'money individually.')
)
position = models.PositiveIntegerField(
default=0,
verbose_name=_("Position")
@@ -446,6 +523,7 @@ class Question(LoggedModel):
* a multi-line string (``TYPE_TEXT``)
* a boolean (``TYPE_BOOLEAN``)
* a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``)
* a file upload (``TYPE_FILE``))
:param event: The event this question belongs to
:type event: Event
@@ -463,13 +541,15 @@ class Question(LoggedModel):
TYPE_BOOLEAN = "B"
TYPE_CHOICE = "C"
TYPE_CHOICE_MULTIPLE = "M"
TYPE_FILE = "F"
TYPE_CHOICES = (
(TYPE_NUMBER, _("Number")),
(TYPE_STRING, _("Text (one line)")),
(TYPE_TEXT, _("Multiline text")),
(TYPE_BOOLEAN, _("Yes/No")),
(TYPE_CHOICE, _("Choose one from a list")),
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list"))
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")),
(TYPE_FILE, _("File upload")),
)
event = models.ForeignKey(
@@ -479,6 +559,11 @@ class Question(LoggedModel):
question = I18nTextField(
verbose_name=_("Question")
)
help_text = I18nTextField(
verbose_name=_("Help text"),
help_text=_("If the question needs to be explained or clarified, do it here!"),
null=True, blank=True,
)
type = models.CharField(
max_length=5,
choices=TYPE_CHOICES,
@@ -571,6 +656,8 @@ class Quota(LoggedModel):
:param event: The event this belongs to
:type event: Event
:param subevent: The event series date this belongs to, if event series are enabled
:type subevent: SubEvent
:param name: This quota's name
:type name: str
:param size: The number of items in this quota
@@ -590,6 +677,13 @@ class Quota(LoggedModel):
related_name="quotas",
verbose_name=_("Event"),
)
subevent = models.ForeignKey(
SubEvent,
null=True, blank=True,
on_delete=models.CASCADE,
related_name="quotas",
verbose_name=pgettext_lazy('subevent', "Date"),
)
name = models.CharField(
max_length=200,
verbose_name=_("Name")
@@ -611,6 +705,9 @@ class Quota(LoggedModel):
blank=True,
verbose_name=_("Variations")
)
cached_availability_state = models.PositiveIntegerField(null=True, blank=True)
cached_availability_number = models.PositiveIntegerField(null=True, blank=True)
cached_availability_time = models.DateTimeField(null=True, blank=True)
class Meta:
verbose_name = _("Quota")
@@ -625,11 +722,24 @@ class Quota(LoggedModel):
self.event.get_cache().clear()
def save(self, *args, **kwargs):
clear_cache = kwargs.pop('clear_cache', True)
super().save(*args, **kwargs)
if self.event:
if self.event and clear_cache:
self.event.get_cache().clear()
def availability(self, now_dt: datetime=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
def rebuild_cache(self, now_dt=None):
self.cached_availability_time = None
self.cached_availability_number = None
self.cached_availability_state = None
self.availability(now_dt=now_dt)
def cache_is_hot(self, now_dt=None):
now_dt = now_dt or now()
return self.cached_availability_time and (now_dt - self.cached_availability_time).total_seconds() < 120
def availability(
self, now_dt: datetime=None, count_waitinglist=True, _cache=None, allow_cache=False
) -> Tuple[int, int]:
"""
This method is used to determine whether Items or ItemVariations belonging
to this quota should currently be available for sale.
@@ -637,12 +747,26 @@ class Quota(LoggedModel):
:returns: a tuple where the first entry is one of the ``Quota.AVAILABILITY_`` constants
and the second is the number of available tickets.
"""
if allow_cache and self.cache_is_hot() and count_waitinglist:
return self.cached_availability_state, self.cached_availability_number
if _cache and count_waitinglist is not _cache.get('_count_waitinglist', True):
_cache.clear()
if _cache is not None and self.pk in _cache:
return _cache[self.pk]
now_dt = now_dt or now()
res = self._availability(now_dt, count_waitinglist)
if count_waitinglist and not self.cache_is_hot(now_dt):
self.cached_availability_state = res[0]
self.cached_availability_number = res[1]
self.cached_availability_time = now_dt
self.save(
update_fields=['cached_availability_state', 'cached_availability_number', 'cached_availability_time'],
clear_cache=False
)
if _cache is not None:
_cache[self.pk] = res
_cache['_count_waitinglist'] = count_waitinglist
@@ -684,11 +808,11 @@ class Quota(LoggedModel):
now_dt = now_dt or now()
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
func = 'MAX'
else:
else: # NOQA
func = 'GREATEST'
return Voucher.objects.filter(
Q(event=self.event) &
Q(event=self.event) & Q(subevent=self.subevent) &
Q(block_quota=True) &
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
Q(Q(self._position_lookup) | Q(quota=self))
@@ -699,7 +823,7 @@ class Quota(LoggedModel):
def count_waiting_list_pending(self) -> int:
from pretix.base.models import WaitingListEntry
return WaitingListEntry.objects.filter(
Q(voucher__isnull=True) &
Q(voucher__isnull=True) & Q(subevent=self.subevent) &
self._position_lookup
).distinct().count()
@@ -708,7 +832,7 @@ class Quota(LoggedModel):
now_dt = now_dt or now()
return CartPosition.objects.filter(
Q(event=self.event) &
Q(event=self.event) & Q(subevent=self.subevent) &
Q(expires__gte=now_dt) &
~Q(
Q(voucher__isnull=False) & Q(voucher__block_quota=True)
@@ -722,14 +846,14 @@ class Quota(LoggedModel):
# This query has beeen benchmarked against a Count('id', distinct=True) aggregate and won by a small margin.
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event, subevent=self.subevent
).values('id').distinct().count()
def count_paid_orders(self):
from pretix.base.models import Order, OrderPosition
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event, subevent=self.subevent
).values('id').distinct().count()
@cached_property

View File

@@ -5,7 +5,10 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.utils.html import escape
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.signals import logentry_object_link
class LogEntry(models.Model):
@@ -49,7 +52,7 @@ class LogEntry(models.Model):
@cached_property
def display_object(self):
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event, TaxRule, SubEvent
if self.content_type.model_class() is Event:
return ''
@@ -66,7 +69,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'code': co.code
}),
'val': co.code,
'val': escape(co.code),
}
elif isinstance(co, Voucher):
a_text = _('Voucher {val}')
@@ -76,7 +79,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'voucher': co.id
}),
'val': co.code[:6],
'val': escape(co.code[:6]),
}
elif isinstance(co, Item):
a_text = _('Product {val}')
@@ -86,7 +89,17 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'item': co.id
}),
'val': co.name,
'val': escape(co.name),
}
elif isinstance(co, SubEvent):
a_text = pgettext_lazy('subevent', 'Date {val}')
a_map = {
'href': reverse('control:event.subevent', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'subevent': co.id
}),
'val': escape(str(co))
}
elif isinstance(co, Quota):
a_text = _('Quota {val}')
@@ -96,7 +109,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'quota': co.id
}),
'val': co.name,
'val': escape(co.name),
}
elif isinstance(co, ItemCategory):
a_text = _('Category {val}')
@@ -106,7 +119,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'category': co.id
}),
'val': co.name,
'val': escape(co.name),
}
elif isinstance(co, Question):
a_text = _('Question {val}')
@@ -116,7 +129,17 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'question': co.id
}),
'val': co.question,
'val': escape(co.question),
}
elif isinstance(co, TaxRule):
a_text = _('Tax rule {val}')
a_map = {
'href': reverse('control:event.settings.tax.edit', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'rule': co.id
}),
'val': escape(co.name),
}
if a_text and a_map:
@@ -125,6 +148,9 @@ class LogEntry(models.Model):
elif a_text:
return a_text
else:
for receiver, response in logentry_object_link.send(self.event, logentry=self):
if response:
return response
return ''
@cached_property

View File

@@ -2,23 +2,31 @@ import copy
import json
import os
import string
from datetime import datetime
from datetime import datetime, time
from decimal import Decimal
from typing import List, Union
from typing import Any, Dict, List, Union
import pytz
from django.conf import settings
from django.db import models
from django.db.models import F, Sum
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.encoding import escape_uri_path
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import CountryField
from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language
from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper
from ..decimal import round_decimal
from .base import LoggedModel
from .event import Event
from .event import Event, SubEvent
from .items import Item, ItemVariation, Question, QuestionOption, Quota
@@ -70,18 +78,14 @@ class Order(LoggedModel):
:type payment_date: datetime
:param payment_provider: The payment provider selected by the user
:type payment_provider: str
:param payment_fee: The payment fee calculated at checkout time
:type payment_fee: decimal.Decimal
:param payment_fee_tax_value: The absolute amount of tax included in the payment fee
:type payment_fee_tax_value: decimal.Decimal
:param payment_fee_tax_rate: The tax rate applied to the payment fee (in percent)
:type payment_fee_tax_rate: decimal.Decimal
:param payment_info: Arbitrary information stored by the payment provider
:type payment_info: str
:param total: The total amount of the order, including the payment fee
:type total: decimal.Decimal
:param comment: An internal comment that will only be visible to staff, and never displayed to the user
:type comment: str
:param download_reminder_sent: A field to indicate whether a download reminder has been sent.
:type download_reminder_sent: boolean
:param meta_info: Additional meta information on the order, JSON-encoded.
:type meta_info: str
"""
@@ -139,18 +143,6 @@ class Order(LoggedModel):
max_length=255,
verbose_name=_("Payment provider")
)
payment_fee = models.DecimalField(
decimal_places=2, max_digits=10,
default=0, verbose_name=_("Payment method fee")
)
payment_fee_tax_rate = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Payment method fee tax rate")
)
payment_fee_tax_value = models.DecimalField(
decimal_places=2, max_digits=10,
default=0, verbose_name=_("Payment method fee tax")
)
payment_info = models.TextField(
verbose_name=_("Payment information"),
null=True, blank=True
@@ -171,6 +163,10 @@ class Order(LoggedModel):
expiry_reminder_sent = models.BooleanField(
default=False
)
download_reminder_sent = models.BooleanField(
default=False
)
meta_info = models.TextField(
verbose_name=_("Meta information"),
null=True, blank=True
@@ -205,29 +201,11 @@ class Order(LoggedModel):
self.assign_code()
if not self.datetime:
self.datetime = now()
if self.payment_fee_tax_rate is None:
self._calculate_tax()
super().save(*args, **kwargs)
def _calculate_tax(self):
"""
Calculates the taxes on the payment fees and sets the parameters payment_fee_tax_rate
and payment_fee_tax_value accordingly.
"""
self.payment_fee_tax_rate = self.event.settings.get('tax_rate_default')
if self.payment_fee_tax_rate:
self.payment_fee_tax_value = round_decimal(
self.payment_fee * (1 - 100 / (100 + self.payment_fee_tax_rate)))
else:
self.payment_fee_tax_value = Decimal('0.00')
@property
def payment_fee_net(self):
return self.payment_fee - self.payment_fee_tax_value
@cached_property
def tax_total(self):
return (self.positions.aggregate(s=Sum('tax_value'))['s'] or 0) + self.payment_fee_tax_value
return (self.positions.aggregate(s=Sum('tax_value'))['s'] or 0) + (self.fees.aggregate(s=Sum('tax_value'))['s'] or 0)
@property
def net_total(self):
@@ -264,7 +242,16 @@ class Order(LoggedModel):
"""
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
return False
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=datetime)
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
if self.event.has_subevents and modify_deadline:
modify_deadline = min([
modify_deadline.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
])
elif modify_deadline:
modify_deadline = modify_deadline.datetime(self.event)
if modify_deadline is not None and now() > modify_deadline:
return False
if self.event.settings.get('invoice_address_asked', as_type=bool):
@@ -278,6 +265,9 @@ class Order(LoggedModel):
@property
def can_user_cancel(self) -> bool:
"""
Returns whether or not this order can be canceled by the user.
"""
positions = self.positions.all().select_related('item')
cancelable = all([op.item.allow_cancel for op in positions])
return self.event.settings.cancel_allow_user and cancelable
@@ -289,6 +279,41 @@ class Order(LoggedModel):
and not self.event.settings.get('payment_term_expire_automatically')
)
@property
def ticket_download_date(self):
"""
Returns the first date the tickets for this order can be downloaded or ``None`` if there is no
restriction.
"""
dl_date = self.event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
if dl_date:
if self.event.has_subevents:
dl_date = min([
dl_date.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
])
else:
dl_date = dl_date.datetime(self.event)
return dl_date
@property
def payment_term_last(self):
tz = pytz.timezone(self.event.settings.timezone)
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if self.event.has_subevents:
term_last = min([
term_last.datetime(se).date()
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
])
else:
term_last = term_last.datetime(self.event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
return term_last
def _can_be_paid(self) -> Union[bool, str]:
error_messages = {
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
@@ -296,9 +321,9 @@ class Order(LoggedModel):
'late': _("The payment can not be accepted as it the order is expired and you configured that no late "
"payments should be accepted in the payment settings."),
}
if self.event.settings.get('payment_term_last'):
if now() > self.event.payment_term_last:
term_last = self.payment_term_last
if term_last:
if now() > term_last:
return error_messages['late_lastdate']
if self.status == self.STATUS_PENDING:
@@ -308,7 +333,7 @@ class Order(LoggedModel):
return self._is_still_available()
def _is_still_available(self, now_dt: datetime=None) -> Union[bool, str]:
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True) -> Union[bool, str]:
error_messages = {
'unavailable': _('The ordered product "{item}" is no longer available.'),
}
@@ -317,14 +342,16 @@ class Order(LoggedModel):
quota_cache = {}
try:
for i, op in enumerate(positions):
quotas = list(op.item.quotas.all()) if op.variation is None else list(op.variation.quotas.all())
quotas = list(op.quotas)
if len(quotas) == 0:
raise Quota.QuotaExceededException(error_messages['unavailable'])
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
item=str(op.item) + (' - ' + str(op.variation) if op.variation else '')
))
for quota in quotas:
if quota.id not in quota_cache:
quota_cache[quota.id] = quota
quota.cached_availability = quota.availability(now_dt)[1]
quota.cached_availability = quota.availability(now_dt, count_waitinglist=count_waitinglist)[1]
else:
# Use cached version
quota = quota_cache[quota.id]
@@ -339,6 +366,59 @@ class Order(LoggedModel):
return str(e)
return True
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
* Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, ``recipient`` and
``order`` parameters.
* Create a ``LogEntry`` with the email contents.
:param subject: Subject of the email
:param template: LazyI18nString or template filename, see ``pretix.base.services.mail.mail`` for more details
:param context: Dictionary to use for rendering the template
:param log_entry_type: Key to be used for the log entry
:param user: Administrative user who triggered this mail to be sent
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
recipient = self.email
email_content = render_mail(template, context)[0]
try:
with language(self.locale):
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
data={
'subject': subject,
'message': email_content,
'recipient': recipient
}
)
def answerfile_name(instance, filename: str) -> str:
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
event = (instance.cartposition if instance.cartposition else instance.orderposition.order).event
return 'cachedfiles/answers/{org}/{ev}/{secret}.{filename}'.format(
org=event.organizer.slug,
ev=event.slug,
secret=secret,
filename=escape_uri_path(filename),
)
class QuestionAnswer(models.Model):
"""
@@ -370,12 +450,52 @@ class QuestionAnswer(models.Model):
QuestionOption, related_name='answers', blank=True
)
answer = models.TextField()
file = models.FileField(
null=True, blank=True, upload_to=answerfile_name
)
@property
def backend_file_url(self):
if self.file:
if self.orderposition:
return reverse('control:event.order.download.answer', kwargs={
'code': self.orderposition.order.code,
'event': self.orderposition.order.event.slug,
'organizer': self.orderposition.order.event.organizer.slug,
'answer': self.pk,
})
return ""
@property
def frontend_file_url(self):
from pretix.multidomain.urlreverse import eventreverse
if self.file:
if self.orderposition:
url = eventreverse(self.orderposition.order.event, 'presale:event.order.download.answer', kwargs={
'order': self.orderposition.order.code,
'secret': self.orderposition.order.secret,
'answer': self.pk,
})
else:
url = eventreverse(self.cartposition.event, 'presale:event.cart.download.answer', kwargs={
'answer': self.pk,
})
return url
return ""
@property
def file_name(self):
return self.file.name.split('.', 1)[-1]
def __str__(self):
if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True":
return str(_("Yes"))
elif self.question.type == Question.TYPE_BOOLEAN and self.answer == "False":
return str(_("No"))
elif self.question.type == Question.TYPE_FILE:
return str(_("<file>"))
else:
return self.answer
@@ -389,6 +509,8 @@ class AbstractPosition(models.Model):
"""
A position can either be one line of an order or an item placed in a cart.
:param subevent: The date in the event series, if event series are enabled
:type subevent: SubEvent
:param item: The selected item
:type item: Item
:param variation: The selected ItemVariation or null, if the item has no variations
@@ -405,7 +527,15 @@ class AbstractPosition(models.Model):
:type attendee_email: str
:param voucher: A voucher that has been applied to this sale
:type voucher: Voucher
:param meta_info: Additional meta information on the position, JSON-encoded.
:type meta_info: str
"""
subevent = models.ForeignKey(
SubEvent,
null=True, blank=True,
on_delete=models.CASCADE,
verbose_name=pgettext_lazy("subevent", "Date"),
)
item = models.ForeignKey(
Item,
verbose_name=_("Item"),
@@ -438,10 +568,21 @@ class AbstractPosition(models.Model):
addon_to = models.ForeignKey(
'self', null=True, blank=True, on_delete=models.CASCADE, related_name='addons'
)
meta_info = models.TextField(
verbose_name=_("Meta information"),
null=True, blank=True
)
class Meta:
abstract = True
@property
def meta_info_data(self):
if self.meta_info:
return json.loads(self.meta_info)
else:
return {}
def cache_answers(self):
"""
Creates two properties on the object.
@@ -466,6 +607,97 @@ class AbstractPosition(models.Model):
def net_price(self):
return self.price - self.tax_value
@property
def quotas(self):
return (self.item.quotas.filter(subevent=self.subevent)
if self.variation is None
else self.variation.quotas.filter(subevent=self.subevent))
class OrderFee(models.Model):
"""
An OrderFee objet represents a fee that is added to the order total independently of
the actual positions. This might for example be a payment or a shipping fee.
"""
FEE_TYPE_PAYMENT = "payment"
FEE_TYPE_SHIPPING = "shipping"
FEE_TYPE_OTHER = "other"
FEE_TYPES = (
(FEE_TYPE_PAYMENT, _("Payment fee")),
(FEE_TYPE_SHIPPING, _("Shipping fee")),
(FEE_TYPE_OTHER, _("Other fees")),
)
value = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Value")
)
order = models.ForeignKey(
Order,
verbose_name=_("Order"),
related_name='fees',
on_delete=models.PROTECT
)
fee_type = models.CharField(
max_length=100, choices=FEE_TYPES
)
description = models.CharField(max_length=190, blank=True)
internal_type = models.CharField(max_length=255, blank=True)
tax_rate = models.DecimalField(
max_digits=7, decimal_places=2,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
'TaxRule',
on_delete=models.PROTECT,
null=True, blank=True
)
tax_value = models.DecimalField(
max_digits=10, decimal_places=2,
verbose_name=_('Tax value')
)
@property
def net_value(self):
return self.value - self.tax_value
def __str__(self):
if self.description:
return '{} - {}'.format(self.get_fee_type_display(), self.description)
else:
return self.get_fee_type_display()
def __repr__(self):
return '<OrderFee: type %s, value %d>' % (
self.fee_type, self.value
)
def _calculate_tax(self):
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
if not self.tax_rule and self.fee_type == "payment" and self.order.event.settings.tax_rate_default:
self.tax_rule = self.order.event.settings.tax_rate_default
if self.tax_rule:
if self.tax_rule.tax_applicable(ia):
tax = self.tax_rule.tax(self.value, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
def save(self, *args, **kwargs):
if self.tax_rate is None:
self._calculate_tax()
return super().save(*args, **kwargs)
class OrderPosition(AbstractPosition):
"""
@@ -487,6 +719,15 @@ class OrderPosition(AbstractPosition):
max_digits=7, decimal_places=2,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
'TaxRule',
on_delete=models.PROTECT,
null=True, blank=True
)
tax_value = models.DecimalField(
max_digits=10, decimal_places=2,
verbose_name=_('Tax value')
)
tax_value = models.DecimalField(
max_digits=10, decimal_places=2,
verbose_name=_('Tax value')
@@ -546,11 +787,22 @@ class OrderPosition(AbstractPosition):
)
def _calculate_tax(self):
self.tax_rate = self.item.tax_rate
if self.tax_rate:
self.tax_value = round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate)))
self.tax_rule = self.item.tax_rule
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
if self.tax_rule:
if self.tax_rule.tax_applicable(ia):
tax = self.tax_rule.tax(self.price, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
def save(self, *args, **kwargs):
if self.tax_rate is None:
@@ -591,6 +843,9 @@ class CartPosition(AbstractPosition):
verbose_name=_("Expiration date"),
db_index=True
)
includes_tax = models.BooleanField(
default=True
)
class Meta:
verbose_name = _("Cart position")
@@ -603,25 +858,33 @@ class CartPosition(AbstractPosition):
@property
def tax_rate(self):
return self.item.tax_rate
if self.includes_tax:
return self.item.tax(self.price, base_price_is='gross').rate
else:
return Decimal('0.00')
@property
def tax_value(self):
if not self.tax_rate:
if self.includes_tax:
return self.item.tax(self.price, base_price_is='gross').tax
else:
return Decimal('0.00')
return round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate)))
class InvoiceAddress(models.Model):
last_modified = models.DateTimeField(auto_now=True)
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address')
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
name = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
street = models.TextField(verbose_name=_('Address'), 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)
country = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'))
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))
vat_id_validated = models.BooleanField(default=False)
def cachedticket_name(instance, filename: str) -> str:
@@ -671,3 +934,17 @@ def cachedticket_delete(sender, instance, **kwargs):
if instance.file:
# Pass false so FileField doesn't save the model.
instance.file.delete(False)
@receiver(post_delete, sender=CachedCombinedTicket)
def cachedcombinedticket_delete(sender, instance, **kwargs):
if instance.file:
# Pass false so FileField doesn't save the model.
instance.file.delete(False)
@receiver(post_delete, sender=QuestionAnswer)
def answer_delete(sender, instance, **kwargs):
if instance.file:
# Pass false so FileField doesn't save the model.
instance.file.delete(False)

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