Compare commits

...

127 Commits

Author SHA1 Message Date
Raphael Michel
4e769ba11e Locking optimizations 2019-05-05 16:08:41 +02:00
Raphael Michel
32e66aeb55 Documentation on scaling 2019-05-05 15:46:06 +02:00
Raphael Michel
9bc6941c14 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-05-01 14:02:11 +02:00
Raphael Michel
987da83894 Refs #1102 -- Accept order URLs in order lookup 2019-05-01 14:01:26 +02:00
Raphael Michel
d029d92a92 Fix #1102 -- "View in backend" (doesn't work with custom domains) 2019-05-01 14:01:26 +02:00
Raphael Michel
f1b07777bc Timezone indicators in the backend 2019-05-01 14:01:26 +02:00
Raphael Michel
db187a2537 Fix #1126 -- Use short datetime format on order details page 2019-05-01 14:01:26 +02:00
Raphael Michel
e9a340d9ca Refs #1128 -- Popover on disabled "add to cart" button 2019-05-01 14:01:26 +02:00
Raphael Michel
6841a30d8f Fix #1153 -- Show preview of uploaded pictures in the backend 2019-05-01 14:01:26 +02:00
Raphael Michel
30b8c0f4b9 Fix ClearableBasenameFileInput with current Django 2019-05-01 14:01:26 +02:00
Raphael Michel
3e8f32e7e3 Fix #1178 -- Invalidate ticket cache after order locale change 2019-05-01 14:01:26 +02:00
Raphael Michel
2b145e254b Fix #1211 -- Locale selection on organizer profile 2019-05-01 14:01:26 +02:00
Raphael Michel
e5c2470fde "Go to shop" for organizers 2019-05-01 14:01:26 +02:00
Raphael Michel
2da93eba26 Fix #1230 -- Stripe: Recognize canceled sources in webhook 2019-05-01 14:01:26 +02:00
Raphael Michel
788f73d842 Fix #1255 -- Approvals of free orders after last date of payments 2019-05-01 14:01:26 +02:00
Raphael Michel
d86b3a2173 Update from Weblate (#1265)
Update from Weblate
2019-04-30 09:51:49 +02:00
Tobias Sundgren
7be6046ed5 Translated on translate.pretix.eu (Swedish)
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Maarten van den Berg
6b90689067 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Maarten van den Berg
815816b9d6 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Tobias Sundgren
3199687fe4 Translated on translate.pretix.eu (Swedish)
Currently translated at 0.8% (24 of 3060 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Maarten van den Berg
6d8b8c6346 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3060 of 3060 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Maarten van den Berg
8a850773f4 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3060 of 3060 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Raphael Michel
2a10f875e4 Added translation on translate.pretix.eu (Swedish) 2019-04-30 07:51:24 +00:00
Raphael Michel
d8d2a21bda Added translation on translate.pretix.eu (Swedish) 2019-04-30 07:51:24 +00:00
oocf
18eb468d8e Translated on translate.pretix.eu (Spanish)
Currently translated at 99.0% (3028 of 3060 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
ThanosTeste
2842b0e720 Translated on translate.pretix.eu (Greek)
Currently translated at 81.4% (79 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Chris Spy
42936a931b Translated on translate.pretix.eu (Greek)
Currently translated at 81.4% (79 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Raphael Michel
a6c72abe75 Change semantics of changing orders (#1260)
* Change semantics of changing orders

This basically does two things to the "Change products" view of orders and the
OrderChangeManager program API:

1) It decouples changing items or subevents from changing prices.
   OrderChangeManager.change_item() and .change_subevent() no longer
   touch the price of a position. Instead .change_price() needs to be
   called explicitly. However, a client-side JavaScript component now
   *proposes* a new price based on the changed item or subevent.

2) The user interface now exposes the possibility of doing multiple
   things at the same time, i.e. changing the item, subevent and price
   in the same operation. OrderChangeManager already allowed this
   before.

(1) is basically a consequence of (2), while (2) is a prerequesite for
e.g. the `seating` branch, where changing the subevent will always
require changing the seat.

* Add tests for price calculation API
2019-04-30 09:51:19 +02:00
Raphael Michel
df3e6f4b9a dekodi: Fix version and mandatory fields 2019-04-30 09:50:47 +02:00
Raphael Michel
8ef99ba828 Dekodi: Merchant PayPal IDs 2019-04-30 09:50:17 +02:00
Raphael Michel
e8e5f5c7bf Dekodi: Get rid of null values 2019-04-29 15:46:48 +02:00
Martin Gross
f0128429e4 Format amount in GiroCode/EPC-QR with dot instead of locale 2019-04-29 13:54:53 +02:00
Raphael Michel
cc8e5a7f83 Widget: original price for variations 2019-04-29 09:30:03 +02:00
Raphael Michel
d4d3928146 Expose is_public in subevent editor 2019-04-29 09:30:03 +02:00
Raphael Michel
cc4602c308 API Auth: Respect staff sessions 2019-04-26 16:24:13 +02:00
Raphael Michel
2bc0dd6076 Dekodi export: date filter 2019-04-26 15:22:10 +02:00
Raphael Michel
f286c5af28 Dekodi: Never encode money as strings 2019-04-25 21:07:10 +02:00
Raphael Michel
ec27ed198b Add Dekodi exporter 2019-04-25 20:36:24 +02:00
Raphael Michel
2ee0f684c5 PDF variable: price including add-ons 2019-04-25 19:34:51 +02:00
Raphael Michel
951386b32c Add subevent column to order list export 2019-04-25 15:08:22 +02:00
Raphael Michel
f498e8fafa Fix faulty test cases 2019-04-25 14:00:55 +02:00
Raphael Michel
b79947fba4 Widget: Original price for variations 2019-04-25 11:54:21 +02:00
Raphael Michel
ef600ceddb Fix invalid handling of variations with quota-level vouchers 2019-04-25 11:54:03 +02:00
Raphael Michel
13bf975dd5 Fix KeyError during form validation 2019-04-25 10:36:29 +02:00
Raphael Michel
8e56c8dcf7 Fix documentation typos 2019-04-23 17:39:09 +02:00
Raphael Michel
a42b31560c Check-in API: Fall back from attendee_name 2019-04-23 17:25:35 +02:00
Raphael Michel
e15e7a5877 Check-in API: Return 400 instead of 404 on checking in unpaid orders 2019-04-23 17:18:16 +02:00
Raphael Michel
e7384f7e85 Check-in API: require_attention and ignore_status 2019-04-23 17:06:24 +02:00
Raphael Michel
840b30c3c2 Linkify email addresses 2019-04-23 17:06:24 +02:00
Martin Gross
1adabec989 Fix test: Price override shows old price as <del> 2019-04-23 15:37:51 +02:00
Martin Gross
171bea59df Show strikethrough price when voucher is granting discount 2019-04-23 14:26:21 +02:00
Martin Gross
3c4b086992 Show strikethrough original_price when redeeming voucher 2019-04-23 11:55:31 +02:00
Raphael Michel
6a4e6e227c Fix isort issue 2019-04-23 11:19:19 +02:00
Raphael Michel
9c3abc5338 More precise log message for skipped attachments 2019-04-23 11:18:03 +02:00
Raphael Michel
91b2d7989a Update from Weblate (#1261)
Update from Weblate
2019-04-23 10:55:30 +02:00
Raphael Michel
c5a80e6daf Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-23 08:55:15 +00:00
Raphael Michel
37ce9fa9af Translated on translate.pretix.eu (German)
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-23 08:55:14 +00:00
Raphael Michel
64fe3d772c Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3060 of 3060 strings)

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

powered by weblate
2019-04-23 08:55:14 +00:00
Raphael Michel
5c82781fcc Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3060 of 3060 strings)

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

powered by weblate
2019-04-23 08:54:56 +00:00
Raphael Michel
0d70e3c8e3 Specify minor version of urllib3 2019-04-23 10:50:59 +02:00
Raphael Michel
85a7f0c0cc Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-04-23 10:50:48 +02:00
Raphael Michel
6d0e1097e6 Update from Weblate (#1257)
Update from Weblate
2019-04-23 10:50:10 +02:00
David100mark
c557087252 Translated on translate.pretix.eu (French)
Currently translated at 77.3% (2366 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
David100mark
62796cdc5f Translated on translate.pretix.eu (French)
Currently translated at 77.2% (2362 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
David100mark
bbe5f9bd98 Translated on translate.pretix.eu (French)
Currently translated at 76.1% (2329 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
Maarten van den Berg
003ccd83bf Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3059 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
Maarten van den Berg
f8f6dc4a51 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3059 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
Raphael Michel
cddf716784 Translated on translate.pretix.eu (German (informal))
Currently translated at 99.9% (3057 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
Raphael Michel
ee495f2777 Add property SubEvent.is_public 2019-04-23 10:46:09 +02:00
Raphael Michel
5bdc9011c1 Widget: Specific wording for mobing back to subevents 2019-04-23 10:37:25 +02:00
Raphael Michel
c6ea30ec1e Widget: Handle resize events 2019-04-23 10:35:07 +02:00
Raphael Michel
f9341b4d47 Downgrade urllib3 2019-04-23 10:15:01 +02:00
Raphael Michel
2205e57650 Fail consistently on invalid payment providers 2019-04-23 09:47:55 +02:00
Raphael Michel
ad8fdd6935 Ignore quota errors during order creation 2019-04-23 09:47:44 +02:00
Raphael Michel
02e936ee7a Fix #522 -- Do not allow any orders after the last date of payments 2019-04-23 09:46:34 +02:00
Raphael Michel
45a6923220 Refs #522 -- Do not allow to create orders after the last date of payments 2019-04-23 09:41:01 +02:00
Raphael Michel
e4417305a2 Fix updatestyles not being sent to background queue 2019-04-18 17:44:14 +02:00
Raphael Michel
bc5d0bea00 updatestyles: Prioritize future events over past ones 2019-04-18 17:27:34 +02:00
Raphael Michel
dbce9b0395 Allow error pages to be embedded in frames (to ease widget troubleshooting) 2019-04-18 17:19:42 +02:00
Martin Gross
2eb88840bd Original price for variations (#1258)
* Original price for variations

* Documentation

* API-GET

* Fix existing tests to accomodate new attribute

* Test for variation's original_price on API
2019-04-18 16:13:49 +02:00
Martin Gross
4838835b1b Remove debug-toolbar template override 2019-04-18 12:21:42 +02:00
Raphael Michel
ab452bd9e3 Fix typo 2019-04-18 09:50:07 +02:00
Raphael Michel
ae298bddb8 Make FakeRedis play nice with metrics 2019-04-18 09:17:55 +02:00
Raphael Michel
9ad4607d26 Move ticket cache invalidation to background task 2019-04-18 09:17:01 +02:00
Raphael Michel
b3684377cd Fix crash in item validation
Fixes Sentry PRETIXEU-10B
2019-04-17 15:40:25 +02:00
Raphael Michel
441badfdbd Bank transfer: Move ack field 2019-04-17 15:38:26 +02:00
Raphael Michel
0d242a0304 Fix internal error during validation
Sentry PRETIXEU-10A
2019-04-17 15:21:42 +02:00
Raphael Michel
2fac8592d4 Add modern invoice renderer 2019-04-17 15:08:58 +02:00
Raphael Michel
58b1a2f115 Fix timezone handling in widget 2019-04-17 14:42:00 +02:00
Raphael Michel
420d44e909 Fix #1170 -- E-mail address in check-in list 2019-04-17 12:12:07 +02:00
Raphael Michel
e0063fce52 Allow superusers to inspect payments and refunds 2019-04-17 10:15:14 +02:00
Raphael Michel
21ef6c7950 Update framework classifier 2019-04-17 10:07:02 +02:00
Sohalt
651f429ffb Fix #1247 -- Allow team invites to be resent (#1250)
* Fix #1247 -- Allow team invites to be resent

* Test resending invalid invites

* Fix tooltip

* Fix test

* Handle invalid types for pk parameter

* Style button
2019-04-16 16:39:31 +02:00
Raphael Michel
66dd7c448b Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-04-16 13:35:36 +02:00
Raphael Michel
e9b4205145 Fix translation of widget headlines 2019-04-16 13:35:07 +02:00
Raphael Michel
6dedea1025 Items API: Note that tax_rate is read-only 2019-04-16 13:35:07 +02:00
Raphael Michel
348ed4e909 Merge pull request #1244 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-04-16 13:34:26 +02:00
Maarten van den Berg
091b3358e4 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-04-16 05:00:07 +00:00
Maarten van den Berg
186e2a6b9a Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-04-16 05:00:06 +00:00
Maarten van den Berg
198b90972c Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-16 05:00:06 +00:00
Maarten van den Berg
4989b6235c Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-16 05:00:05 +00:00
mussol
4cfebab11c Translated on translate.pretix.eu (Catalan)
Currently translated at 35.1% (1073 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Martin Gross
fe944ec643 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Martin Gross
9d92c7b10f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
3b810a3a76 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
7860417177 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 99.4% (3038 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
1438edb3c8 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
a17720062b Translated on translate.pretix.eu (Catalan)
Currently translated at 31.8% (972 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
37ab45b352 Translated on translate.pretix.eu (Catalan)
Currently translated at 31.6% (966 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
6be212df8c Translated on translate.pretix.eu (Catalan)
Currently translated at 31.1% (952 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
b4e85780f4 Translated on translate.pretix.eu (Catalan)
Currently translated at 30.0% (917 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
a9732c4788 Translated on translate.pretix.eu (Catalan)
Currently translated at 30.0% (917 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
273316be25 Translated on translate.pretix.eu (Catalan)
Currently translated at 29.4% (900 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
0f3b269931 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 98.9% (3023 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
4462054d0e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Raphael Michel
ec53022cc8 Do not call task synchronously inside task (celery doesn't allow it any more) 2019-04-15 15:46:37 +02:00
Raphael Michel
0b65b18459 Send emails in an TransactionAwareTask 2019-04-15 15:22:58 +02:00
Raphael Michel
2fac03f47b Add a test case for free orders 2019-04-15 15:14:35 +02:00
Raphael Michel
750d5eda48 Do not mark free orders as paid that require approval 2019-04-15 15:12:26 +02:00
Raphael Michel
f2cd9a2002 Fix logic bug in attachment size check 2019-04-15 12:58:36 +02:00
Raphael Michel
874b38db17 Mark order as paid immediately 2019-04-15 12:58:20 +02:00
Raphael Michel
0f58e1c396 CSV import: Do not skip rows without a reference 2019-04-08 17:55:28 +02:00
Raphael Michel
36e0afc09e Further improvements to the print stylesheet 2019-04-08 17:42:06 +02:00
Raphael Michel
7164124a70 Display category description in add-on step 2019-04-08 15:23:40 +02:00
Raphael Michel
887d8832c0 Improve print CSS of order details 2019-04-07 18:12:12 +02:00
Raphael Michel
beb144f9a0 Fix API log cleanup 2019-04-07 15:31:35 +02:00
Raphael Michel
6d1dea7922 Upgrade to Django 2.2 and modern DRF and py.test (#1246)
* Upgrade django and stuff

* Update to Django 2.2 and recent versions of similar packages

* Provide explicit orderings to all models used in paginated queries

* Resolve naive datetime warnings in test suite

* Deal with deprecation warnings

* Fix sqlparse version
2019-04-07 14:09:49 +01:00
184 changed files with 53273 additions and 33281 deletions

View File

@@ -125,6 +125,8 @@ Example::
Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
.. _`config-replica`:
Database replica settings
-------------------------
@@ -142,6 +144,8 @@ Example::
[replica]
host=192.168.0.2
.. _`config-urls`:
URLs
----

View File

@@ -11,3 +11,4 @@ This documentation is for everyone who wants to install pretix on a server.
installation/index
config
maintainance
scaling

View File

@@ -1,3 +1,5 @@
.. _`installation`:
Installation guide
==================

236
doc/admin/scaling.rst Normal file
View File

@@ -0,0 +1,236 @@
.. _`scaling`:
Scaling guide
=============
Our :ref:`installation guide <installation>` only covers "small-scale" setups, by which we mostly mean
setups that run on a **single (virtual) machine** and do not encounter large traffic peaks.
We do not offer an installation guide for larger-scale setups of pretix, mostly because we believe that
there is no one-size-fits-all solution for this and the desired setup highly depends on your use case,
the platform you run pretix on, and your technical capabilities. We do not recommend trying set up pretix
in a multi-server environment if you do not already have experience with managing server clusters.
This document is intended to give you a general idea on what issues you will encounter when you scale up
and what you should think of.
.. tip::
If you require more help on this, we're happy to help. Our pretix Enterprise support team has built
and helped building, scaling and load-testing pretix installations at any scale and we're looking
forward to work with you on fine-tuning your system. If you intend to sell **more than a thousand
tickets in a very short amount of time**, we highly recommend reaching out and at least talking this
through. Just get in touch at sales@pretix.eu!
Scaling reasons
---------------
There's mainly two reasons to scale up a pretix installation beyond a single server:
* **Availability:** Distributing pretix over multiple servers can allow you to survive failure of one or more single machines, leading to a higher uptime and reliability of your system.
* **Traffic and throughput:** Distributing pretix over multiple servers can allow you to process more web requests and ticket sales at the same time.
You are very unlikely to require scaling for other reasons, such as having too much data in your database.
Components
----------
A pretix installation usually consists of the following components which run performance-relevant processes:
* ``pretix-web`` is the Django-based web application that serves all user interaction.
* ``pretix-worker`` is a Celery-based application that processes tasks that should be run asynchronously outside of the web application process.
* A **SQL database** keeps all the important data and processes the actual transactions. We recommend using PostgreSQL, but MySQL/MariaDB works as well.
* A **web server** that terminates TLS and HTTP connections and forwards them to ``pretix-web``. In some cases, e.g. when serving static files, the web servers might return a response directly. We recommend using ``nginx``.
* A **redis** server responsible for the communication between ``pretix-web`` and ``pretix-worker``, as well as for caching.
* A directory of **media files** such as user-uploaded files or generated files (tickets, invoices, …) that are created and used by ``pretix-web``, ``pretix-worker`` and the web server.
In the following, we will discuss the scaling behavior of every component individually. In general, you can run all of the components
on the same server, but you can just as well distribute every component to its own server, or even use multiple servers for some single
components.
.. warning::
When setting up your system, don't forget about security. In a multi-server environment,
you need to take special care to ensure that no unauthorized access to your database
is possible through the network and that it's not easy to wiretap your connections. We
recommend a rigorous use of firewalls and encryption on all communications. You can
ensure this either on an application level (such as using the TLS support in your
database) or on a network level with a VPN solution.
Web server
""""""""""
Your web server is at the very front of your installation. It will need to absorb all of the traffic, and it should be able to
at least show a decent error message, even when everything else fails. Luckily, web servers are really fast these days, so this
can be achieved without too much work.
We recommend reading up on tuning your web server for high concurrency. For nginx, this means thinking about the number of worker
processes and the number of connections each worker process accepts. Double-check that TLS session caching works, because TLS
handshakes can get really expensive.
During a traffic peak, your web server will be able to make us of more CPU resources, while memory usage will stay comparatively low,
so if you invest in more hardware here, invest in more and faster CPU cores.
Make sure that pretix' static files (such as CSS and JavaScript assets) as well as user-uploaded media files (event logos, etc)
are served directly by your web server and your web server caches them in-memory (nginx does it by default) and sets useful
headers for client-side caching. As an additional performance improvement, you can turn of access logging for these types of files.
If you want, you can even farm out serving static files to a different web server entirely and :ref:`configure pretix to reference
them from a different URL <config-urls>`.
.. tip::
If you expect *really high traffic* for your very popular event, you might want to do some rate limiting on this layer, or,
if you want to ensure a fair and robust first-come-first-served experience and prefer letting users wait over showing them
errors, consider a queuing solution. We're happy to provide you with such systems, just get in touch at sales@pretix.eu.
pretix-web
""""""""""
The ``pretix-web`` process does not carry any internal state can be easily started on as many machines as you like, and you can
use the load balancing features of your frontend web server to redirect to all of them.
You can adjust the number of processes in the ``gunicorn`` command line, and we recommend choosing roughly two times the number
of CPU cores available. Under load, the memory consumption of ``pretix-web`` will stay comparatively constant, while the CPU usage
will increase a lot. Therefore, if you can add more or faster CPU cores, you will be able to serve more users.
pretix-worker
"""""""""""""
The ``pretix-worker`` process performs all operations that are not directly executed in the request-response-cycle of ``pretix-web``.
Just like ``pretix-web`` you can easily start up as many instances as you want on different machines to share the work. As long as they
all talk to the same redis server, they will all receive tasks from ``pretix-web``, work on them and post their result back.
You can configure the number of threads that run tasks in parallel through the ``--concurrency`` command line option of ``celery``.
Just like ``pretix-web``, this process is mostly heavy on CPU, disk IO and network IO, although memory peaks can occur e.g. during the
generation of large PDF files, so we recommend having some reserves here.
``pretix-worker`` performs a variety of tasks which are of different importance.
Some of them are mission-critical and need to be run quickly even during high load (such as
creating a cart or an order), others are irrelevant and can easily run later (such as
distributing tickets on the waiting list). You can fine-tune the capacity you assign to each
of these tasks by running ``pretix-worker`` processes that only work on a specific **queue**.
For example, you could have three servers dedicated only to process order creations and one
server dedicated only to sending emails. This allows you to set priorities and also protects
you from e.g. a slow email server lowering your ticket throughput.
You can do so by specifying one or more queues on the ``celery`` command line of this process, such as ``celery -A pretix.celery_app worker -Q notifications,mail``. Currently,
the following queues exist:
* ``checkout`` -- This queue handles everything related to carts and orders and thereby everything required to process a sale. This includes adding and deleting items from carts as well as creating and canceling orders.
* ``mail`` -- This queue handles sending of outgoing emails.
* ``notifications`` -- This queue handles the processing of any outgoing notifications, such as email notifications to admin users (except for the actual sending) or API notifications to registered webhooks.
* ``background`` -- This queue handles tasks that are expected to take long or have no human waiting for their result immediately, such as refreshing caches, re-generating CSS files, assigning tickets on the waiting list or parsing bank data files.
* ``default`` -- This queue handles everything else with "medium" or unassigned priority, most prominently the generation of files for tickets, invoices, badges, admin exports, etc.
Media files
"""""""""""
Both ``pretix-web``, ``pretix-worker`` and in some cases your webserver need to work with
media files. Media files are all files generated *at runtime* by the software. This can
include files uploaded by the event organizers, such as the event logo, files uploaded by
ticket buyers (if you use such features) or files generated by the software, such as
ticket files, invoice PDFs, data exports or customized CSS files.
Those files are by default stored to the ``media/`` sub-folder of the data directory given
in the ``pretix.cfg`` configuration file. Inside that ``media/`` folder, you will find a
``pub/`` folder containing the subset of files that should be publicly accessible through
the web server. Everything else only needs to be accessible by ``pretix-web`` and
``pretix-worker`` themselves.
If you distribute ``pretix-web`` or ``pretix-worker`` across more than one machine, you
**must** make sure that they all have access to a shared storage to read and write these
files, otherwise you **will** run into errors with the user interface.
The easiest solution for this is probably to store them on a NFS server that you mount
on each of the other servers.
Since we use Django's file storage mechanism internally, you can in theory also use a object-storage solution like Amazon S3, Ceph, or Minio to store these files, although we currently do not expose this through pretix' configuration file and this would require you to ship your own variant of ``pretix/settings.py`` and reference it through the ``DJANGO_SETTINGS_MODULE`` environment variable.
At pretix.eu, we use a custom-built `object storage cluster`_.
SQL database
""""""""""""
One of the most critical parts of the whole setup is the SQL database -- and certainly the
hardest to scale. Tuning relational databases is an art form, and while there's lots of
material on it on the internet, there's not a single recipe that you can apply to every case.
As a general rule of thumb, the more resources you can give your databases, the better.
Most databases will happily use all CPU cores available, but only use memory up to an amount
you configure, so make sure to set this memory usage as high as you can afford. Having more
memory available allows your database to make more use of caching, which is usually good.
Scaling your database to multiple machines needs to be treated with great caution. It's a
good to have a replica of your database for availability reasons. In case your primary
database server fails, you can easily switch over to the replica and continue working.
However, using database replicas for performance gains is much more complicated. When using
replicated database systems, you are always trading in consistency or availability to get
additional performance and the consequences of this can be subtle and it is important
that you have a deep understanding of the semantics of your replication mechanism.
.. warning::
Using an off-the-shelf database proxy solution that redirects read queries to your
replicas and write queries to your primary database **will lead to very nasty bugs.**
As an example, if you buy a ticket, pretix first needs to calculate how many tickets
are left to sell. If this calculation is done on a database replica that lags behind
even for fractions of a second, the decision to allow selling the ticket will be made
on out-of-data data and you can end up with more tickets sold than configured. Similarly,
you could imagine situations leading to double payments etc.
If you do have a replica, you *can* tell pretix about it :ref:`in your configuration <config-replica>`.
This way, pretix can offload complex read-only queries to the replica when it is safe to do so.
As of pretix 2.6, this is mainly used for search queries in the backend and therefore does not really accelerate sales throughput, but we plan on expanding this in the future.
Therefore, for now our clear recommendation is: Try to scale your database vertically and put
it on the most powerful machine you have available.
redis
"""""
While redis is a very important part that glues together some of the components, it isn't used
heavily and can usually handle a fairly large pretix installation easily on a single modern
CPU core.
Having some memory available is good in case of e.g. lots of tasks queuing up during a traffic peak, but we wouldn't expect ever needing more than a gigabyte of it.
Feel free to set up a redis cluster for availability but you won't need it for performance in a long time.
The locking issue
-----------------
Currently, there is one big issue with scaling pretix. We take the reliability and correctness
of pretix' core features very seriously and one part of this is that we want to make absolutely
sure that pretix never sells the wrong number of tickets or tickets it otherwise shouldn't sell.
However, due to pretix flexibility and complexity, ensuring this in a high-concurrency environment is really hard.
We're not just counting down integers, since quota availability in pretix depends on a multitude of factors and requires a handful of complex database queries to calculate.
We can't rely on database-level locking alone to get this right.
Therefore, we currently use a very drastic option:
During relevant actions that typically occur with high concurrency, such as creating carts and completing orders, we create a system-wide lock for the event and all other such
actions need to wait. In essence, this means **for a given event we're only ever selling
one ticket at a time**.
Therefore, it is currently very unlikely that you will be able to exceed **300-400 tickets per minute per event**.
For events up to a few thousand tickets, this isn't a problem and it's not even desirable to be able to sell much faster: If all tickets are reserved in the first minute, the shop shows a big "sold out" and everyone else goes away. Fifteen to thirty minutes later, depending on your settings, the first shopping carts will expire because people didn't actually go through with the purchase and tickets will become available again. This leads to a much more frustrating shopping experience for those trying to get a ticket than if tickets are sold at a slower pace and the first reservations expire before the last reservations are made. Not selling tickets through quickly (e.g. through a queue system) can do a lot to smooth out this process.
That said, we still want to fix this of course and make it possible to achieve much higher
throughput rates. We have some plans on how to soften this limitation, but they require
lots of time and effort to be realized. If you want to use pretix for an event with
10,000+ tickets that will be sold out within minutes, get in touch, we will make it work ;)
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/

View File

@@ -336,11 +336,24 @@ Order position endpoints
The order positions endpoint has been extended by the filter queries ``voucher`` and ``voucher__code``.
.. versionchanged:: 2.7
The resource now contains the new attributes ``require_attention`` and ``order__status`` and accepts the new
``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint
returns ``400`` instead of ``404`` on tickets which are known but not paid.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
Returns a list of all order positions within a given event. The result is the same as
the :ref:`order-position-resource`, with one important difference: the ``checkins`` value will only include
check-ins for the selected list.
the :ref:`order-position-resource`, with the following differences:
* The ``checkins`` value will only include check-ins for the selected list.
* An additional boolean property ``require_attention`` will inform you whether either the order or the item
have the ``checkin_attention`` flag set.
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
addresses.
**Example request**:
@@ -407,6 +420,8 @@ Order position endpoints
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ignore_status: If set to ``true``, results will be returned regardless of the state of
the order they belong to and you will need to do your own filtering by order status.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``order__code``,
``order__datetime``, ``positionid``, ``attendee_name``, ``last_checked_in`` and ``order__email``. Default:
``attendee_name,positionid``
@@ -442,8 +457,15 @@ Order position endpoints
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)/
Returns information on one order position, identified by its internal ID.
The result format is the same as the :ref:`order-position-resource`, with one important difference: the
``checkins`` value will only include check-ins for the selected list.
The result is the same as the :ref:`order-position-resource`, with the following differences:
* The ``checkins`` value will only include check-ins for the selected list.
* An additional boolean property ``require_attention`` will inform you whether either the order or the item
have the ``checkin_attention`` flag set.
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
addresses.
**Instead of an ID, you can also use the ``secret`` field as the lookup parameter.**
@@ -528,7 +550,8 @@ Order position endpoints
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
questions that have not been filled. Defaults to ``false``.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false``.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
list.
:<json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
this request twice with the same nonce, the second request will also succeed but will always
create only one check-in object even when the previous request was successful as well. This

View File

@@ -18,12 +18,18 @@ default_price money (string) The price set d
price money (string) The price used for this variation. This is either the
same as ``default_price`` if that value is set or equal
to the item's ``default_price`` (read-only).
original_price money (string) An original price, shown for comparison, not used
for price calculations (or ``null``).
active boolean If ``false``, this variation will not be sold or shown.
description multi-lingual string A public description of the variation. May contain
Markdown syntax or can be ``null``.
position integer An integer, used for sorting
===================================== ========================== =======================================================
.. versionchanged:: 2.7
The attribute ``original_price`` has been added.
.. versionchanged:: 1.12
This resource has been added.
@@ -67,7 +73,8 @@ Endpoints
},
"position": 0,
"default_price": "223.00",
"price": 223.0
"price": 223.0,
"original_price": null,
},
{
"id": 3,
@@ -120,6 +127,7 @@ Endpoints
},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -167,6 +175,7 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -216,6 +225,7 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": false,
"description": null,
"position": 1

View File

@@ -29,7 +29,8 @@ free_price boolean If ``true``, cu
they buy the product (however, the price can't be set
lower than the price defined by ``default_price`` or
otherwise).
tax_rate decimal (string) The VAT rate to be applied for this item.
tax_rate decimal (string) The VAT rate to be applied for this item (read-only,
set through ``tax_rule``).
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
@@ -81,6 +82,8 @@ variations list of objects A list with one
├ price money (string) The price used for this variation. This is either the
same as ``default_price`` if that value is set or equal
to the item's ``default_price``.
├ original_price money (string) An original price, shown for comparison, not used
for price calculations (or ``null``).
├ active boolean If ``false``, this variation will not be sold or shown.
├ description multi-lingual string A public description of the variation. May contain
Markdown syntax or can be ``null``.
@@ -105,6 +108,10 @@ bundles list of objects Definition of b
taxation. This is not added to the price.
===================================== ========================== =======================================================
.. versionchanged:: 2.7
The attribute ``original_price`` has been added for ``variations``.
.. versionchanged:: 1.7
The attribute ``tax_rule`` has been added. ``tax_rate`` is kept for compatibility. The attribute
@@ -207,6 +214,7 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -215,6 +223,7 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1
@@ -296,6 +305,7 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -304,6 +314,7 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1
@@ -365,6 +376,7 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -373,6 +385,7 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1
@@ -423,6 +436,7 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -431,6 +445,7 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1
@@ -512,6 +527,7 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -520,6 +536,7 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1

View File

@@ -20,6 +20,8 @@ name multi-lingual string The sub-event's
event string The slug of the parent event
active boolean If ``true``, the sub-event ticket shop is publicly
available.
is_public boolean If ``true``, the sub-event ticket shop is publicly
shown in lists.
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``)
@@ -48,6 +50,10 @@ meta_data dict Values set for
.. versionchanged:: 2.6
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 2.7
The attribute ``is_public`` has been added.
Endpoints
---------
@@ -81,6 +87,7 @@ Endpoints
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -128,6 +135,7 @@ Endpoints
{
"name": {"en": "First Sample Conference"},
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -157,6 +165,7 @@ Endpoints
"id": 1,
"name": {"en": "First Sample Conference"},
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -207,6 +216,7 @@ Endpoints
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -270,6 +280,7 @@ Endpoints
"name": {"en": "New Subevent Name"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -353,6 +364,7 @@ Endpoints
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,

View File

@@ -15,6 +15,7 @@ boolean
booleans
cancelled
casted
Ceph
checkbox
checksum
config
@@ -53,6 +54,7 @@ linters
memcached
metadata
middleware
Minio
mixin
mixins
multi
@@ -124,6 +126,7 @@ unconfigured
unix
unprefixed
untrusted
uptime
username
url
versa

View File

@@ -1,7 +1,8 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.api.models import OAuthAccessToken
from pretix.base.models import Device, Event
from pretix.base.models import Device, Event, User
from pretix.base.models.auth import SuperuserPermissionSet
from pretix.base.models.organizer import Organizer, TeamAPIToken
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
@@ -37,10 +38,13 @@ class EventPermission(BasePermission):
slug=request.resolver_match.kwargs['event'],
organizer__slug=request.resolver_match.kwargs['organizer'],
).select_related('organizer').first()
if not request.event or not perm_holder.has_event_permission(request.event.organizer, request.event):
if not request.event or not perm_holder.has_event_permission(request.event.organizer, request.event, request=request):
return False
request.organizer = request.event.organizer
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.eventpermset = SuperuserPermissionSet()
else:
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
if required_permission and required_permission not in request.eventpermset:
return False
@@ -49,9 +53,12 @@ class EventPermission(BasePermission):
request.organizer = Organizer.objects.filter(
slug=request.resolver_match.kwargs['organizer'],
).first()
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer):
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer, request=request):
return False
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.orgapermset = SuperuserPermissionSet()
else:
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
if required_permission and required_permission not in request.orgapermset:
return False

View File

@@ -21,15 +21,15 @@ class IdempotencyMiddleware:
if not request.path.startswith('/api/'):
return self.get_response(request)
if not request.META.get('HTTP_X_IDEMPOTENCY_KEY'):
if not request.headers.get('X-Idempotency-Key'):
return self.get_response(request)
auth_hash_parts = '{}:{}'.format(
request.META.get('HTTP_AUTHORIZATION', ''),
request.headers.get('Authorization', ''),
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
)
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
idempotency_key = request.META.get('HTTP_X_IDEMPOTENCY_KEY', '')
idempotency_key = request.headers.get('X-Idempotency-Key', '')
with transaction.atomic():
call, created = ApiCall.objects.select_for_update().get_or_create(

View File

@@ -77,6 +77,9 @@ class WebHook(models.Model):
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
class Meta:
ordering = ('id',)
@property
def action_types(self):
return [

View File

@@ -199,7 +199,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'event',
'presale_start', 'presale_end', 'location', 'event', 'is_public',
'item_price_overrides', 'variation_price_overrides', 'meta_data')
def validate(self, data):
@@ -212,8 +212,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
Event.clean_dates(data.get('date_from'), data.get('date_to'))
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
SubEvent.clean_items(event, [item['item'] for item in full_data.get('subeventitem_set')])
SubEvent.clean_variations(event, [item['variation'] for item in full_data.get('subeventitemvariation_set')])
SubEvent.clean_items(event, [item['item'] for item in full_data.get('subeventitem_set', [])])
SubEvent.clean_variations(event, [item['variation'] for item in full_data.get('subeventitemvariation_set', [])])
return data
def validate_item_price_overrides(self, data):

View File

@@ -19,7 +19,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price')
'position', 'default_price', 'price', 'original_price')
class ItemVariationSerializer(I18nAwareModelSerializer):
@@ -29,7 +29,7 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price')
'position', 'default_price', 'price', 'original_price')
class InlineItemBundleSerializer(serializers.ModelSerializer):

View File

@@ -14,8 +14,8 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question, QuestionAnswer,
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, SubEvent,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund,
@@ -179,6 +179,55 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
self.fields.pop('pdf_data')
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
return instance.order.checkin_attention or instance.item.checkin_attention
class AttendeeNameField(serializers.Field):
def to_representation(self, instance: OrderPosition):
an = instance.attendee_name
if not an:
if instance.addon_to_id:
an = instance.addon_to.attendee_name
if not an:
try:
an = instance.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return an
class AttendeeNamePartsField(serializers.Field):
def to_representation(self, instance: OrderPosition):
an = instance.attendee_name
p = instance.attendee_name_parts
if not an:
if instance.addon_to_id:
an = instance.addon_to.attendee_name
p = instance.addon_to.attendee_name_parts
if not an:
try:
p = instance.order.invoice_address.name_parts
except InvoiceAddress.DoesNotExist:
pass
return p
class CheckinListOrderPositionSerializer(OrderPositionSerializer):
require_attention = RequireAttentionField(source='*')
attendee_name = AttendeeNameField(source='*')
attendee_name_parts = AttendeeNamePartsField(source='*')
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'require_attention',
'order__status')
class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2
def to_representation(self, instance: Order):
@@ -288,6 +337,23 @@ class OrderSerializer(I18nAwareModelSerializer):
return instance
class PriceCalcSerializer(serializers.Serializer):
item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
locale = serializers.CharField(allow_null=True, required=False)
def __init__(self, *args, **kwargs):
event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['item'].queryset = event.items.all()
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=event)
if event.has_subevents:
self.fields['subevent'].queryset = event.subevents.all()
else:
del self.fields['subevent']
class AnswerCreateSerializer(I18nAwareModelSerializer):
class Meta:

View File

@@ -23,4 +23,4 @@ def cleanup_webhook_logs(sender, **kwargs):
@receiver(periodic_task)
def cleanup_api_logs(sender, **kwargs):
ApiCall.objects.filter(datetime__lte=now() - timedelta(hours=24)).delete()
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()

View File

@@ -31,10 +31,10 @@ class RichOrderingFilter(OrderingFilter):
class ConditionalListView:
def list(self, request, **kwargs):
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
if_modified_since = request.headers.get('If-Modified-Since')
if if_modified_since:
if_modified_since = parse_http_date_safe(if_modified_since)
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
if_unmodified_since = request.headers.get('If-Unmodified-Since')
if if_unmodified_since:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
if not hasattr(request, 'event'):

View File

@@ -7,13 +7,13 @@ from django.utils.functional import cached_property
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.decorators import action
from rest_framework.fields import DateTimeField
from rest_framework.response import Response
from pretix.api.serializers.checkin import CheckinListSerializer
from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.serializers.order import CheckinListOrderPositionSerializer
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.models import (
@@ -77,7 +77,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
)
super().perform_destroy(instance)
@detail_route(methods=['GET'])
@action(detail=True, methods=['GET'])
def status(self, *args, **kwargs):
clist = self.get_object()
cqs = Checkin.objects.filter(
@@ -153,7 +153,7 @@ class CheckinOrderPositionFilter(OrderPositionFilter):
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
serializer_class = CheckinListOrderPositionSerializer
queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('attendee_name_cached', 'positionid')
@@ -189,7 +189,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
except ValueError:
raise Http404()
def get_queryset(self):
def get_queryset(self, ignore_status=False):
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.checkinlist.pk
@@ -199,11 +199,15 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
qs = OrderPosition.objects.filter(
order__event=self.request.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID],
subevent=self.checkinlist.subevent
).annotate(
last_checked_in=Subquery(cqs)
)
if self.request.query_params.get('ignore_status', 'false') != 'true' and not ignore_status:
qs = qs.filter(
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID]
)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
@@ -225,7 +229,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to'
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address'
)
else:
qs = qs.prefetch_related(
@@ -235,19 +239,19 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address')
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order')
if not self.checkinlist.all_products:
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
return qs
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def redeem(self, *args, **kwargs):
force = bool(self.request.data.get('force', False))
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
nonce = self.request.data.get('nonce')
op = self.get_object()
op = self.get_object(ignore_status=True)
if 'datetime' in self.request.data:
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
@@ -281,7 +285,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
return Response({
'status': 'incomplete',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
]
@@ -291,17 +295,17 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
'status': 'error',
'reason': e.code,
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=400)
else:
return Response({
'status': 'ok',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
def get_object(self, ignore_status=False):
queryset = self.filter_queryset(self.get_queryset(ignore_status=ignore_status))
if self.kwargs['pk'].isnumeric():
obj = get_object_or_404(queryset, Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:

View File

@@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
@@ -499,7 +499,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
)
super().perform_destroy(instance)
@detail_route(methods=['get'])
@action(detail=True, methods=['get'])
def availability(self, request, *args, **kwargs):
quota = self.get_object()

View File

@@ -12,7 +12,7 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import detail_route
from rest_framework.decorators import action
from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError,
)
@@ -24,14 +24,16 @@ from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.order import (
InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer,
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
)
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, Order,
OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
Order, OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
generate_position_secret, generate_secret,
)
from pretix.base.payment import PaymentException
from pretix.base.services import tickets
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice,
@@ -41,10 +43,12 @@ from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
)
from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate
from pretix.base.signals import (
order_modified, order_placed, register_ticket_outputs,
)
from pretix.base.templatetags.money import money_filter
class OrderFilter(FilterSet):
@@ -127,7 +131,7 @@ class OrderViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, headers={'X-Page-Generated': date})
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)
order = self.get_object()
@@ -149,7 +153,7 @@ class OrderViewSet(viewsets.ModelViewSet):
)
return resp
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
order = self.get_object()
@@ -190,7 +194,7 @@ class OrderViewSet(viewsets.ModelViewSet):
status=status.HTTP_400_BAD_REQUEST
)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_canceled(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
cancellation_fee = request.data.get('cancellation_fee', None)
@@ -224,7 +228,7 @@ class OrderViewSet(viewsets.ModelViewSet):
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def approve(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
@@ -242,7 +246,7 @@ class OrderViewSet(viewsets.ModelViewSet):
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def deny(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
comment = request.data.get('comment', '')
@@ -260,7 +264,7 @@ class OrderViewSet(viewsets.ModelViewSet):
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_pending(self, request, **kwargs):
order = self.get_object()
@@ -279,7 +283,7 @@ class OrderViewSet(viewsets.ModelViewSet):
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_expired(self, request, **kwargs):
order = self.get_object()
@@ -296,7 +300,7 @@ class OrderViewSet(viewsets.ModelViewSet):
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_refunded(self, request, **kwargs):
order = self.get_object()
@@ -313,7 +317,7 @@ class OrderViewSet(viewsets.ModelViewSet):
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def create_invoice(self, request, **kwargs):
order = self.get_object()
has_inv = order.invoices.exists() and not (
@@ -345,7 +349,7 @@ class OrderViewSet(viewsets.ModelViewSet):
status=status.HTTP_201_CREATED
)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def resend_link(self, request, **kwargs):
order = self.get_object()
if not order.email:
@@ -359,7 +363,7 @@ class OrderViewSet(viewsets.ModelViewSet):
status=status.HTTP_204_NO_CONTENT
)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
@transaction.atomic
def regenerate_secrets(self, request, **kwargs):
order = self.get_object()
@@ -370,6 +374,8 @@ class OrderViewSet(viewsets.ModelViewSet):
order.save(update_fields=['secret'])
CachedTicket.objects.filter(order_position__order=order).delete()
CachedCombinedTicket.objects.filter(order=order).delete()
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk,
'order': order.pk})
order.log_action(
'pretix.event.order.secret.changed',
user=self.request.user,
@@ -377,7 +383,7 @@ class OrderViewSet(viewsets.ModelViewSet):
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def extend(self, request, **kwargs):
new_date = request.data.get('expires', None)
force = request.data.get('force', False)
@@ -508,6 +514,7 @@ class OrderViewSet(viewsets.ModelViewSet):
)
serializer.save()
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.event.pk, 'order': serializer.instance.pk})
if 'invoice_address' in self.request.data:
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
@@ -619,7 +626,84 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
return prov
raise NotFound('Unknown output provider.')
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
@action(detail=True, methods=['POST'], url_name='price_calc')
def price_calc(self, request, *args, **kwargs):
"""
This calculates the price assuming a change of product or subevent. This endpoint
is deliberately not documented and considered a private API, only to be used by
pretix' web interface.
Sample input:
{
"item": 2,
"variation": null,
"subevent": 3
}
Sample output:
{
"gross": "2.34",
"gross_formatted": "2,34",
"net": "2.34",
"tax": "0.00",
"rate": "0.00",
"name": "VAT"
}
"""
serializer = PriceCalcSerializer(data=request.data, event=request.event)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
pos = self.get_object()
try:
ia = pos.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = InvoiceAddress()
kwargs = {
'item': pos.item,
'variation': pos.variation,
'voucher': pos.voucher,
'subevent': pos.subevent,
'addon_to': pos.addon_to,
'invoice_address': ia,
}
if data.get('item'):
item = data.get('item')
kwargs['item'] = item
if item.has_variations:
variation = data.get('variation') or pos.variation
if not variation:
raise ValidationError('No variation given')
if variation.item != item:
raise ValidationError('Variation does not belong to item')
kwargs['variation'] = variation
else:
variation = None
kwargs['variation'] = None
if pos.voucher and not pos.voucher.applies_to(item, variation):
kwargs['voucher'] = None
if data.get('subevent'):
kwargs['subevent'] = data.get('subevent')
price = get_price(**kwargs)
with language(data.get('locale') or self.request.event.settings.locale):
return Response({
'gross': price.gross,
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),
'net': price.net,
'rate': price.rate,
'name': str(price.name),
'tax': price.tax,
})
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)
pos = self.get_object()
@@ -670,7 +754,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
return order.payments.all()
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def confirm(self, request, **kwargs):
payment = self.get_object()
force = request.data.get('force', False)
@@ -691,7 +775,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
pass
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def refund(self, request, **kwargs):
payment = self.get_object()
amount = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
@@ -756,7 +840,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
payment.order.save(update_fields=['status', 'expires'])
return Response(OrderRefundSerializer(r).data, status=status.HTTP_200_OK)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def cancel(self, request, **kwargs):
payment = self.get_object()
@@ -784,7 +868,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
return order.refunds.all()
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def cancel(self, request, **kwargs):
refund = self.get_object()
@@ -801,7 +885,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def process(self, request, **kwargs):
refund = self.get_object()
@@ -826,7 +910,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
refund.order.save(update_fields=['status', 'expires'])
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def done(self, request, **kwargs):
refund = self.get_object()
@@ -916,7 +1000,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
nr=Concat('prefix', 'invoice_no')
)
@detail_route()
@action(detail=True, )
def download(self, request, **kwargs):
invoice = self.get_object()
@@ -934,7 +1018,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
return resp
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def regenerate(self, request, **kwarts):
inv = self.get_object()
if inv.canceled:
@@ -953,7 +1037,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
)
return Response(status=204)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def reissue(self, request, **kwarts):
inv = self.get_object()
if inv.canceled:

View File

@@ -7,7 +7,7 @@ from django_filters.rest_framework import (
BooleanFilter, DjangoFilterBackend, FilterSet,
)
from rest_framework import status, viewsets
from rest_framework.decorators import list_route
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
@@ -116,7 +116,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
instance.cartposition_set.all().delete()
super().perform_destroy(instance)
@list_route(methods=['POST'])
@action(detail=False, methods=['POST'])
def batch_create(self, request, *args, **kwargs):
if any(self._predict_quota_check(d, None) for d in request.data):
lockfn = request.event.lock

View File

@@ -1,7 +1,7 @@
import django_filters
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
@@ -69,7 +69,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
)
super().perform_destroy(instance)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def send_voucher(self, *args, **kwargs):
try:
self.get_object().send_voucher(

View File

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

View File

@@ -0,0 +1,219 @@
import json
from collections import OrderedDict
from decimal import Decimal
import dateutil
from django import forms
from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver
from django.utils.translation import ugettext, ugettext_lazy
from pretix.base.i18n import language
from pretix.base.models import Invoice, OrderPayment
from ..exporter import BaseExporter
from ..signals import register_data_exporters
class DekodiNREIExporter(BaseExporter):
identifier = 'dekodi_nrei'
verbose_name = 'dekodi NREI (JSON)'
# Specification: http://manuals.dekodi.de/nexuspub/schnittstellenbuch/
def _encode_invoice(self, invoice: Invoice):
p_last = invoice.order.payments.filter(state=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED]).last()
gross_total = Decimal('0.00')
net_total = Decimal('0.00')
positions = []
for l in invoice.lines.all():
positions.append({
'ADes': l.description.replace("<br />", "\n"),
'ANetA': round(float(l.net_value), 2),
'ANo': self.event.slug,
'AQ': -1 if invoice.is_cancellation else 1,
'AVatP': round(float(l.tax_rate), 2),
'DIDt': (l.subevent or invoice.order.event).date_from.isoformat().replace('Z', '+00:00'),
'PosGrossA': round(float((-1 if invoice.is_cancellation else 1) * l.gross_value), 2),
'PosNetA': round(float((-1 if invoice.is_cancellation else 1) * l.net_value), 2),
})
gross_total += l.gross_value
net_total += l.net_value
payments = []
paypal_email = None
for p in invoice.order.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_REFUNDED)
):
if p.provider == 'paypal':
paypal_email = p.info_data.get('payer', {}).get('payer_info', {}).get('email')
try:
ppid = p.info_data['transactions'][0]['related_resources']['sale']['id']
except:
ppid = p.info_data.get('id')
payments.append({
'PTID': '1',
'PTN': 'PayPal',
'PTNo1': ppid,
'PTNo2': p.info_data.get('id'),
'PTNo7': round(float(p.amount), 2),
'PTNo8': str(self.event.currency),
'PTNo11': paypal_email or '',
'PTNo15': p.full_id or '',
})
elif p.provider == 'banktransfer':
payments.append({
'PTID': '4',
'PTN': 'Vorkasse',
'PTNo4': p.info_data.get('reference') or p.payment_provider._code(invoice.order),
'PTNo7': round(float(p.amount), 2),
'PTNo8': str(self.event.currency),
'PTNo10': p.info_data.get('payer') or '',
'PTNo14': p.info_data.get('date') or '',
'PTNo15': p.full_id or '',
})
elif p.provider == 'sepadebit':
with language(invoice.order.locale):
payments.append({
'PTID': '5',
'PTN': 'Lastschrift',
'PTNo4': ugettext('Event ticket {event}-{code}').format(
event=self.event.slug.upper(),
code=invoice.order.code
),
'PTNo5': p.info_data.get('iban') or '',
'PTNo6': p.info_data.get('bic') or '',
'PTNo7': round(float(p.amount), 2),
'PTNo8': str(self.event.currency) or '',
'PTNo9': p.info_data.get('date') or '',
'PTNo10': p.info_data.get('account') or '',
'PTNo14': p.info_data.get('reference') or '',
'PTNo15': p.full_id or '',
})
elif p.provider.startswith('stripe'):
src = p.info_data.get("source", "{}")
payments.append({
'PTID': '81',
'PTN': 'Stripe',
'PTNo1': p.info_data.get("id") or '',
'PTNo5': src.get("card", {}).get("last4") or '',
'PTNo7': round(float(p.amount), 2) or '',
'PTNo8': str(self.event.currency) or '',
'PTNo10': src.get('owner', {}).get('verified_name') or src.get('owner', {}).get('name') or '',
'PTNo15': p.full_id or '',
})
else:
payments.append({
'PTID': '0',
'PTN': p.provider,
'PTNo7': round(float(p.amount), 2) or '',
'PTNo8': str(self.event.currency) or '',
'PTNo15': p.full_id or '',
})
payments = [
{
k: v for k, v in p.items() if v is not None
} for p in payments
]
hdr = {
'C': str(invoice.invoice_to_country) or self.event.settings.invoice_address_from_country,
'CC': self.event.currency,
'City': invoice.invoice_to_city,
'CN': invoice.invoice_to_company,
'DIC': self.event.settings.invoice_address_from_country,
# DIC is a little bit unclean, should be the event location's country
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
'DT': '30' if invoice.is_cancellation else '10',
'EM': invoice.order.email,
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1],
'FN': invoice.invoice_to_name.rsplit(' ', 1)[0] if ' ' in invoice.invoice_to_name else '',
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
'INo': invoice.full_invoice_no,
'IsNet': invoice.reverse_charge,
'ODt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
'OID': invoice.order.code,
'SID': self.event.slug,
'SN': str(self.event),
'Str': invoice.invoice_to_street or '',
'TGrossA': round(float(gross_total), 2),
'TNetA': round(float(net_total), 2),
'TVatA': round(float(gross_total - net_total), 2),
'VatDp': False,
'Zip': invoice.invoice_to_zipcode
}
if not hdr['FamN'] and not hdr['CN']:
hdr['CN'] = "Unbekannter Kunde"
if invoice.refers:
hdr['PvrINo'] = invoice.refers.full_invoice_no
if p_last:
hdr['PmDt'] = p_last.payment_date.isoformat().replace('Z', '+00:00')
if paypal_email:
hdr['PPEm'] = paypal_email
if invoice.invoice_to_vat_id:
hdr['VatID'] = invoice.invoice_to_vat_id
return {
'IsValid': True,
'Hdr': hdr,
'InvcPstns': positions,
'PmIs': payments,
'ValidationMessage': ''
}
def render(self, form_data):
qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent')
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__gte=date_value)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
jo = {
'Format': 'NREI',
'Version': '18.10.2.0',
'SourceSystem': 'pretix',
'Data': [
self._encode_invoice(i) for i in qs
]
}
return '{}_nrei.json'.format(self.event.slug), 'application/json', json.dumps(jo, cls=DjangoJSONEncoder, indent=4)
@property
def export_form_fields(self):
return OrderedDict(
[
('date_from',
forms.DateField(
label=ugettext_lazy('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=ugettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.')
)),
('date_to',
forms.DateField(
label=ugettext_lazy('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=ugettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
]
)
@receiver(register_data_exporters, dispatch_uid="exporter_dekodi_nrei")
def register_dekodi_export(sender, **kwargs):
return DekodiNREIExporter

View File

@@ -6,7 +6,7 @@ from django import forms
from django.db.models import DateTimeField, F, Max, OuterRef, Subquery, Sum
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
from pretix.base.models import (
InvoiceAddress, InvoiceLine, Order, OrderPosition,
@@ -269,6 +269,10 @@ class OrderListExporter(MultiSheetListExporter):
_('Status'),
_('Email'),
_('Order date'),
]
if self.event.has_subevents:
headers.append(pgettext('subevent', 'Date'))
headers += [
_('Product'),
_('Variation'),
_('Price'),
@@ -311,6 +315,10 @@ class OrderListExporter(MultiSheetListExporter):
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
]
if self.event.has_subevents:
row.append(op.subevent)
row += [
str(op.item),
str(op.variation) if op.variation else '',
op.price,

View File

@@ -9,14 +9,17 @@ 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 get_language, pgettext, ugettext
from django.utils.translation import (
get_language, pgettext, ugettext, ugettext_lazy,
)
from PIL.Image import BICUBIC
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.enums import TA_LEFT, TA_RIGHT
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.pdfmetrics import stringWidth
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import (
@@ -122,6 +125,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='InvoiceFrom', parent=stylesheet['Normal']))
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10))
@@ -254,49 +258,64 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.restoreState()
invoice_to_width = 85 * mm
invoice_to_height = 50 * mm
invoice_to_left = 25 * mm
invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas):
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])
p = Paragraph(self.invoice.address_invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
invoice_from_width = 70 * mm
invoice_from_height = 50 * mm
invoice_from_left = 25 * mm
invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas):
p = Paragraph(self.invoice.full_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])
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(self.font_regular, 8)
if self.invoice.order.testmode:
canvas.saveState()
canvas.setFont('OpenSansBd', 30)
canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
canvas.restoreState()
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())
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet[
'InvoiceFrom'])
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
p_size = p.wrap(self.invoice_from_width, self.invoice_from_height)
p.drawOn(canvas, self.invoice_from_left, self.pagesize[1] - p_size[1] - self.invoice_from_top)
def _draw_invoice_from_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Invoice from')))
canvas.drawText(textobject)
self._draw_invoice_from(canvas)
def _draw_invoice_to_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Invoice to')))
canvas.drawText(textobject)
self._draw_invoice_to(canvas)
logo_width = 25 * mm
logo_height = 25 * mm
logo_left = 95 * mm
logo_top = 13 * mm
logo_anchor = 'n'
def _draw_logo(self, canvas):
if self.invoice.event.settings.invoice_logo_image:
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
ir = ThumbnailingImageReader(logo_file)
try:
ir.resize(self.logo_width, self.logo_height, 300)
except:
logger.exception("Can not resize image")
pass
canvas.drawImage(ir,
self.logo_left,
self.pagesize[1] - self.logo_height - self.logo_top,
width=self.logo_width, height=self.logo_height,
preserveAspectRatio=True, anchor=self.logo_anchor,
mask='auto')
def _draw_metadata(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Order code')))
@@ -348,37 +367,37 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.drawText(textobject)
if self.invoice.event.settings.invoice_logo_image:
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
ir = ThumbnailingImageReader(logo_file)
try:
ir.resize(25 * mm, 25 * mm, 300)
except:
logger.exception("Can not resize image")
pass
canvas.drawImage(ir,
95 * mm, (297 - 38) * mm,
width=25 * mm, height=25 * mm,
preserveAspectRatio=True, anchor='n',
mask='auto')
event_left = 125 * mm
event_top = 17 * mm
event_width = 65 * mm
event_height = 50 * mm
def _draw_event_label(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
canvas.drawText(textobject)
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(65 * mm, 50 * mm)
p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(65 * mm, 50 * mm)
p_size = p.wrap(self.event_width, self.event_height)
return txt
if not self.invoice.event.has_subevents:
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
p_str = (
shorten(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
shorten(self.invoice.event.name) + '\n' +
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
from_date=self.invoice.event.get_date_from_display(),
to_date=self.invoice.event.get_date_to_display())
to_date=self.invoice.event.get_date_to_display()
)
)
else:
p_str = (
@@ -388,15 +407,38 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
p_str = shorten(self.invoice.event.name)
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])
p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
self._draw_event_label(canvas)
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
canvas.drawText(textobject)
def _draw_footer(self, canvas):
canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
def _draw_testmode(self, canvas):
if self.invoice.order.testmode:
canvas.saveState()
canvas.setFont('OpenSansBd', 30)
canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
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()
self._draw_footer(canvas)
self._draw_testmode(canvas)
self._draw_invoice_from_label(canvas)
self._draw_invoice_from(canvas)
self._draw_invoice_to_label(canvas)
self._draw_invoice_to(canvas)
self._draw_metadata(canvas)
self._draw_logo(canvas)
self._draw_event(canvas)
canvas.restoreState()
def _get_first_page_frames(self, doc):
@@ -434,6 +476,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
self.stylesheet['Normal']
))
if self.invoice.invoice_to_vat_id:
story.append(Paragraph(
pgettext('invoice', 'Customer VAT ID') + ':<br />' +
bleach.clean(self.invoice.invoice_to_vat_id, tags=[]).replace("\n", "<br />\n"),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(Paragraph(
pgettext('invoice', 'Beneficiary') + ':<br />' +
@@ -554,6 +603,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
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(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table
@@ -607,6 +657,114 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return story
class Modern1Renderer(ClassicInvoiceRenderer):
identifier = 'modern1'
verbose_name = ugettext_lazy('Modern Invoice Renderer (pretix 2.7)')
bottom_margin = 16.9 * mm
top_margin = 16.9 * mm
right_margin = 20 * mm
invoice_to_height = 27.3 * mm
invoice_to_width = 80 * mm
invoice_to_left = 25 * mm
invoice_to_top = (40 + 17.7) * mm
invoice_from_left = 125 * mm
invoice_from_top = 50 * mm
invoice_from_width = pagesizes.A4[0] - invoice_from_left - right_margin
invoice_from_height = 50 * mm
logo_width = 75 * mm
logo_height = 25 * mm
logo_left = pagesizes.A4[0] - logo_width - right_margin
logo_top = top_margin
logo_anchor = 'e'
event_left = 25 * mm
event_top = top_margin
event_width = 80 * mm
event_height = 25 * mm
def _get_stylesheet(self):
stylesheet = super()._get_stylesheet()
stylesheet.add(ParagraphStyle(name='Sender', fontName=self.font_regular, fontSize=8, leading=10))
stylesheet['InvoiceFrom'].alignment = TA_RIGHT
return stylesheet
def _draw_invoice_from(self, canvas):
if not self.invoice.invoice_from:
return
c = self.invoice.address_invoice_from.strip().split('\n')
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas)
def _draw_invoice_to_label(self, canvas):
pass
def _draw_invoice_from_label(self, canvas):
pass
def _draw_event_label(self, canvas):
pass
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 - 95 * mm,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
def _draw_metadata(self, canvas):
begin_top = 100 * mm
textobject = canvas.beginText(self.left_margin, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Order code'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.order.full_code)
canvas.drawText(textobject)
if self.invoice.is_cancellation:
textobject = canvas.beginText(self.left_margin + 50 * mm, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Cancellation number'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
canvas.drawText(textobject)
textobject = canvas.beginText(self.left_margin + 100 * mm, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Original invoice'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.refers.number)
canvas.drawText(textobject)
else:
textobject = canvas.beginText(self.left_margin + 70 * mm, self.pagesize[1] - begin_top)
textobject.textLine(pgettext('invoice', 'Invoice number'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
canvas.drawText(textobject)
p = Paragraph(date_format(self.invoice.date, "DATE_FORMAT"), style=self.stylesheet['Normal'])
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
p.wrapOn(canvas, w, 15 * mm)
date_x = self.pagesize[0] - w - self.right_margin
p.drawOn(canvas, date_x, self.pagesize[1] - begin_top - 10 - 6)
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
if self.invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation date'))
else:
textobject.textLine(pgettext('invoice', 'Invoice date'))
canvas.drawText(textobject)
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
def recv_classic(sender, **kwargs):
return ClassicInvoiceRenderer
return [ClassicInvoiceRenderer, Modern1Renderer]

View File

@@ -97,7 +97,7 @@ def get_language_from_event(request: HttpRequest) -> str:
def get_language_from_browser(request: HttpRequest) -> str:
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
accept = request.headers.get('Accept-Language', '')
for accept_lang, unused in parse_accept_lang_header(accept):
if accept_lang == '*':
break

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2 on 2019-04-18 11:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0116_auto_20190402_0722'),
]
operations = [
migrations.AddField(
model_name='itemvariation',
name='original_price',
field=models.DecimalField(blank=True, decimal_places=2, help_text='If set, this will be displayed next to '
'the current price to show that the '
'current price is a discounted one. '
'This is just a cosmetic setting and '
'will not actually impact pricing.',
max_digits=7, null=True, verbose_name='Original price'),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.2 on 2019-04-23 08:39
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0117_auto_20190418_1149'),
]
operations = [
migrations.AddField(
model_name='subevent',
name='is_public',
field=models.BooleanField(default=True, help_text='If selected, this event will show up publicly on the list of dates for your event.', verbose_name='Show in lists'),
),
]

View File

@@ -111,6 +111,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
class Meta:
verbose_name = _("User")
verbose_name_plural = _("Users")
ordering = ('email',)
def save(self, *args, **kwargs):
self.email = self.email.lower()

View File

@@ -639,22 +639,6 @@ class Event(EventMixin, LoggedModel):
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')
def subevents_annotated(self, channel):
return SubEvent.annotated(self.subevents, channel)
@@ -667,7 +651,7 @@ class Event(EventMixin, LoggedModel):
'name_descending': ('-name', 'date_from'),
}[ordering]
subevs = queryset.filter(
Q(active=True) & (
Q(active=True) & Q(is_public=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
| Q(date_to__gte=now() - timedelta(hours=24))
)
@@ -833,6 +817,8 @@ class SubEvent(EventMixin, LoggedModel):
:type event: Event
:param active: Whether to show the subevent
:type active: bool
:param is_public: Whether to show the subevent in lists
:type is_public: bool
:param name: This event's full title
:type name: str
:param date_from: The datetime this event starts
@@ -851,6 +837,10 @@ class SubEvent(EventMixin, LoggedModel):
active = models.BooleanField(default=False, verbose_name=_("Active"),
help_text=_("Only with this checkbox enabled, this date is visible in the "
"frontend to users."))
is_public = models.BooleanField(default=True,
verbose_name=_("Show in lists"),
help_text=_("If selected, this event will show up publicly on the list of dates "
"for your event."))
name = I18nCharField(
max_length=200,
verbose_name=_("Name"),

View File

@@ -37,7 +37,7 @@ class MultiStringField(TextField):
def get_prep_lookup(self, lookup_type, value): # NOQA
raise TypeError('Lookups on multi strings are currently not supported.')
def from_db_value(self, value, expression, connection, context):
def from_db_value(self, value, expression, connection):
if value:
return [v for v in value.split(DELIMITER) if v]
else:

View File

@@ -117,12 +117,35 @@ class Invoice(models.Model):
self.invoice_from_name,
self.invoice_from,
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
str(self.invoice_from_country),
self.invoice_from_country.name if self.invoice_from_country else "",
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id if self.invoice_from_tax_id else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])
@property
def address_invoice_from(self):
parts = [
self.invoice_from_name,
self.invoice_from,
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
self.invoice_from_country.name if self.invoice_from_country else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])
@property
def address_invoice_to(self):
if self.invoice_to and not self.invoice_to_company and not self.invoice_to_name:
return self.invoice_to
parts = [
self.invoice_to_company,
self.invoice_to_name,
self.invoice_to_street,
(self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or ""),
self.invoice_to_country.name if self.invoice_to_country else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])
def _get_numeric_invoice_number(self):
numeric_invoices = Invoice.objects.filter(
event__organizer=self.event.organizer,

View File

@@ -550,6 +550,8 @@ class ItemVariation(models.Model):
:type active: bool
:param default_price: This variation's default price
:type default_price: decimal.Decimal
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
:type original_price: decimal.Decimal
"""
item = models.ForeignKey(
Item,
@@ -578,6 +580,13 @@ class ItemVariation(models.Model):
null=True, blank=True,
verbose_name=_("Default price"),
)
original_price = models.DecimalField(
verbose_name=_('Original price'),
blank=True, null=True,
max_digits=7, decimal_places=2,
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
)
class Meta:
verbose_name = _("Product variation")
@@ -1391,7 +1400,7 @@ class Quota(LoggedModel):
@staticmethod
def clean_variations(items, variations):
for variation in variations:
for variation in (variations or []):
if variation.item not in items:
raise ValidationError(_('All variations must belong to an item contained in the items list.'))
break

View File

@@ -32,6 +32,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from .base import LockModel, LoggedModel
@@ -606,7 +607,7 @@ class Order(LockModel, LoggedModel):
), tz)
return term_last
def _can_be_paid(self, count_waitinglist=True) -> Union[bool, str]:
def _can_be_paid(self, count_waitinglist=True, ignore_date=False) -> Union[bool, str]:
error_messages = {
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
"payment settings is over."),
@@ -617,13 +618,13 @@ class Order(LockModel, LoggedModel):
if self.require_approval:
return error_messages['require_approval']
term_last = self.payment_term_last
if term_last:
if term_last and not ignore_date:
if now() > term_last:
return error_messages['late_lastdate']
if self.status == self.STATUS_PENDING:
return True
if not self.event.settings.get('payment_term_accept_late'):
if not self.event.settings.get('payment_term_accept_late') and not ignore_date:
return error_messages['late']
return self._is_still_available(count_waitinglist=count_waitinglist)
@@ -1137,9 +1138,9 @@ class OrderPayment(models.Model):
"""
return self.order.event.get_payment_providers().get(self.provider)
def _mark_paid(self, force, count_waitinglist, user, auth, overpaid=False):
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
from pretix.base.signals import order_paid
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist)
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date)
if not force and can_be_paid is not True:
self.order.log_action('pretix.event.order.quotaexceeded', {
'message': can_be_paid
@@ -1159,7 +1160,7 @@ class OrderPayment(models.Model):
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
order_paid.send(self.order.event, order=self.order)
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text=''):
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='', ignore_date=False, lock=True):
"""
Marks the payment as complete. If possible, this also marks the order as paid if no further
payment is required
@@ -1169,6 +1170,7 @@ class OrderPayment(models.Model):
:type count_waitinglist: boolean
:param force: Whether this payment should be marked as paid even if no remaining
quota is available (default: ``False``).
:param ignore_date: Whether this order should be marked as paid even when the last date of payments is over.
:type force: boolean
:param send_mail: Whether an email should be sent to the user about this event (default: ``True``).
:type send_mail: boolean
@@ -1218,14 +1220,16 @@ class OrderPayment(models.Model):
if payment_sum - refund_sum < self.order.total:
return
if self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12):
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
# database transaction is more than enough.
with transaction.atomic():
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total)
lockfn = NoLockManager
else:
with self.order.event.lock():
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total)
lockfn = self.order.event.lock
with lockfn():
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total,
ignore_date=ignore_date)
invoice = None
if invoice_qualified(self.order):

View File

@@ -118,6 +118,9 @@ class TaxRule(LoggedModel):
)
custom_rules = models.TextField(blank=True, null=True)
class Meta:
ordering = ('event', 'rate', 'id')
def allow_delete(self):
from pretix.base.models.orders import OrderFee, OrderPosition

View File

@@ -344,6 +344,8 @@ class Voucher(LoggedModel):
a variation).
"""
if self.quota_id:
if variation:
return variation.quotas.filter(pk=self.quota_id).exists()
return item.quotas.filter(pk=self.quota_id).exists()
if self.item_id and not self.variation_id:
return self.item_id == item.pk

View File

@@ -87,6 +87,15 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: money_filter(op.price, event.currency)
}),
("price_with_addons", {
"label": _("Price including add-ons"),
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: money_filter(op.price + sum(
p.price
for p in op.addons.all()
if not p.canceled
), event.currency)
}),
("attendee_name", {
"label": _("Attendee name"),
"editor_sample": _("John Doe"),

View File

@@ -1,14 +1,14 @@
from collections import Counter, defaultdict, namedtuple
from datetime import timedelta
from datetime import datetime, time, timedelta
from decimal import Decimal
from typing import List, Optional
from celery.exceptions import MaxRetriesExceededError
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db import DatabaseError, transaction
from django.db.models import Q
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext as _
from pretix.base.i18n import language
@@ -19,8 +19,9 @@ from pretix.base.models import (
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.checkin import _save_answers
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -134,6 +135,15 @@ class CartManager:
raise CartError(error_messages['not_started'])
if self.event.presale_has_ended:
raise CartError(error_messages['ended'])
if not self.event.has_subevents:
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(self.event).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < self.now_dt:
raise CartError(error_messages['ended'])
def _extend_expiry_of_valid_existing_positions(self):
# Extend this user's cart session to ensure all items in the cart expire at the same time
@@ -152,6 +162,18 @@ class CartManager:
err = error_messages['some_subevent_ended']
cp.addons.all().delete()
cp.delete()
if cp.subevent:
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(cp.subevent).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < self.now_dt:
err = error_messages['some_subevent_ended']
cp.addons.all().delete()
cp.delete()
return err
def _update_subevents_cache(self, se_ids: List[int]):
@@ -216,6 +238,16 @@ class CartManager:
if op.subevent and op.subevent.presale_has_ended:
raise CartError(error_messages['ended'])
if op.subevent:
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(op.subevent).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < self.now_dt:
raise CartError(error_messages['ended'])
if isinstance(op, self.AddOperation):
if op.item.category and op.item.category.is_addon and not (op.addon_to and op.addon_to != 'FAKE'):
raise CartError(error_messages['addon_only'])
@@ -759,7 +791,11 @@ class CartManager:
if available_count == 1:
op.position.expires = self._expiry
op.position.price = op.price.gross
op.position.save()
try:
op.position.save(force_update=True)
except DatabaseError:
# Best effort... The position might have been deleted in the meantime!
pass
elif available_count == 0:
op.position.addons.all().delete()
op.position.delete()
@@ -774,17 +810,33 @@ class CartManager:
CartPosition.objects.bulk_create([p for p in new_cart_positions if not getattr(p, '_answers', None) and not p.pk])
return err
def _require_locking(self):
if self._voucher_use_diff:
# If any vouchers are used, we lock to make sure we don't redeem them to often
return True
if self._quota_diff and any(q.size is not None for q in self._quota_diff):
# If any quotas are affected that are not unlimited, we lock
return True
return False
def commit(self):
self._check_presale_dates()
self._check_max_cart_size()
self._calculate_expiry()
with self.event.lock() as now_dt:
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
lockfn = NoLockManager
if self._require_locking():
lockfn = self.event.lock
with lockfn() as now_dt:
with transaction.atomic():
self.now_dt = now_dt
self._extend_expiry_of_valid_existing_positions()
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
err = self._perform_operations() or err
if err:
raise CartError(err)

View File

@@ -120,7 +120,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
positions = list(
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
addon_c=Count('addons')
)
).order_by('positionid', 'id')
)
reverse_charge = False

View File

@@ -13,6 +13,18 @@ logger = logging.getLogger('pretix.base.locking')
LOCK_TIMEOUT = 120
class NoLockManager:
def __init__(self):
pass
def __enter__(self):
return now()
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
return False
class LockManager:
def __init__(self, event):
self.event = event

View File

@@ -15,6 +15,7 @@ from pretix.base.email import ClassicMailRenderer
from pretix.base.i18n import language
from pretix.base.models import Event, Invoice, InvoiceAddress, Order
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter
from pretix.celery_app import app
@@ -177,7 +178,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
chain(*task_chain).apply_async()
@app.task(bind=True)
@app.task(base=TransactionAwareTask, bind=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None,
order: int=None, attach_tickets=False) -> bool:
@@ -219,14 +220,23 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
args.append((name, content, ct.type))
attach_size += len(content)
if attach_tickets < 4 * 1024 * 1024:
if attach_size < 4 * 1024 * 1024:
# Do not attach more than 4MB, it will bounce way to often.
for a in args:
try:
email.attach(*a)
except:
pass
else:
order.log_action(
'pretix.event.order.email.attachments.skipped',
data={
'subject': 'Attachments skipped',
'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size),
'recipient': '',
'invoices': [],
}
)
email = email_filter.send_chained(event, 'message', message=email, order=order)

View File

@@ -1,7 +1,7 @@
import json
import logging
from collections import Counter, namedtuple
from datetime import datetime, timedelta
from datetime import datetime, time, timedelta
from decimal import Decimal
from typing import List, Optional
@@ -14,7 +14,7 @@ from django.db.models.functions import Greatest
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _
from pretix.api.models import OAuthApplication
@@ -28,16 +28,18 @@ from pretix.base.models import (
from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemBundle
from pretix.base.models.orders import (
CachedCombinedTicket, CachedTicket, InvoiceAddress, OrderFee, OrderRefund,
generate_position_secret, generate_secret,
InvoiceAddress, OrderFee, OrderRefund, generate_position_secret,
generate_secret,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.tax import TaxedPrice
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services import tickets
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_qualified,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledTask
@@ -160,7 +162,7 @@ def mark_order_expired(order, user=None, auth=None):
return order
def approve_order(order, user=None, send_mail: bool=True, auth=None):
def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False):
"""
Mark this order as approved
:param order: The order to change
@@ -183,7 +185,7 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None):
fee=None
)
try:
p.confirm(send_mail=False, count_waitinglist=False, user=user, auth=auth)
p.confirm(send_mail=False, count_waitinglist=False, user=user, auth=auth, ignore_date=True, force=force)
except Quota.QuotaExceededException:
raise OrderError(error_messages['unavailable'])
@@ -402,6 +404,16 @@ def _check_date(event: Event, now_dt: datetime):
if event.presale_has_ended:
raise OrderError(error_messages['ended'])
if not event.has_subevents:
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(event).date(),
time(hour=23, minute=59, second=59)
), event.timezone)
if term_last < now_dt:
raise OrderError(error_messages['ended'])
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None):
err = None
@@ -456,6 +468,18 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
delete(cp)
break
if cp.subevent:
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(cp.subevent).date(),
time(hour=23, minute=59, second=59)
), event.timezone)
if term_last < now_dt:
err = err or error_messages['some_subevent_ended']
delete(cp)
break
if cp.subevent and cp.subevent.presale_has_ended:
err = err or error_messages['some_subevent_ended']
delete(cp)
@@ -567,6 +591,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
meta_info: dict=None, sales_channel: str='web'):
fees, pf = _get_fees(positions, payment_provider, address, meta_info, event)
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
p = None
with transaction.atomic():
order = Order(
@@ -600,7 +625,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
fee.save()
if payment_provider and not order.require_approval:
order.payments.create(
p = order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED,
provider=payment_provider.identifier,
amount=total,
@@ -616,7 +641,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
order.log_action('pretix.event.order.consent', data={'msg': msg})
order_placed.send(event, order=order)
return order
return order, p
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
@@ -640,16 +665,32 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
except InvoiceAddress.DoesNotExist:
pass
with event.lock() as now_dt:
positions = list(CartPosition.objects.filter(
id__in=position_ids).select_related('item', 'variation', 'subevent', 'addon_to').prefetch_related('addons'))
positions = CartPosition.objects.filter(id__in=position_ids, event=event)
lockfn = NoLockManager
locked = False
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2))).exists():
# Performance optimization: If no voucher is used and no cart position is dangerously close to its expiry date,
# creating this order shouldn't be prone to any race conditions and we don't need to lock the event.
locked = True
lockfn = event.lock
with lockfn() as now_dt:
positions = list(positions.select_related('item', 'variation', 'subevent', 'addon_to').prefetch_related('addons'))
if len(positions) == 0:
raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions):
raise OrderError(error_messages['internal'])
_check_positions(event, now_dt, positions, address=addr)
order = _create_order(event, email, positions, now_dt, pprov,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel)
order, payment = _create_order(event, email, positions, now_dt, pprov,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel)
free_order_flow = payment and payment_provider == 'free' and order.total == Decimal('0.00') and not order.require_approval
if free_order_flow:
try:
payment.confirm(send_mail=False, lock=not locked)
except Quota.QuotaExceededException:
pass
invoice = order.invoices.last() # Might be generated by plugin already
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
@@ -664,7 +705,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
if order.require_approval:
email_template = event.settings.mail_text_order_placed_require_approval
log_entry = 'pretix.event.order.email.order_placed_require_approval'
elif payment_provider == 'free':
elif free_order_flow:
email_template = event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
else:
@@ -840,8 +881,8 @@ class OrderChangeManager:
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
'subevent_required': _('You need to choose a subevent for the new position.'),
}
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation', 'price'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent', 'price'))
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
CancelOperation = namedtuple('CancelOperation', ('position',))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent'))
@@ -852,6 +893,7 @@ class OrderChangeManager:
self.order = order
self.user = user
self.auth = auth
self.event = order.event
self.split_order = None
self._committed = False
self._totaldiff = 0
@@ -860,33 +902,18 @@ class OrderChangeManager:
self.notify = notify
self._invoice_dirty = False
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation], keep_price=False):
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation'])
if keep_price:
price = TaxedPrice(gross=position.price, net=position.price - position.tax_value,
tax=position.tax_value, rate=position.tax_rate,
name=position.tax_rule.name if position.tax_rule else None)
else:
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent,
invoice_address=self._invoice_address)
if price is None: # NOQA
raise OrderError(self.error_messages['product_invalid'])
new_quotas = (variation.quotas.filter(subevent=position.subevent)
if variation else item.quotas.filter(subevent=position.subevent))
if not new_quotas:
raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff += price.gross - position.price
self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas)
self._operations.append(self.ItemOperation(position, item, variation, price))
self._operations.append(self.ItemOperation(position, item, variation))
def change_subevent(self, position: OrderPosition, subevent: SubEvent):
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent,
@@ -900,19 +927,15 @@ class OrderChangeManager:
if not new_quotas:
raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff += price.gross - position.price
self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas)
self._operations.append(self.SubeventOperation(position, subevent, price))
self._operations.append(self.SubeventOperation(position, subevent))
def regenerate_secret(self, position: OrderPosition):
self._operations.append(self.RegenerateSecretOperation(position))
def change_price(self, position: OrderPosition, price: Decimal):
price = position.item.tax(price)
price = position.item.tax(price, base_price_is='gross')
self._totaldiff += price.gross - position.price
@@ -1069,14 +1092,11 @@ class OrderChangeManager:
'new_variation': op.variation.pk if op.variation else None,
'old_price': op.position.price,
'addon_to': op.position.addon_to_id,
'new_price': op.price.gross
'new_price': op.position.price
})
op.position.item = op.item
op.position.variation = op.variation
op.position.price = op.price.gross
op.position.tax_rate = op.price.rate
op.position.tax_value = op.price.tax
op.position.tax_rule = op.item.tax_rule
op.position._calculate_tax()
op.position.save()
elif isinstance(op, self.SubeventOperation):
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
@@ -1085,13 +1105,9 @@ class OrderChangeManager:
'old_subevent': op.position.subevent.pk,
'new_subevent': op.subevent.pk,
'old_price': op.position.price,
'new_price': op.price.gross
'new_price': op.position.price
})
op.position.subevent = op.subevent
op.position.price = op.price.gross
op.position.tax_rate = op.price.rate
op.position.tax_value = op.price.tax
op.position.tax_rule = op.position.item.tax_rule
op.position.save()
elif isinstance(op, self.PriceOperation):
self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={
@@ -1102,9 +1118,7 @@ class OrderChangeManager:
'new_price': op.price.gross
})
op.position.price = op.price.gross
op.position.tax_rate = op.price.rate
op.position.tax_value = op.price.tax
op.position.tax_rule = op.position.item.tax_rule
op.position._calculate_tax()
op.position.save()
elif isinstance(op, self.CancelOperation):
for opa in op.position.addons.all():
@@ -1154,8 +1168,8 @@ class OrderChangeManager:
elif isinstance(op, self.RegenerateSecretOperation):
op.position.secret = generate_position_secret()
op.position.save()
CachedTicket.objects.filter(order_position__order=self.order).delete()
CachedCombinedTicket.objects.filter(order=self.order).delete()
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
'order': self.order.pk})
self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={
'position': op.position.pk,
'positionid': op.position.positionid,
@@ -1388,11 +1402,11 @@ class OrderChangeManager:
order_changed.send(self.order.event, order=self.order)
def _clear_tickets_cache(self):
CachedTicket.objects.filter(order_position__order=self.order).delete()
CachedCombinedTicket.objects.filter(order=self.order).delete()
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
'order': self.order.pk})
if self.split_order:
CachedTicket.objects.filter(order_position__order=self.split_order).delete()
CachedCombinedTicket.objects.filter(order=self.split_order).delete()
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
'order': self.split_order.pk})
def _get_payment_provider(self):
lp = self.order.payments.last()

View File

@@ -123,8 +123,10 @@ def get_tickets_for_order(order):
order=order, provider=p.identifier, file__isnull=False
).last()
if not ct or not ct.file:
retval = generate.apply(args=('order', order.pk, p.identifier))
ct = CachedCombinedTicket.objects.get(pk=retval.get())
retval = generate_order(order.pk, p.identifier)
if not retval:
continue
ct = CachedCombinedTicket.objects.get(pk=retval)
tickets.append((
"{}-{}-{}{}".format(
order.event.slug.upper(), order.code, ct.provider, ct.extension,
@@ -140,8 +142,10 @@ def get_tickets_for_order(order):
order_position=pos, provider=p.identifier, file__isnull=False
).last()
if not ct or not ct.file:
retval = generate.apply(args=('orderposition', pos.pk, p.identifier))
ct = CachedTicket.objects.get(pk=retval.get())
retval = generate_orderposition(pos.pk, p.identifier)
if not retval:
continue
ct = CachedCombinedTicket.objects.get(pk=retval)
tickets.append((
"{}-{}-{}-{}{}".format(
order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension,
@@ -152,3 +156,26 @@ def get_tickets_for_order(order):
logger.exception('Failed to generate ticket.')
return tickets
@app.task(base=ProfiledTask)
def invalidate_cache(event: int, item: int=None, provider: str=None, order: int=None, **kwargs):
event = Event.objects.get(id=event)
qs = CachedTicket.objects.filter(order_position__order__event=event)
qsc = CachedCombinedTicket.objects.filter(order__event=event)
if item:
qs = qs.filter(order_position__item_id=item)
if provider:
qs = qs.filter(provider=provider)
qsc = qsc.filter(provider=provider)
if order:
qs = qs.filter(order_position__order_id=order)
qsc = qsc.filter(order_id=order)
for ct in qs:
ct.delete()
for ct in qsc:
ct.delete()

View File

@@ -1,6 +1,6 @@
{% extends "error.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
{% block title %}{% trans "Bad Request" %}{% endblock %}
{% block content %}
<i class="fa fa-frown-o fa-fw big-icon"></i>

View File

@@ -1,6 +1,6 @@
{% extends "error.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
{% block title %}{% trans "Permission denied" %}{% endblock %}
{% block content %}
<i class="fa fa-fw fa-lock big-icon"></i>

View File

@@ -1,6 +1,6 @@
{% extends "error.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
{% block title %}{% trans "Not found" %}{% endblock %}
{% block content %}
<i class="fa fa-meh-o fa-fw big-icon"></i>

View File

@@ -1,6 +1,6 @@
{% extends "error.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
{% block title %}{% trans "Internal Server Error" %}{% endblock %}
{% block content %}
<i class="fa fa-bolt big-icon fa-fw"></i>

View File

@@ -1,6 +1,6 @@
{% extends "error.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
{% block title %}{% trans "Verification failed" %}{% endblock %}
{% block content %}
<i class="fa fa-frown-o big-icon fa-fw"></i>

View File

@@ -1,66 +0,0 @@
{% load i18n %}{% load static from staticfiles %}
<link rel="stylesheet" href="{% static 'debug_toolbar/css/print.css' %}" type="text/css" media="print" />
<link rel="stylesheet" href="{% static 'debug_toolbar/css/toolbar.css' %}" type="text/css" />
<!-- Prevent our copy of jQuery from registering as an AMD module on sites that use RequireJS. -->
<script src="{% static 'debug_toolbar/js/jquery_pre.js' %}"></script>
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script src="{% static 'debug_toolbar/js/jquery_post.js' %}"></script>
<script src="{% static 'debug_toolbar/js/toolbar.js' %}"></script>
<div id="djDebug" class="djdt-hidden" dir="ltr"
data-store-id="{{ toolbar.store_id }}" data-render-panel-url="{% url 'djdt:render_panel' %}"
{{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }}>
<div class="djdt-hidden" id="djDebugToolbar">
<ul id="djDebugPanelList">
{% if toolbar.panels %}
<li><a id="djHideToolBarButton" href="#" title="{% trans "Hide toolbar" %}">{% trans "Hide" %} &#187;</a></li>
{% else %}
<li id="djDebugButton">DEBUG</li>
{% endif %}
{% for panel in toolbar.panels %}
<li class="djDebugPanelButton">
<input type="checkbox" data-cookie="djdt{{ panel.panel_id }}" {% if panel.enabled %}checked="checked" title="{% trans "Disable for next and successive requests" %}"{% else %}title="{% trans "Enable for next and successive requests" %}"{% endif %} />
{% if panel.has_content and panel.enabled %}
<a href="#" title="{{ panel.title }}" class="{{ panel.panel_id }}">
{% else %}
<div class="djdt-contentless{% if not panel.enabled %} djdt-disabled{% endif %}">
{% endif %}
{{ panel.nav_title }}
{% if panel.enabled %}
{% with panel.nav_subtitle as subtitle %}
{% if subtitle %}<br /><small>{{ subtitle }}</small>{% endif %}
{% endwith %}
{% endif %}
{% if panel.has_content and panel.enabled %}
</a>
{% else %}
</div>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
<div class="djdt-hidden" id="djDebugToolbarHandle">
<span title="{% trans "Show toolbar" %}" id="djShowToolBarButton">&#171;</span>
</div>
{% for panel in toolbar.panels %}
{% if panel.has_content and panel.enabled %}
<div id="{{ panel.panel_id }}" class="djdt-panelContent">
<div class="djDebugPanelTitle">
<a href="" class="djDebugClose"></a>
<h3>{{ panel.title|safe }}</h3>
</div>
<div class="djDebugPanelContent">
{% if toolbar.store_id %}
<img src="{% static 'debug_toolbar/img/ajax-loader.gif' %}" alt="loading" class="djdt-loader" />
<div class="djdt-scroll"></div>
{% else %}
<div class="djdt-scroll">{{ panel.content }}</div>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
<div id="djDebugWindow" class="djdt-panelContent"></div>
</div>

View File

@@ -1,6 +1,6 @@
{% load compress %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
<!DOCTYPE html>
<html>
<head>

View File

@@ -0,0 +1,6 @@
{% load thumb %}
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}">
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.value.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
{{ widget.input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>

View File

@@ -72,9 +72,11 @@ def safelink_callback(attrs, new=False):
def abslink_callback(attrs, new=False):
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, attrs.get((None, 'href'), '/'))
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
url = attrs.get((None, 'href'), '/')
if not url.startswith('mailto:') and not url.startswith('tel:'):
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, url)
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
return attrs
@@ -90,7 +92,7 @@ def markdown_compile_email(source):
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS,
))
), parse_email=True)
def markdown_compile(source):
@@ -116,6 +118,7 @@ def rich_text(text: str, **kwargs):
text = str(text)
body_md = bleach.linkify(
markdown_compile(text),
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback])
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
parse_email=True
)
return mark_safe(body_md)

View File

@@ -56,7 +56,9 @@ def page_not_found(request, exception):
}
template = get_template('404.html')
body = template.render(context, request)
return HttpResponseNotFound(body)
r = HttpResponseNotFound(body)
r.xframe_options_exempt = True
return r
@requires_csrf_token
@@ -65,7 +67,9 @@ def server_error(request):
template = loader.get_template('500.html')
except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
return HttpResponseServerError(template.render({
r = HttpResponseServerError(template.render({
'request': request,
'sentry_event_id': last_event_id(),
}))
r.xframe_options_exempt = True
return r

View File

@@ -2,7 +2,7 @@ from django.utils import timezone
from django.utils.translation.trans_real import DjangoTranslation
from django.views.decorators.cache import cache_page
from django.views.decorators.http import etag
from django.views.i18n import JavaScriptCatalog, render_javascript_catalog
from django.views.i18n import JavaScriptCatalog
# Yes, we want to regenerate this every time the module has been imported to
# refresh the cache at least at every code deployment
@@ -21,4 +21,5 @@ js_info_dict = {
def js_catalog(request, lang):
c = JavaScriptCatalog()
c.translation = DjangoTranslation(lang, domain='djangojs')
return render_javascript_catalog(c.get_catalog(), c.get_plural())
context = c.get_context_data()
return c.render_to_response(context)

View File

@@ -20,10 +20,10 @@ def serve_metrics(request):
return unauthed_response()
# check if the user is properly authorized:
if "HTTP_AUTHORIZATION" not in request.META:
if "Authorization" not in request.headers:
return unauthed_response()
method, credentials = request.META["HTTP_AUTHORIZATION"].split(" ", 1)
method, credentials = request.headers["Authorization"].split(" ", 1)
if method.lower() != "basic":
return unauthed_response()

View File

@@ -133,7 +133,7 @@ class AsyncAction:
return str(exception)
else:
logger.error('Unexpected exception: %r' % exception)
return _('An unexpected error has occurred.')
return _('An unexpected error has occurred, please try again later.')
def get_success_message(self, value):
return _('The task has been completed.')

View File

@@ -4,8 +4,8 @@ import os
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files import File
from django.forms.utils import from_current_timezone
from django.utils.html import conditional_escape
from django.utils.translation import ugettext_lazy as _
from ...base.forms import I18nModelForm
@@ -67,16 +67,31 @@ def selector(values, prop):
class ClearableBasenameFileInput(forms.ClearableFileInput):
template_name = 'pretixbase/forms/widgets/thumbnailed_file_input.html'
def get_template_substitution_values(self, value):
"""
Return value-related substitutions.
"""
bname = os.path.basename(value.name)
return {
'initial': conditional_escape(bname),
'initial_url': conditional_escape(value.url),
}
class FakeFile(File):
def __init__(self, file):
self.file = file
@property
def name(self):
return self.file.name
@property
def is_img(self):
return any(self.file.name.endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif'))
def __str__(self):
return os.path.basename(self.file.name).split('.', 1)[-1]
@property
def url(self):
return self.file.url
def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs)
ctx['widget']['value'] = self.FakeFile(value)
return ctx
class ExtFileField(forms.FileField):

View File

@@ -463,6 +463,7 @@ class ItemVariationForm(I18nModelForm):
'value',
'active',
'default_price',
'original_price',
'description',
]

View File

@@ -9,9 +9,7 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.models import (
InvoiceAddress, Item, ItemAddOn, Order, OrderPosition,
)
from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price
from pretix.control.forms.widgets import Select2
@@ -150,15 +148,6 @@ class CommentForm(I18nModelForm):
}
class SubEventChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
p = get_price(self.instance.item, self.instance.variation,
voucher=self.instance.voucher,
subevent=obj)
return '{} {} ({})'.format(obj.name, obj.get_date_range_display(),
p.print(self.instance.order.event.currency))
class OtherOperationsForm(forms.Form):
recalculate_taxes = forms.BooleanField(
label=_('Re-calculate taxes'),
@@ -265,12 +254,13 @@ class OrderPositionAddForm(forms.Form):
class OrderPositionChangeForm(forms.Form):
itemvar = forms.ChoiceField()
subevent = SubEventChoiceField(
itemvar = forms.ChoiceField(
required=False,
)
subevent = forms.ModelChoiceField(
SubEvent.objects.none(),
label=pgettext_lazy('subevent', 'New date'),
required=True,
empty_label=None
required=False,
empty_label=_('(Unchanged)')
)
price = forms.DecimalField(
required=False,
@@ -278,53 +268,49 @@ class OrderPositionChangeForm(forms.Form):
localize=True,
label=_('New price (gross)')
)
operation = forms.ChoiceField(
operation_secret = forms.BooleanField(
required=False,
widget=forms.RadioSelect,
choices=(
('product', 'Change product'),
('price', 'Change price'),
('subevent', 'Change event date'),
('cancel', 'Remove product'),
('split', 'Split into new order'),
('secret', 'Regenerate secret'),
)
label=_('Generate a new secret')
)
operation_cancel = forms.BooleanField(
required=False,
label=_('Cancel this position')
)
operation_split = forms.BooleanField(
required=False,
label=_('Split into new order')
)
change_product_keep_price = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance')
initial = kwargs.get('initial', {})
try:
ia = instance.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
if instance:
try:
if instance.variation:
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
elif instance.item:
initial['itemvar'] = str(instance.item.pk)
except Item.DoesNotExist:
pass
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
initial['price'] = instance.price - instance.tax_value
else:
initial['price'] = instance.price
initial['subevent'] = instance.subevent
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
initial['price'] = instance.price - instance.tax_value
else:
initial['price'] = instance.price
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if instance.order.event.has_subevents:
self.fields['subevent'].instance = instance
self.fields['subevent'].queryset = instance.order.event.subevents.all()
self.fields['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': instance.order.event.slug,
'organizer': instance.order.event.organizer.slug,
}),
'data-placeholder': _('(Unchanged)')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
else:
del self.fields['subevent']
choices = []
choices = [
('', _('(Unchanged)'))
]
for i in instance.order.event.items.prefetch_related('variations').all():
pname = str(i)
if not i.is_available():
@@ -333,14 +319,10 @@ class OrderPositionChangeForm(forms.Form):
if variations:
for v in variations:
p = get_price(i, v, voucher=instance.voucher, subevent=instance.subevent,
invoice_address=ia)
choices.append(('%d-%d' % (i.pk, v.pk),
'%s %s (%s)' % (pname, v.value, p.print(instance.order.event.currency))))
'%s %s' % (pname, v.value)))
else:
p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent,
invoice_address=ia)
choices.append((str(i.pk), '%s (%s)' % (pname, p.print(instance.order.event.currency))))
choices.append((str(i.pk), pname))
self.fields['itemvar'].choices = choices
change_decimal_field(self.fields['price'], instance.order.event.currency)

View File

@@ -29,6 +29,7 @@ class SubEventForm(I18nModelForm):
fields = [
'name',
'active',
'is_public',
'date_from',
'date_to',
'date_admission',

View File

@@ -140,7 +140,7 @@ class VoucherForm(I18nModelForm):
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
cnt = len(data['codes']) * data.get('max_usages', 0)
else:
cnt = data['max_usages']
cnt = data.get('max_usages', 0)
Voucher.clean_item_properties(
data, self.instance.event,

View File

@@ -189,6 +189,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
'pretix.event.order.email.error': _('Sending of an email has failed.'),
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attachments since they '
'would have been too large to be likely to arrive.'),
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
'pretix.event.order.email.download_reminder_sent': _('An email has been sent with a reminder that the ticket '
'is available for download.'),
@@ -389,6 +391,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if logentry.action_type == 'pretix.team.invite.created':
return _('{user} has been invited to the team.').format(user=data.get('email'))
if logentry.action_type == 'pretix.team.invite.resent':
return _('Invite for {user} has been resent.').format(user=data.get('email'))
if logentry.action_type == 'pretix.team.invite.deleted':
return _('The invite for {user} has been revoked.').format(user=data.get('email'))

View File

@@ -1,6 +1,6 @@
{% load compress %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
<!DOCTYPE html>
<html>
<head>

View File

@@ -12,6 +12,7 @@
{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixcontrol/scss/main.scss" %}" />
<link rel="stylesheet" type="text/x-scss" href="{% static "lightbox/css/lightbox.scss" %}" />
{% endcompress %}
{% if DEBUG %}
<script type="text/javascript" src="{% url 'javascript-catalog' lang=request.LANGUAGE_CODE %}"
@@ -43,6 +44,7 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/orderchange.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
@@ -50,6 +52,7 @@
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
<script type="text/javascript" src="{% static "fileupload/jquery.ui.widget.js" %}"></script>
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>
<script type="text/javascript" src="{% static "lightbox/js/lightbox.min.js" %}"></script>
{% endcompress %}
{{ html_head|safe }}
@@ -101,6 +104,11 @@
<i class="fa fa-eye"></i>
</a>
{% endif %}
{% elif request.organizer %}
<a href="{% eventurl request.organizer "presale:organizer.index" %}" title="{% trans "Public profile" %}" target="_blank"
class="navbar-toggle mobile-navbar-view-link">
<i class="fa fa-eye"></i>
</a>
{% endif %}
<a class="navbar-brand" href="{% url "control:index" %}">
<img src="{% static "pretixbase/img/pretix-icon.svg" %}" />
@@ -123,6 +131,12 @@
</a>
{% endif %}
</li>
{% elif request.organizer %}
<li>
<a href="{% eventurl request.organizer "presale:organizer.index" %}" title="{% trans "Public profile" %}" target="_blank">
<i class="fa fa-eye"></i> {% trans "Public profile" %}
</a>
</li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-top-links navbar-right">
@@ -366,6 +380,12 @@
{% block content %}
{% endblock %}
<footer>
{% if request.timezone %}
<span class="fa fa-globe"></span>
{% blocktrans trimmed with tz=request.timezone %}
Times displayed in {{ tz }}
{% endblocktrans %} &middot;
{% endif %}
{% with "href='http://pretix.eu'" as a_attr %}
{% blocktrans trimmed %}
powered by <a {{ a_attr }}>pretix</a>

View File

@@ -95,7 +95,15 @@
{% endif %}
</td>
<td>{{ e.item }}{% if e.variation %} {{ e.variation }}{% endif %}</td>
<td>{{ e.order.email }}</td>
<td>
{% if e.addon_to and e.addon_to.attendee_email %}
{{ e.addon_to.attendee_email }}
{% elif e.attendee_email %}
{{ e.attendee_email }}
{% else %}
{{ e.order.email }}
{% endif %}
</td>
<td>
{% if e.addon_to %}
{{ e.addon_to.attendee_name }}

View File

@@ -106,6 +106,9 @@
{{ e.get_short_date_to_display }}
{% endif %}
{% endif %}
{% if e.settings.timezone != request.timezone %}
<span class="fa fa-globe text-muted" data-toggle="tooltip" title="{{ e.timezone }}"></span>
{% endif %}
</td>
<td>
{% for q in e.first_quotas|slice:":3" %}

View File

@@ -45,6 +45,7 @@
{% bootstrap_form_errors form %}
{% bootstrap_field form.active layout="control" %}
{% bootstrap_field form.default_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.original_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.description layout="control" %}
</div>
</div>
@@ -78,6 +79,7 @@
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.active layout="control" %}
{% bootstrap_field formset.empty_form.default_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field formset.empty_form.original_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field formset.empty_form.description layout="control" %}
</div>
</div>

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load money %}
{% block title %}
{% blocktrans trimmed with code=order.code %}
Change order: {{ code }}
@@ -71,92 +72,87 @@
</h3>
</div>
<div class="panel-body">
<div class="form-inline form-order-change">
<div class="form-order-change" data-pricecalc-endpoint="{% url "api-v1:orderposition-price_calc" organizer=order.event.organizer.slug event=order.event.slug pk=position.pk %}">
{% bootstrap_form_errors position.form %}
{% if position.custom_error %}
<div class="alert alert-danger">
{{ position.custom_error }}
</div>
{% endif %}
<p class="help-block">
{% trans "Ticket secret:" %} {{ position.secret|slice:":12" }}…
</p>
<div class="radio">
<label>
<input name="{{ position.form.prefix }}-operation" type="radio" value=""
{% if not position.form.operation.value %}checked="checked"{% endif %}>
{% trans "Keep unchanged" %}
</label>
<div class="row">
<div class="col-sm-5 col-sm-offset-3">
<strong>{% trans "Current value" %}</strong>
</div>
<div class="col-sm-4">
<strong>{% trans "Change to" %}</strong>
</div>
</div>
<div class="radio">
<label>
<input name="{{ position.form.prefix }}-operation" type="radio" value="product"
{% if position.form.operation.value == "product" %}checked="checked"{% endif %}>
{% trans "Change product to" %}
<div class="row">
<div class="col-sm-3">
<strong>{% trans "Product" %}</strong>
</div>
<div class="col-sm-5">
{{ position.item }}
{% if position.variation %}
{{ position.variation }}
{% endif %}
</div>
<div class="col-sm-4">
{% bootstrap_field position.form.itemvar layout='inline' %}
</label>
<label class="checkbox">
{{ position.form.change_product_keep_price }}
{% trans "Keep price the same" %}
</label>
</div>
</div>
{% if request.event.has_subevents %}
<div class="radio">
<label>
<input name="{{ position.form.prefix }}-operation" type="radio" value="subevent"
{% if position.form.operation.value == "subevent" %}checked="checked"{% endif %}>
{% trans "Change date to" context "subevent" %}
<div class="row">
<div class="col-sm-3">
<strong>{% trans "Date" context "subevent" %}</strong>
</div>
<div class="col-sm-5">
{{ position.subevent }}
</div>
<div class="col-sm-4">
{% bootstrap_field position.form.subevent layout='inline' %}
</label>
</div>
</div>
{% endif %}
<div class="radio">
<label>
<input name="{{ position.form.prefix }}-operation" type="radio" value="price"
{% if position.form.operation.value == "price" %}checked="checked"{% endif %}>
{% trans "Change price to" %}
<div class="row">
<div class="col-sm-3">
<strong>{% trans "Price" %}</strong>
</div>
<div class="col-sm-5">
{{ position.price|money:request.event.currency }}<br>
{% if position.tax_rate %}
<small>{% blocktrans trimmed with rate=position.tax_rate name=position.tax_rule.name %}
<strong>incl.</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% endif %}
</div>
<div class="col-sm-4 field-container">
{% bootstrap_field position.form.price addon_after=request.event.currency layout='inline' %}
{% if position.apply_tax %}
{% if position.item.tax_rule and not position.item.tax_rule.price_includes_tax %}
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
{% elif position.item.tax_rule %}
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %}
<strong>incl.</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
{% endif %}
{% else %}
{% trans "no taxes apply" %}
{% endif %}
</label>
<small><strong>{% trans "including all taxes" %}</strong></small>
</div>
</div>
<div class="radio">
<label>
<input name="{{ position.form.prefix }}-operation" type="radio" value="split"
{% if position.form.operation.value == "split" %}checked="checked"{% endif %}>
{% trans "Split into new order" %}
</label>
</div>
<div class="radio">
<label>
<input name="{{ position.form.prefix }}-operation" type="radio" value="secret"
{% if position.form.operation.value == "secret" %}checked="checked"{% endif %}>
{% trans "Generate a new secret" %}
</label>
</div>
<div class="radio">
<label>
<input name="{{ position.form.prefix }}-operation" type="radio" value="cancel"
{% if position.form.operation.value == "cancel" %}checked="checked"{% endif %}>
{% trans "Cancel position" %}
{% if position.addons.exists %}
<em class="text-danger">
{% trans "Removing this position will also remove all add-ons to this position." %}
</em>
{% endif %}
</label>
<div class="row">
<div class="col-sm-3">
<strong>{% trans "Ticket secret" %}</strong>
</div>
<div class="col-sm-5">
{{ position.secret|slice:":12" }}…
</div>
<div class="col-sm-4">
{% bootstrap_field position.form.operation_secret layout='inline' %}
</div>
</div>
{% bootstrap_field position.form.operation_cancel layout='inline' %}
{% bootstrap_field position.form.operation_split layout='inline' %}
{% if position.addons.exists %}
<em class="text-danger">
{% trans "Removing this position will also remove all add-ons to this position." %}
</em>
{% endif %}
</div>
</div>
</div>

View File

@@ -118,7 +118,7 @@
<dt>{% trans "Order code" %}</dt>
<dd>{{ order.code }}</dd>
<dt>{% trans "Order date" %}</dt>
<dd>{{ order.datetime }}</dd>
<dd>{{ order.datetime|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% if sales_channel %}
<dt>{% trans "Sales channel" %}</dt>
<dd>{{ sales_channel.verbose_name }}</dd>
@@ -132,7 +132,7 @@
</dd>
{% if order.status == "n" %}
<dt>{% trans "Expiry date" %}</dt>
<dd>{{ order.expires }}</dd>
<dd>{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% endif %}
<dt>{% trans "User" %}</dt>
<dd>
@@ -489,7 +489,27 @@
{% if p.html_info %}
<tr>
<td colspan="1"></td>
<td colspan="5">{{ p.html_info|safe }}</td>
<td colspan="5">
{{ p.html_info|safe }}
{% if staff_session %}
<p>
<a href="" class="btn btn-default btn-xs" data-expandpayment data-id="{{ p.pk }}">
<span class="fa-eye fa fa-fw"></span>
{% trans "Inspect" %}
</a>
</p>
{% endif %}
</td>
</tr>
{% elif staff_session %}
<tr>
<td colspan="1"></td>
<td colspan="5">
<a href="" class="btn btn-default btn-xs" data-expandpayment data-id="{{ p.pk }}">
<span class="fa-eye fa fa-fw"></span>
{% trans "Inspect" %}
</a>
</td>
</tr>
{% endif %}
{% endfor %}
@@ -574,6 +594,18 @@
{% endif %}
</td>
</tr>
{% if staff_session %}
<tr>
<td colspan="1"></td>
<td colspan="7">
<a href="" class="btn btn-default btn-xs" data-expandrefund
data-id="{{ r.pk }}">
<span class="fa-eye fa fa-fw"></span>
{% trans "Inspect" %}
</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>

View File

@@ -3,8 +3,16 @@
{% load bootstrap3 %}
{% block inner %}
<h1>
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
{% blocktrans with name=request.organizer.name %}Organizer: {{ name }}{% endblocktrans %}
</h1>
{% if "can_create_events" in request.orgapermset %}
<p>
<a href="{% url "control:events.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new event" %}
</a>
</p>
{% endif %}
{% if events|length == 0 %}
<p>
<em>{% trans "You currently do not have access to any events." %}</em>
@@ -14,7 +22,12 @@
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Start date" %}</th>
<th>
{% trans "Start date" %}
/
{% trans "End date" %}
</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
@@ -25,16 +38,47 @@
<strong><a
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
</td>
<td>{{ e.get_date_from_display }}</td>
<td>
{% if e.has_subevents %}
{{ e.min_from|default_if_none:""|date:"SHORT_DATETIME_FORMAT" }}
{% else %}
{{ e.get_short_date_from_display }}
{% endif %}
{% if e.has_subevents %}
<span class="label label-default">{% trans "Series" %}</span>
{% endif %}
{% if e.settings.show_date_to and e.date_to %}
<br>
{% if e.has_subevents %}
{{ e.max_fromto|default_if_none:e.max_from|default_if_none:e.max_to|default_if_none:""|date:"SHORT_DATETIME_FORMAT" }}
{% else %}
{{ e.get_short_date_to_display }}
{% endif %}
{% endif %}
{% if e.settings.timezone != request.timezone %}
<span class="fa fa-globe text-muted" data-toggle="tooltip" title="{{ e.timezone }}"></span>
{% endif %}
</td>
<td>
{% if not e.live %}
<span class="label label-danger">{% trans "Shop disabled" %}</span>
{% elif e.presale_has_ended %}
<span class="label label-warning">{% trans "Presale over" %}</span>
{% elif not e.presale_is_running %}
<span class="label label-warning">{% trans "Presale not started" %}</span>
{% else %}
<span class="label label-success">{% trans "On sale" %}</span>
{% endif %}
</td>
<td class="text-right">
<a href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}"
class="btn btn-sm btn-default" title="{% trans "Open event dashboard" %}"
data-toggle="tooltip">
class="btn btn-sm btn-default" title="{% trans "Open event dashboard" %}"
data-toggle="tooltip">
<span class="fa fa-eye"></span>
</a>
{% if "can_create_events" in request.orgapermset %}
<a href="{% url "control:events.add" %}?clone={{ e.pk }}" class="btn btn-sm btn-default"
title="{% trans "Clone event" %}" data-toggle="tooltip">
title="{% trans "Clone event" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
{% endif %}
@@ -43,11 +87,6 @@
{% endfor %}
</tbody>
</table>
{% endif %}
{% if "can_create_events" in request.orgapermset %}
<a href="{% url "control:events.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new event" %}
</a>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>

View File

@@ -51,6 +51,11 @@
{{ i.email }}
<span class="fa fa-envelope-o" data-toggle="tooltip"
title="{% trans "invited, pending response" %}"></span>
<button type="submit" name="resend-invite" value="{{ i.id }}"
data-toggle="tooltip" title="{% trans "resend invite" %}"
class="btn-invisible">
<span class="fa fa-repeat"></span>
</button>
</td>
<td class="text-right">
<button type="submit" name="remove-invite" value="{{ i.id }}"

View File

@@ -269,6 +269,7 @@
{% bootstrap_field form.location layout="control" %}
{% bootstrap_field form.time_admission layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>

View File

@@ -27,6 +27,7 @@
{% bootstrap_field form.location layout="control" %}
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>

View File

@@ -19,6 +19,8 @@ urlpatterns = [
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
url(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
url(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
url(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),
url(r'^logdetail/refund/$', global_settings.RefundDetailView.as_view(), name='global.refunddetail'),
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
url(r'^sudo/$', user.StartStaffSession.as_view(), name='user.sudo'),
url(r'^sudo/stop/$', user.StopStaffSession.as_view(), name='user.sudo.stop'),

View File

@@ -100,6 +100,7 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
'list': self.list.pk,
'web': True
}, user=request.user)
op.order.touch()
messages.success(request, _('The selected check-ins have been reverted.'))
else:
@@ -247,7 +248,7 @@ class CheckinListDelete(EventPermissionRequiredMixin, DeleteView):
self.object = self.get_object()
success_url = self.get_success_url()
self.object.checkins.all().delete()
self.object.log_action(action='pretix.event.orders.deleted', user=request.user)
self.object.log_action(action='pretix.event.checkinlists.deleted', user=request.user)
self.object.delete()
messages.success(self.request, _('The selected list has been deleted.'))
return HttpResponseRedirect(success_url)

View File

@@ -360,7 +360,8 @@ def widgets_for_event_qs(request, qs, user, nmax):
'_settings_objects', 'organizer___settings_objects'
).select_related('organizer')[:nmax]
for event in events:
tz = pytz.timezone(event.cache.get_or_set('timezone', lambda: event.settings.timezone))
tzname = event.cache.get_or_set('timezone', lambda: event.settings.timezone)
tz = pytz.timezone(tzname)
if event.has_subevents:
if event.min_from is None:
dr = pgettext("subevent", "No dates")
@@ -393,6 +394,9 @@ def widgets_for_event_qs(request, qs, user, nmax):
((date_format(event.date_admission.astimezone(tz), 'TIME_FORMAT') + ' / ')
if event.date_admission and event.date_admission != event.date_from else '')
+ (date_format(event.date_from.astimezone(tz), 'TIME_FORMAT') if event.date_from else '')
) + (
' <span class="fa fa-globe text-muted" data-toggle="tooltip" title="{}"></span>'.format(tzname)
if tzname != request.timezone and not event.has_subevents else ''
),
url=reverse('control:event.index', kwargs={
'event': event.slug,

View File

@@ -31,8 +31,7 @@ from pytz import timezone
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import LazyCurrencyNumber
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, LogEntry, Order, RequiredAction,
TaxRule, Voucher,
Event, LogEntry, Order, RequiredAction, TaxRule, Voucher,
)
from pretix.base.models.event import EventMetaValue
from pretix.base.services import tickets
@@ -789,12 +788,7 @@ class TicketSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
for k in provider.form.changed_data
}
)
CachedTicket.objects.filter(
order_position__order__event=self.request.event, provider=provider.identifier
).delete()
CachedCombinedTicket.objects.filter(
order__event=self.request.event, provider=provider.identifier
).delete()
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'provider': provider.identifier})
else:
success = False
form = self.get_form(self.get_form_class())

View File

@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from django.views import View
from django.views.generic import FormView, TemplateView
from pretix.base.models import LogEntry
from pretix.base.models import LogEntry, OrderPayment, OrderRefund
from pretix.base.services.update_check import check_result_table, update_check
from pretix.base.settings import GlobalSettingsObject
from pretix.control.forms.global_settings import (
@@ -71,3 +71,15 @@ class LogDetailView(AdministratorPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
le = get_object_or_404(LogEntry, pk=request.GET.get('pk'))
return JsonResponse({'data': le.parsed_data})
class PaymentDetailView(AdministratorPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
p = get_object_or_404(OrderPayment, pk=request.GET.get('pk'))
return JsonResponse({'data': p.info_data})
class RefundDetailView(AdministratorPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
p = get_object_or_404(OrderRefund, pk=request.GET.get('pk'))
return JsonResponse({'data': p.info_data})

View File

@@ -18,11 +18,12 @@ from django.views.generic.edit import DeleteView
from pretix.base.forms import I18nFormSet
from pretix.base.models import (
CachedTicket, CartPosition, Item, ItemCategory, ItemVariation, Order,
Question, QuestionAnswer, QuestionOption, Quota, Voucher,
CartPosition, Item, ItemCategory, ItemVariation, Order, Question,
QuestionAnswer, QuestionOption, Quota, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemAddOn, ItemBundle
from pretix.base.services.tickets import invalidate_cache
from pretix.control.forms.item import (
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
ItemBundleFormSet, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
@@ -49,7 +50,9 @@ class ItemList(ListView):
event=self.request.event
).annotate(
var_count=Count('variations')
).prefetch_related("category")
).prefetch_related("category").order_by(
'category__position', 'category', 'position'
)
def item_move(request, item, up=True):
@@ -873,7 +876,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
self.object.log_action(
'pretix.event.item.changed', user=self.request.user, data=data
)
CachedTicket.objects.filter(order_position__item=self.item).delete()
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'item': self.object.pk})
for f in self.plugin_forms:
f.save()
return super().form_valid(form)

View File

@@ -2,6 +2,7 @@ import json
import logging
import mimetypes
import os
import re
from datetime import timedelta
from decimal import Decimal, DecimalException
@@ -43,6 +44,7 @@ from pretix.base.models.orders import (
)
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.payment import PaymentException
from pretix.base.services import tickets
from pretix.base.services.export import export
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
@@ -1239,7 +1241,11 @@ class OrderChange(OrderView):
return False
try:
if p.form.cleaned_data['operation'] == 'product':
if p.form.cleaned_data['operation_cancel']:
ocm.cancel(p)
continue
if p.form.cleaned_data['itemvar']:
if '-' in p.form.cleaned_data['itemvar']:
itemid, varid = p.form.cleaned_data['itemvar'].split('-')
else:
@@ -1250,16 +1256,19 @@ class OrderChange(OrderView):
variation = ItemVariation.objects.get(pk=varid, item=item)
else:
variation = None
ocm.change_item(p, item, variation, keep_price=p.form.cleaned_data['change_product_keep_price'])
elif p.form.cleaned_data['operation'] == 'price':
ocm.change_price(p, p.form.cleaned_data['price'])
elif p.form.cleaned_data['operation'] == 'subevent':
if item != p.item or variation != p.variation:
ocm.change_item(p, item, variation)
if self.request.event.has_subevents and p.form.cleaned_data['subevent'] and p.form.cleaned_data['subevent'] != p.subevent:
ocm.change_subevent(p, p.form.cleaned_data['subevent'])
elif p.form.cleaned_data['operation'] == 'cancel':
ocm.cancel(p)
elif p.form.cleaned_data['operation'] == 'split':
if p.form.cleaned_data['price'] != p.price:
ocm.change_price(p, p.form.cleaned_data['price'])
if p.form.cleaned_data['operation_split']:
ocm.split(p)
elif p.form.cleaned_data['operation'] == 'secret':
if p.form.cleaned_data['operation_secret']:
ocm.regenerate_secret(p)
except OrderError as e:
@@ -1319,8 +1328,7 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
'you need to do this manually.')
messages.success(self.request, _(success_message))
CachedTicket.objects.filter(order_position__order=self.order).delete()
CachedCombinedTicket.objects.filter(order=self.order).delete()
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
order_modified.send(sender=self.request.event, order=self.order)
return redirect(self.get_order_url())
@@ -1363,8 +1371,7 @@ class OrderContactChange(OrderView):
for op in self.order.all_positions.all():
op.secret = generate_position_secret()
op.save()
CachedTicket.objects.filter(order_position__order=self.order).delete()
CachedCombinedTicket.objects.filter(order=self.order).delete()
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
self.order.log_action('pretix.event.order.secret.changed', user=self.request.user)
self.form.save()
@@ -1405,6 +1412,7 @@ class OrderLocaleChange(OrderView):
)
self.form.save()
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
messages.success(self.request, _('The order has been changed.'))
return redirect(self.get_order_url())
return self.get(*args, **kwargs)
@@ -1585,6 +1593,10 @@ class OrderGo(EventPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
code = request.GET.get("code", "").upper().strip()
if '://' in code:
m = re.match('.*/ORDER/([A-Z0-9]{' + str(settings.ENTROPY['order_code']) + '})/.*', code)
if m:
code = m.group(1)
try:
if code.startswith(request.event.slug.upper()):
code = code[len(request.event.slug):]

View File

@@ -6,7 +6,8 @@ from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.db import transaction
from django.db.models import Count, ProtectedError
from django.db.models import Count, Max, Min, ProtectedError
from django.db.models.functions import Coalesce, Greatest
from django.forms import inlineformset_factory
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
@@ -19,7 +20,7 @@ from django.views.generic import (
from pretix.api.models import WebHook
from pretix.base.models import Device, Organizer, Team, TeamInvite, User
from pretix.base.models.event import EventMetaProperty
from pretix.base.models.event import Event, EventMetaProperty
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.filter import OrganizerFilterForm
@@ -86,18 +87,33 @@ class OrganizerDetailViewMixin:
return self.request.organizer
class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Organizer
class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = Event
template_name = 'pretixcontrol/organizers/detail.html'
permission = None
context_object_name = 'organizer'
context_object_name = 'events'
def get_object(self, queryset=None) -> Organizer:
@property
def organizer(self):
return self.request.organizer
def get_queryset(self):
qs = self.request.user.get_events_with_any_permission(self.request).select_related('organizer').prefetch_related(
'_settings_objects', 'organizer___settings_objects'
).filter(organizer=self.request.organizer).order_by('-date_from')
qs = qs.annotate(
min_from=Min('subevents__date_from'),
max_from=Max('subevents__date_from'),
max_to=Max('subevents__date_to'),
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
).annotate(
order_from=Coalesce('min_from', 'date_from'),
order_to=Coalesce('max_fromto', 'max_to', 'max_from', 'date_to', 'date_from'),
)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['events'] = self.request.organizer.events.all()
return ctx
@@ -518,7 +534,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
if 'remove-member' in request.POST:
try:
user = User.objects.get(pk=request.POST.get('remove-member'))
except User.DoesNotExist:
except (User.DoesNotExist, ValueError):
pass
else:
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
@@ -542,7 +558,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
elif 'remove-invite' in request.POST:
try:
invite = self.object.invites.get(pk=request.POST.get('remove-invite'))
except TeamInvite.DoesNotExist:
except (TeamInvite.DoesNotExist, ValueError):
messages.error(self.request, _('Invalid invite selected.'))
return redirect(self.get_success_url())
else:
@@ -555,10 +571,26 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
messages.success(self.request, _('The invite has been revoked.'))
return redirect(self.get_success_url())
elif 'resend-invite' in request.POST:
try:
invite = self.object.invites.get(pk=request.POST.get('resend-invite'))
except (TeamInvite.DoesNotExist, ValueError):
messages.error(self.request, _('Invalid invite selected.'))
return redirect(self.get_success_url())
else:
self._send_invite(invite)
self.object.log_action(
'pretix.team.invite.resent', user=self.request.user, data={
'email': invite.email
}
)
messages.success(self.request, _('The invite has been resent.'))
return redirect(self.get_success_url())
elif 'remove-token' in request.POST:
try:
token = self.object.tokens.get(pk=request.POST.get('remove-token'))
except TeamAPIToken.DoesNotExist:
except (TeamAPIToken.DoesNotExist, ValueError):
messages.error(self.request, _('Invalid token selected.'))
return redirect(self.get_success_url())
else:

View File

@@ -13,7 +13,7 @@ class SessionReauthRequired(Exception):
def get_user_agent_hash(request):
return hashlib.sha256(request.META['HTTP_USER_AGENT'].encode()).hexdigest()
return hashlib.sha256(request.headers['User-Agent'].encode()).hexdigest()
def assert_session_valid(request):
@@ -26,7 +26,7 @@ def assert_session_valid(request):
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
raise SessionReauthRequired()
if 'HTTP_USER_AGENT' in request.META:
if 'User-Agent' in request.headers:
if 'pinned_user_agent' in request.session:
if request.session.get('pinned_user_agent') != get_user_agent_hash(request):
raise SessionInvalid()

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -186,11 +186,11 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:306
#: pretix/static/pretixcontrol/js/ui/main.js:305
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:306
msgid "None"
msgstr ""
@@ -198,10 +198,14 @@ msgstr ""
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:646
#: pretix/static/pretixcontrol/js/ui/main.js:652
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
msgid "Calculating default price…"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr ""
@@ -246,229 +250,238 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
#: pretix/static/pretixpresale/js/widget/widget.js:14
msgctxt "widget"
msgid "Sold out"
#: pretix/static/pretixpresale/js/ui/main.js:201
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Buy"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Reserved"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "FREE"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "incl. taxes"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "plus taxes"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
"ticketing powered by pretix</a>"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "See variations"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Next month"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Previous month"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgid "Tu"
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "We"
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Th"
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "Fr"
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Sa"
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "December"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -185,11 +185,11 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:306
#: pretix/static/pretixcontrol/js/ui/main.js:305
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:306
msgid "None"
msgstr ""
@@ -197,10 +197,14 @@ msgstr ""
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:646
#: pretix/static/pretixcontrol/js/ui/main.js:652
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
msgid "Calculating default price…"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr ""
@@ -237,229 +241,238 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/widget/widget.js:14
msgctxt "widget"
msgid "Sold out"
#: pretix/static/pretixpresale/js/ui/main.js:201
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Buy"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Reserved"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "FREE"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "incl. taxes"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "plus taxes"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
"ticketing powered by pretix</a>"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "See variations"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Next month"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Previous month"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgid "Tu"
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "We"
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Th"
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "Fr"
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Sa"
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "December"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -185,11 +185,11 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:306
#: pretix/static/pretixcontrol/js/ui/main.js:305
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:306
msgid "None"
msgstr ""
@@ -197,10 +197,14 @@ msgstr ""
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:646
#: pretix/static/pretixcontrol/js/ui/main.js:652
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
msgid "Calculating default price…"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr ""
@@ -239,229 +243,238 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
#: pretix/static/pretixpresale/js/widget/widget.js:14
msgctxt "widget"
msgid "Sold out"
#: pretix/static/pretixpresale/js/ui/main.js:201
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Buy"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Reserved"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "FREE"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "incl. taxes"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "plus taxes"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
"ticketing powered by pretix</a>"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "See variations"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Next month"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Previous month"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgid "Tu"
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "We"
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Th"
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "Fr"
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Sa"
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "December"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"PO-Revision-Date: 2018-04-24 14:22+0000\n"
"Last-Translator: Pernille Thorsen <perth@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -202,11 +202,11 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:306
#: pretix/static/pretixcontrol/js/ui/main.js:305
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:306
msgid "None"
msgstr "Ingen"
@@ -214,10 +214,16 @@ msgstr "Ingen"
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:646
#: pretix/static/pretixcontrol/js/ui/main.js:652
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
#, fuzzy
#| msgid "Contacting Stripe …"
msgid "Calculating default price…"
msgstr "Kontakter Stripe …"
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr "Andre"
@@ -256,89 +262,93 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Varerne i din kurv er reserveret for dig i et minut."
msgstr[1] "Varerne i din kurv er reserveret for dig i {num} minutter."
#: pretix/static/pretixpresale/js/widget/widget.js:14
#: pretix/static/pretixpresale/js/ui/main.js:201
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Sold out"
msgstr "Udsolgt"
#: pretix/static/pretixpresale/js/widget/widget.js:15
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Buy"
msgstr "Læg i kurv"
#: pretix/static/pretixpresale/js/widget/widget.js:16
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Reserved"
msgstr "Reserveret"
#: pretix/static/pretixpresale/js/widget/widget.js:17
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "FREE"
msgstr "GRATIS"
#: pretix/static/pretixpresale/js/widget/widget.js:18
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr "fra %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/widget/widget.js:19
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr "inkl. %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:20
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr "plus %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:21
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
#: pretix/static/pretixpresale/js/widget/widget.js:24
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr "tilgængelig: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Only available with a voucher"
msgstr "Kun tilgængelig med en voucher"
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr "minimumsantal: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Close ticket shop"
msgstr "Luk billetbutik"
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr "Billetbutikken kunne ikke hentes."
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr "Kurven kunne ikke oprettes. Prøv igen senere"
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Waiting list"
msgstr "Venteliste"
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:31
#, fuzzy
#| msgctxt "widget"
#| msgid ""
@@ -354,12 +364,12 @@ msgstr ""
"varer vil de blive tilføjet din eksisterende kurv. Klik på denne besked for "
"at gå mod kassen med din kurv."
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
@@ -368,129 +378,134 @@ msgstr ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener"
"\">billetsystem drevet af pretix</a>"
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Redeem a voucher"
msgstr "Indløs voucher"
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Redeem"
msgstr "Indløs"
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Voucher code"
msgstr "Voucherkode"
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Close"
msgstr "Luk"
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "Continue"
msgstr "Fortsæt"
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "See variations"
msgstr "Vis varianter"
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Next month"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Previous month"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgid "Tu"
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "We"
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Th"
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "Fr"
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Sa"
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "December"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
"PO-Revision-Date: 2019-03-22 15:00+0000\n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"PO-Revision-Date: 2019-04-23 08:55+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
"de/>\n"
@@ -205,11 +205,11 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wählen Sie eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:306
#: pretix/static/pretixcontrol/js/ui/main.js:305
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:306
msgid "None"
msgstr "Keine"
@@ -217,10 +217,16 @@ msgstr "Keine"
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:646
#: pretix/static/pretixcontrol/js/ui/main.js:652
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
#, fuzzy
#| msgid "Contacting Stripe …"
msgid "Calculating default price…"
msgstr "Kontaktiere Stripe …"
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr "Sonstige"
@@ -259,89 +265,93 @@ msgstr[0] ""
msgstr[1] ""
"Die Produkte in Ihrem Warenkorb sind noch {num} Minuten für Sie reserviert."
#: pretix/static/pretixpresale/js/widget/widget.js:14
#: pretix/static/pretixpresale/js/ui/main.js:201
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Sold out"
msgstr "Ausverkauft"
#: pretix/static/pretixpresale/js/widget/widget.js:15
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Buy"
msgstr "In den Warenkorb"
#: pretix/static/pretixpresale/js/widget/widget.js:16
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Reserved"
msgstr "Reserviert"
#: pretix/static/pretixpresale/js/widget/widget.js:17
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "FREE"
msgstr "GRATIS"
#: pretix/static/pretixpresale/js/widget/widget.js:18
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr "ab %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/widget/widget.js:19
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr "inkl. %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:20
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr "zzgl. %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:21
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "incl. taxes"
msgstr "inkl. Steuern"
#: pretix/static/pretixpresale/js/widget/widget.js:22
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "plus taxes"
msgstr "zzgl. Steuern"
#: pretix/static/pretixpresale/js/widget/widget.js:23
#: pretix/static/pretixpresale/js/widget/widget.js:24
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr "aktuell verfügbar: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Only available with a voucher"
msgstr "Nur mit Gutschein verfügbar"
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr "minimale Bestellmenge: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Close ticket shop"
msgstr "Ticket-Shop schließen"
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr "Der Ticket-Shop konnte nicht geladen werden."
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr "Der Warenkorb konnte nicht erstellt werden. Bitte erneut versuchen."
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Waiting list"
msgstr "Warteliste"
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
@@ -350,12 +360,12 @@ msgstr ""
"Sie haben einen aktiven Warenkorb für diese Veranstaltung. Wenn Sie mehr "
"Produkte auswählen, werden diese zu Ihrem Warenkorb hinzugefügt."
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Resume checkout"
msgstr "Kauf fortsetzen"
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
@@ -364,129 +374,134 @@ msgstr ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">Event-"
"Ticketshop von pretix</a>"
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Redeem a voucher"
msgstr "Gutschein einlösen"
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Redeem"
msgstr "Einlösen"
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Voucher code"
msgstr "Gutscheincode"
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Close"
msgstr "Schließen"
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "Continue"
msgstr "Weiter"
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "See variations"
msgstr "Varianten zeigen"
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Choose a different event"
msgstr "Andere Veranstaltung auswählen"
#: pretix/static/pretixpresale/js/widget/widget.js:42
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Choose a different date"
msgstr "Anderen Termin auswählen"
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Back"
msgstr "Zurück"
#: pretix/static/pretixpresale/js/widget/widget.js:43
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Next month"
msgstr "Nächster Monat"
#: pretix/static/pretixpresale/js/widget/widget.js:44
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget"
msgid "Previous month"
msgstr "Vorheriger Monat"
#: pretix/static/pretixpresale/js/widget/widget.js:46
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "Mo"
msgstr "Mo"
#: pretix/static/pretixpresale/js/widget/widget.js:47
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Tu"
msgstr "Di"
#: pretix/static/pretixpresale/js/widget/widget.js:48
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "We"
msgstr "Mi"
#: pretix/static/pretixpresale/js/widget/widget.js:49
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Th"
msgstr "Do"
#: pretix/static/pretixpresale/js/widget/widget.js:50
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Fr"
msgstr "Fr"
#: pretix/static/pretixpresale/js/widget/widget.js:51
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Sa"
msgstr "Sa"
#: pretix/static/pretixpresale/js/widget/widget.js:52
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "Su"
msgstr "So"
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "January"
msgstr "Januar"
#: pretix/static/pretixpresale/js/widget/widget.js:56
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "February"
msgstr "Februar"
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "March"
msgstr "März"
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "April"
msgstr "April"
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "May"
msgstr "Mai"
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "June"
msgstr "Juni"
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "July"
msgstr "Juli"
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "August"
msgstr "August"
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "September"
msgstr "September"
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "October"
msgstr "Oktober"
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "November"
msgstr "November"
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "December"
msgstr "Dezember"

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
"PO-Revision-Date: 2019-03-22 15:00+0000\n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"PO-Revision-Date: 2019-04-23 08:55+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/de_Informal/>\n"
@@ -204,11 +204,11 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wähle eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:306
#: pretix/static/pretixcontrol/js/ui/main.js:305
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:306
msgid "None"
msgstr "Keine"
@@ -216,10 +216,16 @@ msgstr "Keine"
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:646
#: pretix/static/pretixcontrol/js/ui/main.js:652
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
#, fuzzy
#| msgid "Contacting Stripe …"
msgid "Calculating default price…"
msgstr "Kontaktiere Stripe …"
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr "Sonstige"
@@ -258,89 +264,93 @@ msgstr[0] ""
msgstr[1] ""
"Die Produkte in deinem Warenkorb sind noch {num} Minuten für dich reserviert."
#: pretix/static/pretixpresale/js/widget/widget.js:14
#: pretix/static/pretixpresale/js/ui/main.js:201
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Sold out"
msgstr "Ausverkauft"
#: pretix/static/pretixpresale/js/widget/widget.js:15
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Buy"
msgstr "In den Warenkorb"
#: pretix/static/pretixpresale/js/widget/widget.js:16
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Reserved"
msgstr "Reserviert"
#: pretix/static/pretixpresale/js/widget/widget.js:17
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "FREE"
msgstr "GRATIS"
#: pretix/static/pretixpresale/js/widget/widget.js:18
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr "ab %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/widget/widget.js:19
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr "inkl. %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:20
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr "zzgl. %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:21
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "incl. taxes"
msgstr "inkl. Steuern"
#: pretix/static/pretixpresale/js/widget/widget.js:22
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "plus taxes"
msgstr "zzgl. Steuern"
#: pretix/static/pretixpresale/js/widget/widget.js:23
#: pretix/static/pretixpresale/js/widget/widget.js:24
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr "aktuell verfügbar: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Only available with a voucher"
msgstr "Nur mit Gutschein verfügbar"
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr "minimale Bestellmenge: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Close ticket shop"
msgstr "Ticket-Shop schließen"
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr "Der Ticket-Shop konnte nicht geladen werden."
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr "Der Warenkorb konnte nicht erstellt werden. Bitte erneut versuchen."
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Waiting list"
msgstr "Warteliste"
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
@@ -349,12 +359,12 @@ msgstr ""
"Du hast einen aktiven Warenkorb für diese Veranstaltung. Wenn du mehr "
"Produkte auswählst, werden diese zu deinem Warenkorb hinzugefügt."
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Resume checkout"
msgstr "Kauf fortsetzen"
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
@@ -363,129 +373,134 @@ msgstr ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">Event-"
"Ticketshop von pretix</a>"
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Redeem a voucher"
msgstr "Gutschein einlösen"
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Redeem"
msgstr "Einlösen"
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Voucher code"
msgstr "Gutscheincode"
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Close"
msgstr "Schließen"
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "Continue"
msgstr "Fortfahren"
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "See variations"
msgstr "Varianten zeigen"
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Choose a different event"
msgstr "Andere Veranstaltung auswählen"
#: pretix/static/pretixpresale/js/widget/widget.js:42
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Choose a different date"
msgstr "Anderen Termin auswählen"
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Back"
msgstr "Zurück"
#: pretix/static/pretixpresale/js/widget/widget.js:43
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Next month"
msgstr "Nächster Monat"
#: pretix/static/pretixpresale/js/widget/widget.js:44
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget"
msgid "Previous month"
msgstr "Vorheriger Monat"
#: pretix/static/pretixpresale/js/widget/widget.js:46
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "Mo"
msgstr "Mo"
#: pretix/static/pretixpresale/js/widget/widget.js:47
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Tu"
msgstr "Di"
#: pretix/static/pretixpresale/js/widget/widget.js:48
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "We"
msgstr "Mi"
#: pretix/static/pretixpresale/js/widget/widget.js:49
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Th"
msgstr "Do"
#: pretix/static/pretixpresale/js/widget/widget.js:50
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Fr"
msgstr "Fr"
#: pretix/static/pretixpresale/js/widget/widget.js:51
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Sa"
msgstr "Sa"
#: pretix/static/pretixpresale/js/widget/widget.js:52
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "Su"
msgstr "So"
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "January"
msgstr "Januar"
#: pretix/static/pretixpresale/js/widget/widget.js:56
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "February"
msgstr "Februar"
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "March"
msgstr "März"
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "April"
msgstr "April"
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "May"
msgstr "Mai"
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "June"
msgstr "Juni"
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "July"
msgstr "Juli"
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "August"
msgstr "August"
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "September"
msgstr "September"
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "October"
msgstr "Oktober"
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "November"
msgstr "November"
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "December"
msgstr "Dezember"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -186,11 +186,11 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:306
#: pretix/static/pretixcontrol/js/ui/main.js:305
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
#: pretix/static/pretixcontrol/js/ui/main.js:306
msgid "None"
msgstr ""
@@ -198,10 +198,14 @@ msgstr ""
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:646
#: pretix/static/pretixcontrol/js/ui/main.js:652
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
msgid "Calculating default price…"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr ""
@@ -238,229 +242,238 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/widget/widget.js:14
msgctxt "widget"
msgid "Sold out"
#: pretix/static/pretixpresale/js/ui/main.js:201
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Buy"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Reserved"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "FREE"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "incl. taxes"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "plus taxes"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
"ticketing powered by pretix</a>"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "See variations"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Next month"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Previous month"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgid "Tu"
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "We"
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Th"
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "Fr"
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Sa"
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "December"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,47 +7,49 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-03 13:19+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"PO-Revision-Date: 2019-04-24 22:00+0000\n"
"Last-Translator: ThanosTeste <testebasisth@unisystems.eu>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
"el/>\n"
"Language: el\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.5.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
msgid "Marked as paid"
msgstr ""
msgstr "Επισήμανση ως πληρωμένο"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
msgid "Comment:"
msgstr ""
msgstr "Σχόλιο:"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Placed orders"
msgstr ""
msgstr "Παραγγελίες"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Paid orders"
msgstr ""
msgstr "Πληρωμένες παραγγελίες"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
msgid "Total revenue"
msgstr ""
msgstr "Συνολικά κέρδη"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
msgid "Contacting Stripe …"
msgstr ""
msgstr "Επικοινωνία με το Stripe …"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:56
msgid "Total"
msgstr ""
msgstr "??????"
#: pretix/static/pretixbase/js/asynctask.js:39
#: pretix/static/pretixbase/js/asynctask.js:95
@@ -55,6 +57,9 @@ msgid ""
"Your request has been queued on the server and will now be processed. "
"Depending on the size of your event, this might take up to a few minutes."
msgstr ""
"Το αίτημά σας έχει τεθεί σε ουρά στο διακομιστή και θα υποβληθεί σε "
"επεξεργασία. Ανάλογα με το μέγεθος του συμβάντος, αυτό μπορεί να διαρκέσει "
"μερικά λεπτά."
#: pretix/static/pretixbase/js/asynctask.js:45
#: pretix/static/pretixbase/js/asynctask.js:101
@@ -63,33 +68,40 @@ msgid ""
"If this takes longer than two minutes, please contact us or go back in your "
"browser and try again."
msgstr ""
"Το αίτημά σας έφτασε στο διακομιστή, αλλά περιμένουμε ακόμα την επεξεργασία "
"του. Αν αυτό διαρκεί περισσότερο από δύο λεπτά, επικοινωνήστε μαζί μας ή "
"επιστρέψτε στο πρόγραμμα περιήγησής σας και δοκιμάστε ξανά."
#: pretix/static/pretixbase/js/asynctask.js:66
#: pretix/static/pretixbase/js/asynctask.js:124
#: pretix/static/pretixcontrol/js/ui/mail.js:23
msgid "An error of type {code} occurred."
msgstr ""
msgstr "Παρουσιάστηκε σφάλμα τύπου {code}."
#: pretix/static/pretixbase/js/asynctask.js:69
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
msgstr ""
"Αυτήν τη στιγμή δεν μπορούμε να φτάσουμε στο διακομιστή, αλλά συνεχίζουμε να "
"προσπαθούμε. Τελευταίος κωδικός σφάλματος: {code}"
#: pretix/static/pretixbase/js/asynctask.js:115
#: pretix/static/pretixcontrol/js/ui/mail.js:20
msgid "The request took to long. Please try again."
msgstr ""
msgstr "Το αίτημα διήρκησε πολύ. Παρακαλώ προσπαθήστε ξανά."
#: pretix/static/pretixbase/js/asynctask.js:127
#: pretix/static/pretixcontrol/js/ui/mail.js:25
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Αυτήν τη στιγμή δεν μπορούμε να συνδεθούμε με το διακομιστή. Παρακαλώ "
"προσπαθήστε ξανά. Κωδικός σφάλματος: {code}"
#: pretix/static/pretixbase/js/asynctask.js:148
msgid "We are processing your request …"
msgstr ""
msgstr "Επεξεργαζόμαστε το αίτημά σας …"
#: pretix/static/pretixbase/js/asynctask.js:156
msgid ""
@@ -97,43 +109,47 @@ msgid ""
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
"Προς το παρόν στέλνουμε το αίτημά σας στο διακομιστή. Αν αυτό διαρκεί "
"περισσότερο από ένα λεπτό, ελέγξτε τη σύνδεσή σας στο διαδίκτυο και στη "
"συνέχεια επαναλάβετε τη φόρτωση αυτής της σελίδας και δοκιμάστε ξανά."
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixcontrol/js/ui/main.js:20
msgid "Close message"
msgstr ""
msgstr "Κλείσιμο μηνύματος"
#: pretix/static/pretixcontrol/js/clipboard.js:23
msgid "Copied!"
msgstr ""
msgstr "Αντιγράφηκε!"
#: pretix/static/pretixcontrol/js/clipboard.js:29
msgid "Press Ctrl-C to copy!"
msgstr ""
msgstr "Πατήστε Ctrl-C για αντιγραφή!"
#: pretix/static/pretixcontrol/js/ui/editor.js:43
msgid "Lead Scan QR"
msgstr ""
msgstr "Οδηγός σάρωσης QR"
#: pretix/static/pretixcontrol/js/ui/editor.js:45
msgid "Check-in QR"
msgstr ""
msgstr "Έλεγχος QR"
#: pretix/static/pretixcontrol/js/ui/editor.js:249
msgid "The PDF background file could not be loaded for the following reason:"
msgstr ""
"Το αρχείο φόντου PDF δεν ήταν δυνατό να φορτωθεί για τον ακόλουθο λόγο:"
#: pretix/static/pretixcontrol/js/ui/editor.js:418
msgid "Group of objects"
msgstr ""
msgstr "Ομάδα αντικειμένων"
#: pretix/static/pretixcontrol/js/ui/editor.js:424
msgid "Text object"
msgstr ""
msgstr "Αντικείμενο κειμένου"
#: pretix/static/pretixcontrol/js/ui/editor.js:426
msgid "Barcode area"
msgstr ""
msgstr "Περιοχή Barcode"
#: pretix/static/pretixcontrol/js/ui/editor.js:428
msgid "Powered by pretix"
@@ -141,65 +157,78 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:430
msgid "Object"
msgstr ""
msgstr "Αντικείμενο"
#: pretix/static/pretixcontrol/js/ui/editor.js:434
msgid "Ticket design"
msgstr ""
msgstr "Σχεδιασμός εισιτηρίων"
#: pretix/static/pretixcontrol/js/ui/editor.js:687
msgid "Saving failed."
msgstr ""
msgstr "Η αποθήκευση απέτυχε."
#: pretix/static/pretixcontrol/js/ui/editor.js:735
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
"Θέλετε πραγματικά να αφήσετε τον επεξεργαστή χωρίς να αποθηκεύσετε τις "
"αλλαγές σας;"
#: pretix/static/pretixcontrol/js/ui/editor.js:749
msgid "Error while uploading your PDF file, please try again."
msgstr ""
msgstr "Σφάλμα κατά τη μεταφόρτωση του αρχείου PDF, δοκιμάστε ξανά."
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr ""
msgstr "Παρουσιάστηκε σφάλμα."
#: pretix/static/pretixcontrol/js/ui/mail.js:52
msgid "Generating messages …"
msgstr ""
msgstr "Δημιουργία μηνυμάτων …"
#: pretix/static/pretixcontrol/js/ui/main.js:55
msgid "Unknown error."
msgstr ""
msgstr "Αγνωστο σφάλμα."
#: pretix/static/pretixcontrol/js/ui/main.js:217
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
"Το χρώμα σας έχει μεγάλη αντίθεση και είναι πολύ εύκολο να το διαβάσετε!"
#: pretix/static/pretixcontrol/js/ui/main.js:221
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Το χρώμα σας έχει αξιοπρεπή αντίθεση και είναι ίσως αρκετά καλό για να "
"διαβάσετε!"
#: pretix/static/pretixcontrol/js/ui/main.js:225
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
"Το χρώμα σας έχει κακή αντίθεση για κείμενο σε λευκό φόντο, επιλέξτε μια πιο "
"σκούρα σκιά."
#: pretix/static/pretixcontrol/js/ui/main.js:305
msgid "All"
msgstr "Ολα"
#: pretix/static/pretixcontrol/js/ui/main.js:306
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:307
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:595
msgid "Use a different name internally"
msgstr ""
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
#: pretix/static/pretixcontrol/js/ui/main.js:646
#: pretix/static/pretixcontrol/js/ui/main.js:652
msgid "Click to close"
msgstr ""
msgstr "Κάντε κλικ για να κλείσετε"
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
#, fuzzy
#| msgid "Contacting Stripe …"
msgid "Calculating default price…"
msgstr "Επικοινωνία με το Stripe …"
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
@@ -207,15 +236,15 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:71
msgid "Count"
msgstr ""
msgstr "Λογαριασμός"
#: pretix/static/pretixcontrol/js/ui/question.js:120
msgid "Yes"
msgstr ""
msgstr "Ναι"
#: pretix/static/pretixcontrol/js/ui/question.js:121
msgid "No"
msgstr ""
msgstr "Όχι"
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
msgid "(one more date)"
@@ -225,11 +254,11 @@ msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
msgstr ""
msgstr "Τα αντικείμενα στο καλάθι σας δεν είναι πλέον αποκλειστικά για εσάς."
#: pretix/static/pretixpresale/js/ui/cart.js:41
msgid "Cart expired"
msgstr ""
msgstr "Το καλάθι έληξε"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
@@ -237,229 +266,242 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/widget/widget.js:14
msgctxt "widget"
msgid "Sold out"
#: pretix/static/pretixpresale/js/ui/main.js:201
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:15
msgctxt "widget"
msgid "Buy"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Reserved"
msgstr ""
msgid "Buy"
msgstr "Αγορά"
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "FREE"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
msgid "FREE"
msgstr "ΔΩΡΕΑΝ"
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "incl. taxes"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "plus taxes"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Waiting list"
msgstr ""
msgstr "Λίστα αναμονής"
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
"Αυτήν τη στιγμή έχετε ένα ενεργό καλάθι για αυτό το συμβάν. Αν επιλέξετε "
"περισσότερα προϊόντα, θα προστεθούν στο υπάρχον καλάθι σας."
#: pretix/static/pretixpresale/js/widget/widget.js:33
msgctxt "widget"
msgid "Resume checkout"
msgstr "Συνεχίστε την ολοκλήρωση της αγοράς"
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
"ticketing powered by pretix</a>"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
"ticketing powered by pretix</a>"
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Redeem"
msgstr ""
msgid "Redeem a voucher"
msgstr "Εξαργυρώστε ένα κουπόνι"
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Voucher code"
msgstr ""
msgid "Redeem"
msgstr "Εξαργυρώστε"
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Close"
msgstr ""
msgid "Voucher code"
msgstr "Κωδικός κουπονιού"
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Continue"
msgstr ""
msgid "Close"
msgstr "Κλείσιμο"
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "See variations"
msgstr ""
msgid "Continue"
msgstr "Συνέχεια"
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
msgid "See variations"
msgstr "Δείτε παραλλαγές"
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Back"
msgstr ""
msgid "Choose a different event"
msgstr "Επιλέξτε διαφορετική εκδήλωση"
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Next month"
msgstr ""
msgid "Choose a different date"
msgstr "Επιλέξτε διαφορετική ημερομηνία"
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Previous month"
msgstr ""
msgid "Back"
msgstr "Πίσω"
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Next month"
msgstr "Επόμενος μήνας"
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgid "Tu"
msgstr ""
msgctxt "widget"
msgid "Previous month"
msgstr "Προηγούμενος μήνας"
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgid "We"
msgstr ""
msgid "Mo"
msgstr "Δευ"
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgid "Th"
msgstr ""
msgid "Tu"
msgstr "Τρι"
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgid "Fr"
msgstr ""
msgid "We"
msgstr "Τετ"
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgid "Sa"
msgstr ""
msgid "Th"
msgstr "Πεμ"
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Fr"
msgstr "Παρ"
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Sa"
msgstr "Σαβ"
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgid "February"
msgstr ""
msgstr "Κυρ"
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "March"
msgstr ""
msgid "January"
msgstr "Ιανουάριος"
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "April"
msgstr ""
msgid "February"
msgstr "Φεβρουάριος"
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "May"
msgstr ""
msgid "March"
msgstr "Μάρτιος"
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "June"
msgstr ""
msgid "April"
msgstr "Απρίλιος"
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "July"
msgstr ""
msgid "May"
msgstr "Μάιος"
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "August"
msgstr ""
msgid "June"
msgstr "Ιούνιος"
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "September"
msgstr ""
msgid "July"
msgstr "Ιούλιος"
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "October"
msgstr ""
msgid "August"
msgstr "Αύγουστος"
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "November"
msgstr ""
msgid "September"
msgstr "Σεπτέμβριος"
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "October"
msgstr "Οκτώβριος"
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "November"
msgstr "Νοέμβριος"
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "December"
msgstr ""
msgstr "Δεκέμβριος"

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