Compare commits

...

218 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

powered by weblate
2023-05-02 10:58:22 +02:00
Raphael Michel 3810dcd5b8 Waiting list: Optionally allow multiple entries per email (#3277)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-05-02 10:27:56 +02:00
Raphael Michel fa4cdbfe4a Fix #3281 -- Docker build broken 2023-05-02 10:13:38 +02:00
Richard Schreiber 0e008812c3 Control/Widget: improve empty label for dates dropdown 2023-05-02 10:10:13 +02:00
246 changed files with 166077 additions and 89881 deletions
+49
View File
@@ -0,0 +1,49 @@
name: Build
on:
push:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
permissions:
contents: read # to fetch code (actions/checkout)
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ubuntu-22.04
name: Packaging
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext unzip
- name: Install Python dependencies
run: pip3 install -U setuptools build pip check-manifest
- name: Run check-manifest
run: check-manifest
- name: Run build
run: python -m build
- name: Check files
run: unzip -l dist/pretix*whl | grep node_modules || exit 1
+1 -1
View File
@@ -30,7 +30,7 @@ pypi:
- make npminstall
- cd ..
- check-manifest
- python setup.py sdist bdist_wheel
- python -m build
- twine check dist/*
- twine upload dist/*
tags:
+11 -13
View File
@@ -41,18 +41,6 @@ RUN apt-get update && \
ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings
# To copy only the requirements files needed to install from PIP
COPY src/setup.py /pretix/src/setup.py
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix/src && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached,mysql]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
COPY deployment/docker/supervisord /etc/supervisord
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
@@ -60,9 +48,19 @@ COPY deployment/docker/supervisord.web.conf /etc/supervisord.web.conf
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
COPY deployment/docker/nginx-max-body-size.conf /etc/nginx/conf.d/nginx-max-body-size.conf
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY pyproject.toml /pretix/pyproject.toml
COPY _build /pretix/_build
COPY src /pretix/src
RUN cd /pretix/src && python setup.py install
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached,mysql]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \
+10 -9
View File
@@ -1,6 +1,7 @@
include LICENSE
include README.rst
include src/Makefile
include _build/backend.py
global-include *.proto
recursive-include src/pretix/static *
recursive-include src/pretix/static.dist *
@@ -32,15 +33,15 @@ recursive-include src/pretix/plugins/returnurl/templates *
recursive-include src/pretix/plugins/returnurl/static *
recursive-include src/pretix/plugins/webcheckin/templates *
recursive-include src/pretix/plugins/webcheckin/static *
recursive-include src *.cfg
recursive-include src *.csv
recursive-include src *.gitkeep
recursive-include src *.jpg
recursive-include src *.json
recursive-include src *.py
recursive-include src *.svg
recursive-include src *.txt
recursive-include src Makefile
recursive-include src *.cfg
recursive-include src *.csv
recursive-include src *.gitkeep
recursive-include src *.jpg
recursive-include src *.json
recursive-include src *.py
recursive-include src *.svg
recursive-include src *.txt
recursive-include src Makefile
recursive-exclude doc *
recursive-exclude deployment *
+12
View File
@@ -0,0 +1,12 @@
import tomli
from setuptools import build_meta as _orig
from setuptools.build_meta import *
def get_requires_for_build_wheel(config_settings=None):
with open("pyproject.toml", "rb") as f:
p = tomli.load(f)
return [
*_orig.get_requires_for_build_wheel(config_settings),
*p['project']['dependencies']
]
+86 -1
View File
@@ -51,7 +51,7 @@ For our standard docker installation, create the database and user like this::
# sudo -u postgres createuser -P pretix
# sudo -u postgres createdb -O pretix pretix
Make sure that your database listens on the network. If PostgreSQL on the same same host as docker, but not inside a docker container, we recommend that you just listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
Make sure that your database listens on the network. If PostgreSQL on the same same host as docker, but not inside a docker container, we recommend that you listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
listen_addresses = 'localhost,172.17.0.1'
@@ -153,4 +153,89 @@ And you're done! After you've verified everything has been copied correctly, you
.. note:: Don't forget to update your backup process to back up your PostgreSQL database instead of your MySQL database now.
Troubleshooting
---------------
Peer authentication failed
""""""""""""""""""""""""""
Sometimes you might see an error message like this::
django.db.utils.OperationalError: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: Peer authentication failed for user "pretix"
It is important to understand that PostgreSQL by default offers two types of authentication:
- **Peer authentication**, which works automatically based on the Linux user you are working as. This requires that
the connection is made through a local socket (empty ``host=`` in ``pretix.cfg``) and the name of the PostgreSQL user
and the Linux user are identical.
- Typically, you might run into this error if you accidentally execute ``python -m pretix`` commands as root instead
of the ``pretix`` user.
- **Password authentication**, which requires a username and password and works over network connections. To force
password authentication instead of peer authentication, set ``host=127.0.0.1`` in ``pretix.cfg``.
- You can alter the password on a PostgreSQL shell using the command ``ALTER USER pretix WITH PASSWORD '***';``.
When creating a user with the ``createuser`` command, pass option ``-P`` to set a new password.
- Even with password authentication, PostgreSQL by default only allows local connections. To allow remote connections,
you need to adjust both the ``listen_address`` configuration parameter as well as the ``pg_hba.conf`` file (see above
for an example with the docker networking setup).
Database error: relation does not exist
"""""""""""""""""""""""""""""""""""""""
If you see an error like this::
2023-04-17T19:20:47.744023Z ERROR Database error 42P01: relation "public.pretix_foobar" does not exist
QUERY: ALTER TABLE public.pretix_foobar DROP CONSTRAINT IF EXISTS pretix_foobar_order_id_57e2cb41_fk_pretixbas CASCADE;
2023-04-17T19:20:47.744023Z FATAL Failed to create the schema, see above.
The reason is most likely that in the past, you installed a pretix plugin that you no longer have installed. However,
the database still contains tables of that plugin. If you want to keep the data, reinstall the plugin and re-run the
``migrate`` step from above. If you want to get rid of the data, manually drop the table mentioned in the error message
from your MySQL database::
# mysql -u root pretix
mysql> DROP TABLE pretix_foobar;
Then, retry. You might see a new error message with a new table, which you can handle the same way.
Cleaning out a failed attempt
"""""""""""""""""""""""""""""
You might want to clean your PostgreSQL database before you try again after an error. You can do so like this::
# sudo -u postgres psql pretix
pretix=# DROP SCHEMA public CASCADE;
pretix=# CREATE SCHEMA public;
pretix=# ALTER SCHEMA public OWNER TO pretix;
``pgloader`` crashes with heap exhaustion error
"""""""""""""""""""""""""""""""""""""""""""""""
On some larger databases, we've seen ``pgloader`` crash with error messages similar to this::
Heap exhausted during garbage collection: 16 bytes available, 48 requested.
Or this::
2021-01-04T21:31:17.367000Z ERROR A SB-KERNEL::HEAP-EXHAUSTED-ERROR condition without bindings for heap statistics. (If
you did not expect to see this message, please report it.
2021-01-04T21:31:17.382000Z ERROR The value
NIL
is not of type
NUMBER
when binding SB-KERNEL::X
The ``pgloader`` version distributed for Debian and Ubuntu is compiled with the ``SBCL`` compiler. If compiled with
``CCL``, these bugs go away. Unfortunately, it is pretty hard to compile ``pgloader`` manually with ``CCL``. If you
run into this, we therefore recommend using the docker container provided by the ``pgloader`` maintainers::
sudo docker run --rm -v /tmp:/tmp --network host -it dimitri/pgloader:ccl.latest pgloader /tmp/pretix.load
As peer authentication is not available from inside the container, this requires you to use password-based authentication
in PostgreSQL (see above).
.. _PostgreSQL repositories: https://wiki.postgresql.org/wiki/Apt
+38 -1
View File
@@ -20,6 +20,12 @@ currency string Currency of the
testmode boolean Whether this is a test gift card
expires datetime Expiry date (or ``null``)
conditions string Special terms and conditions for this card (or ``null``)
owner_ticket integer Internal ID of an order position that is the "owner" of
this gift card and can view all transactions. When setting
this field, you can also give the ``secret`` of an order
position.
issuer string Organizer slug of the organizer who created this gift
card and is responsible for it.
===================================== ========================== =======================================================
The gift card transaction resource contains the following public fields:
@@ -35,8 +41,17 @@ value money (string) Transaction amo
event string Event slug, if the gift card was used in the web shop (or ``null``)
order string Order code, if the gift card was used in the web shop (or ``null``)
text string Custom text of the transaction (or ``null``)
info object Additional data about the transaction (or ``null``)
acceptor string Organizer slug of the organizer who created this transaction
(can be ``null`` for all transactions performed before
this field was added.)
===================================== ========================== =======================================================
.. versionchanged:: 4.20
The ``owner_ticket`` and ``issuer`` attributes of the gift card and the ``info`` and ``acceptor`` attributes of the
gift card transaction resource have been added.
Endpoints
---------
@@ -72,6 +87,8 @@ Endpoints
"testmode": false,
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "13.37"
}
]
@@ -81,6 +98,10 @@ Endpoints
:query string secret: Only show gift cards with the given secret.
:query boolean testmode: Filter for gift cards that are (not) in test mode.
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that the ``owner_ticket``
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -113,6 +134,8 @@ Endpoints
"testmode": false,
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "13.37"
}
@@ -157,10 +180,16 @@ Endpoints
"currency": "EUR",
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "13.37"
}
:param organizer: The ``slug`` field of the organizer to create a gift card for
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that the ``owner_ticket``
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:statuscode 201: no error
:statuscode 400: The gift card could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
@@ -205,6 +234,8 @@ Endpoints
"currency": "EUR",
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "14.00"
}
@@ -250,6 +281,8 @@ Endpoints
"testmode": false,
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "15.37"
}
@@ -293,7 +326,11 @@ Endpoints
"value": "50.00",
"event": "democon",
"order": "FXQYW",
"text": null
"text": null,
"acceptor": "bigevents",
"info": {
"created_by": "plugin1"
}
}
]
}
+1
View File
@@ -18,6 +18,7 @@ at :ref:`plugin-docs`.
item_variations
item_bundles
item_add-ons
item_meta_properties
questions
question_options
quotas
+211
View File
@@ -0,0 +1,211 @@
Item Meta Properties
====================
Resource description
--------------------
An Item Meta Property is used to include (event internally relevant) meta information with every item (product). This
could be internal categories like booking positions.
The Item Meta Properties resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Unique ID for this property
name string Name of the property
default string Value of the default option
required boolean If ``true``, this property will have to be assigned a
value in all items of the related event
allowed_values list List of all permitted values for this property,
or ``null`` for no limitation
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/
Returns a list of all Item Meta Properties within a given event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Color",
"default": "red",
"required": true,
"allowed_values": ["red", "green", "blue"]
}
]
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
Returns information on one property, identified by its id.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"id": 1,
"name": "Color",
"default": "red",
"required": true,
"allowed_values": ["red", "green", "blue"]
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the item meta property to retrieve
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/
Creates a new item meta property
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "ref-code",
"default": "abcde",
"required": true,
"allowed_values": null
}
**Example response**:
.. sourcecode:: http
{
"id": 2,
"name": "ref-code",
"default": "abcde",
"required": true,
"allowed_values": null
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:statuscode 201: no error
:statuscode 400: The item meta property could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
Update an item meta property. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide
all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the
fields that you want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/2/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"required": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 2,
"name": "ref-code",
"default": "abcde",
"required": false,
"allowed_values": []
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the item meta property to modify
:statuscode 200: no error
:statuscode 400: The property could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
Delete an item meta property.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the item meta property to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
+10 -10
View File
@@ -91,11 +91,11 @@ Endpoints
:query string updated_since: Only show media updated since a given date.
:query integer linked_orderposition: Only show media linked to the given ticket.
:query integer linked_giftcard: Only show media linked to the given gift card.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -138,11 +138,11 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the medium to fetch
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
+29 -6
View File
@@ -47,6 +47,8 @@ tag string A string that i
comment string An internal comment on the voucher
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
===================================== ========================== =======================================================
@@ -95,6 +97,9 @@ Endpoints
"comment": "",
"seat": null,
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
]
}
@@ -161,7 +166,10 @@ Endpoints
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -198,7 +206,10 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
**Example response**:
@@ -225,7 +236,10 @@ Endpoints
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
:param organizer: The ``slug`` field of the organizer to create a voucher for
@@ -264,7 +278,10 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
},
{
"code": "ASDKLJCYXCASDASD",
@@ -279,7 +296,10 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
},
**Example response**:
@@ -353,7 +373,10 @@ Endpoints
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
:param organizer: The ``slug`` field of the organizer to modify
+2 -2
View File
@@ -13,7 +13,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators
register_ticket_secret_generators, gift_card_transaction_display
Order events
""""""""""""
@@ -21,7 +21,7 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins
"""""""""
+1 -1
View File
@@ -1,4 +1,4 @@
sphinx==6.2.*
sphinx==7.0.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
+1 -1
View File
@@ -1,5 +1,5 @@
-e ../
sphinx==6.2.*
sphinx==7.0.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
+4
View File
@@ -201,6 +201,10 @@ record for the subdomain ``pretix._domainkey`` with the following contents::
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
in this case, as pretix only retries email delivery for a certain time period.
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
+16 -38
View File
@@ -26,7 +26,6 @@ classifiers = [
]
dependencies = [
# Note that many of these are repeated as build-time dependencies down below -- change them too in case of updates!
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.12.*",
@@ -41,9 +40,9 @@ dependencies = [
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
"django-filter==23.1",
"django-filter==23.2",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.4",
"django-formtools==2.4.1",
"django-hierarkey==1.1.*",
"django-hijack==3.3.*",
"django-i18nfield==1.9.*,>=1.9.4",
@@ -52,13 +51,13 @@ dependencies = [
"django-markup",
"django-mysql",
"django-oauth-toolkit==2.2.*",
"django-otp==1.1.*",
"django-phonenumber-field==7.0.*",
"django-otp==1.2.*",
"django-phonenumber-field==7.1.*",
"django-redis==5.2.*",
"django-scopes==1.2.*",
"django-scopes==2.0.*",
"django-statici18n==2.3.*",
"djangorestframework==3.14.*",
"dnspython==2.2.*",
"dnspython==2.3.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
@@ -69,7 +68,7 @@ dependencies = [
"lxml",
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.23.*",
"mt-940==4.30.*",
"oauthlib==3.2.*",
"openpyxl==3.1.*",
"packaging",
@@ -78,12 +77,12 @@ dependencies = [
"PyJWT==2.6.*",
"phonenumberslite==8.13.*",
"Pillow==9.5.*",
"protobuf==4.22.*",
"protobuf==4.23.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.21",
"pycryptodome==3.17.*",
"pypdf==3.8.*",
"pycryptodome==3.18.*",
"pypdf==3.9.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
"python-dateutil==2.8.*",
"python-u2flib-server==4.*",
@@ -91,8 +90,8 @@ dependencies = [
"pyuca",
"qrcode==7.4.*",
"redis==4.5.*,>=4.5.4",
"reportlab==3.6.*",
"requests==2.28.*",
"reportlab==4.0.*",
"requests==2.30.*",
"sentry-sdk==1.15.*",
"sepaxml==2.6.*",
"slimit",
@@ -113,13 +112,9 @@ mysql = ["mysqlclient"]
dev = [
"coverage",
"coveralls",
"django-debug-toolbar==4.0.*",
"django-formset-js-improved==0.5.0.3",
"django-oauth-toolkit==2.2.*",
"flake8==6.0.*",
"freezegun",
"isort==5.12.*",
"oauthlib==3.2.*",
"pep8-naming==0.13.*",
"potypo",
"pycodestyle==2.10.*",
@@ -140,31 +135,14 @@ build = "pretix._build:CustomBuild"
build_ext = "pretix._build:CustomBuildExt"
[build-system]
build-backend = "backend"
backend-path = ["_build"]
requires = [
"setuptools",
"setuptools-rust",
"wheel",
"importlib_metadata",
# These are runtime dependencies that we unfortunately need to be import in the step that generates
# all CSS and JS asset files. We should keep their versions in sync with the definition above.
"babel",
"Django==3.2.*,>=3.2.18",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
"django-formtools==2.4",
"django-hierarkey==1.1.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-phonenumber-field==7.0.*",
"django-statici18n==2.3.*",
"djangorestframework==3.14.*",
"libsass==0.22.*",
"phonenumberslite==8.13.*",
"pycountry",
"pyuca",
"slimit",
"tomli",
]
[project.urls]
@@ -182,4 +160,4 @@ version = {attr = "pretix.__version__"}
[tool.setuptools.packages.find]
where = ["src"]
include = ["pretix*"]
namespaces = false
namespaces = false
+25 -1
View File
@@ -19,8 +19,32 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import sys
from pathlib import Path
import setuptools
sys.path.append(str(Path.cwd() / 'src'))
def _CustomBuild(*args, **kwargs):
print(sys.path)
from pretix._build import CustomBuild
return CustomBuild(*args, **kwargs)
def _CustomBuildExt(*args, **kwargs):
from pretix._build import CustomBuildExt
return CustomBuildExt(*args, **kwargs)
cmdclass = {
'build': _CustomBuild,
'build_ext': _CustomBuildExt,
}
if __name__ == "__main__":
setuptools.setup()
setuptools.setup(
cmdclass=cmdclass,
)
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "4.20.0.dev0"
__version__ = "4.21.0.dev0"
+1
View File
@@ -80,6 +80,7 @@ ALL_LANGUAGES = [
('de-informal', _('German (informal)')),
('ar', _('Arabic')),
('zh-hans', _('Chinese (simplified)')),
('zh-hant', _('Chinese (traditional)')),
('cs', _('Czech')),
('da', _('Danish')),
('nl', _('Dutch')),
+1
View File
@@ -201,6 +201,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'),
('PATCH', 'api-v1:giftcard-detail'),
('GET', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'),
+27
View File
@@ -19,3 +19,30 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from rest_framework import serializers
class AsymmetricField(serializers.Field):
def __init__(self, read, write, **kwargs):
self.read = read
self.write = write
super().__init__(
required=self.write.required,
default=self.write.default,
initial=self.write.initial,
source=self.write.source if self.write.source != self.write.field_name else None,
label=self.write.label,
allow_null=self.write.allow_null,
error_messages=self.write.error_messages,
validators=self.write.validators,
**kwargs
)
def to_internal_value(self, data):
return self.write.to_internal_value(data)
def to_representation(self, value):
return self.read.to_representation(value)
def run_validation(self, data=serializers.empty):
return self.write.run_validation(data)
+25 -1
View File
@@ -50,7 +50,9 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.models.items import (
ItemMetaProperty, SubEventItem, SubEventItemVariation,
)
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
@@ -683,6 +685,7 @@ class EventSettingsSerializer(SettingsSerializer):
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'waiting_list_limit_per_user',
'max_items_per_order',
'reservation_time',
'contact_mail',
@@ -765,6 +768,7 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_footer_text',
'invoice_eu_currencies',
'invoice_logo_image',
'invoice_renderer_highlight_order_code',
'cancel_allow_user',
'cancel_allow_user_until',
'cancel_allow_user_unpaid_keep',
@@ -902,3 +906,23 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
else []
)
)
class MultiLineStringField(serializers.Field):
def to_representation(self, value):
return [v.strip() for v in value.splitlines()]
def to_internal_value(self, data):
if isinstance(data, list) and len(data) > 0:
return "\n".join(data)
else:
raise ValidationError('Invalid data type.')
class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
allowed_values = MultiLineStringField(allow_null=True)
class Meta:
model = ItemMetaProperty
fields = ('id', 'name', 'default', 'required', 'allowed_values')
+3 -1
View File
@@ -64,7 +64,9 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
super().__init__(*args, **kwargs)
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True)
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
else:
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
required=False,
@@ -70,6 +70,8 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
def validate(self, data):
data = super().validate(data)
if 'order' in self.context:
data['order'] = self.context['order']
if data.get('addon_to'):
try:
data['addon_to'] = data['order'].positions.get(positionid=data['addon_to'])
+52 -4
View File
@@ -22,12 +22,14 @@
import logging
from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers import AsymmetricField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.api.serializers.settings import SettingsSerializer
@@ -35,8 +37,8 @@ from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region
from pretix.base.models import (
Customer, Device, GiftCard, GiftCardTransaction, Membership,
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User,
MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan,
Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail
@@ -127,8 +129,52 @@ class MembershipSerializer(I18nAwareModelSerializer):
return super().update(instance, validated_data)
class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
queryset = self.get_queryset()
if isinstance(data, int):
try:
return queryset.get(pk=data)
except ObjectDoesNotExist:
self.fail('does_not_exist', pk_value=data)
elif isinstance(data, str):
try:
return queryset.get(
Q(secret=data)
| Q(pseudonymization_id=data)
| Q(pk__in=ReusableMedium.objects.filter(
organizer=self.context['organizer'],
type='barcode',
identifier=data
))
)
except ObjectDoesNotExist:
self.fail('does_not_exist', pk_value=data)
self.fail('incorrect_type', data_type=type(data).__name__)
class GiftCardSerializer(I18nAwareModelSerializer):
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
issuer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['owner_ticket'].queryset = OrderPosition.objects.filter(order__event__organizer=self.context['organizer'])
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
from pretix.api.serializers.media import (
NestedOrderPositionSerializer,
)
self.fields['owner_ticket'] = AsymmetricField(
NestedOrderPositionSerializer(read_only=True, context=self.context),
self.fields['owner_ticket'],
)
def validate(self, data):
data = super().validate(data)
@@ -151,7 +197,8 @@ class GiftCardSerializer(I18nAwareModelSerializer):
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
'issuer')
class OrderEventSlugField(serializers.RelatedField):
@@ -162,11 +209,12 @@ class OrderEventSlugField(serializers.RelatedField):
class GiftCardTransactionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
acceptor = serializers.SlugRelatedField(slug_field='slug', read_only=True)
event = OrderEventSlugField(source='order', read_only=True)
class Meta:
model = GiftCardTransaction
fields = ('id', 'datetime', 'value', 'event', 'order', 'text')
fields = ('id', 'datetime', 'value', 'event', 'order', 'text', 'info', 'acceptor')
class EventSlugField(serializers.SlugRelatedField):
+2 -1
View File
@@ -63,7 +63,8 @@ class VoucherSerializer(I18nAwareModelSerializer):
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
'all_bundles_included')
read_only_fields = ('id', 'redeemed')
list_serializer_class = VoucherListSerializer
+1 -1
View File
@@ -39,7 +39,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
WaitingListEntry.clean_duplicate(full_data.get('email'), full_data.get('item'), full_data.get('variation'),
WaitingListEntry.clean_duplicate(event, full_data.get('email'), full_data.get('item'), full_data.get('variation'),
full_data.get('subevent'), self.instance.pk if self.instance else None)
WaitingListEntry.clean_itemvar(event, full_data.get('item'), full_data.get('variation'))
WaitingListEntry.clean_subevent(event, full_data.get('subevent'))
+1
View File
@@ -89,6 +89,7 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
event_router.register(r'cartpositions', cart.CartPositionViewSet)
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
+3 -1
View File
@@ -562,7 +562,9 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
else:
op_candidates = [media.linked_orderposition] + list(media.linked_orderposition.addons.all())
op_candidates = [media.linked_orderposition]
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
op_candidates += list(media.linked_orderposition.addons.all())
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
+3
View File
@@ -93,6 +93,9 @@ class InitializeView(APIView):
if device.initialized:
raise ValidationError({'token': ['This initialization token has already been used.']})
if device.revoked:
raise ValidationError({'token': ['This initialization token has been revoked.']})
device.initialized = now()
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
+52 -2
View File
@@ -47,11 +47,13 @@ from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
CartPosition, Device, Event, SeatCategoryMapping, TaxRule, TeamAPIToken,
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
TaxRule, TeamAPIToken,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.quotas import QuotaAvailability
@@ -522,6 +524,54 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
super().perform_destroy(instance)
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = ItemMetaPropertiesSerializer
queryset = ItemMetaProperty.objects.none()
write_permission = 'can_change_event_settings'
def get_queryset(self):
qs = self.request.event.item_meta_properties.all()
return qs
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.item_meta_property.deleted',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk}
)
instance.delete()
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.item_meta_property.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.item_meta_property.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
class EventSettingsView(views.APIView):
permission = None
write_permission = 'can_change_event_settings'
+36 -17
View File
@@ -155,7 +155,9 @@ class GiftCardViewSet(viewsets.ModelViewSet):
qs = self.request.organizer.accepted_gift_cards
else:
qs = self.request.organizer.issued_gift_cards.all()
return qs
return qs.prefetch_related(
'issuer'
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -166,7 +168,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
value = serializer.validated_data.pop('value')
inst = serializer.save(issuer=self.request.organizer)
inst.transactions.create(value=value)
inst.transactions.create(value=value, acceptor=self.request.organizer)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
@@ -179,18 +181,32 @@ class GiftCardViewSet(viewsets.ModelViewSet):
if 'include_accepted' in self.request.GET:
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
old_value = serializer.instance.value
value = serializer.validated_data.pop('value')
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode)
diff = value - old_value
inst.transactions.create(value=diff)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': diff}
)
value = serializer.validated_data.pop('value', None)
if any(k != 'value' for k in self.request.data):
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode)
inst.log_action(
'pretix.giftcards.modified',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
else:
inst = serializer.instance
if 'value' in self.request.data and value is not None:
old_value = serializer.instance.value
diff = value - old_value
inst.transactions.create(value=diff, acceptor=self.request.organizer)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': diff}
)
return inst
@action(detail=True, methods=["POST"])
@@ -203,18 +219,21 @@ class GiftCardViewSet(viewsets.ModelViewSet):
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
request.data.get('text', '')
)
info = serializers.JSONField(required=False, allow_null=True).to_internal_value(
request.data.get('info', {})
)
if gc.value + value < Decimal('0.00'):
return Response({
'value': ['The gift card does not have sufficient credit for this operation.']
}, status=status.HTTP_409_CONFLICT)
gc.transactions.create(value=value, text=text)
gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer)
gc.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': value, 'text': text}
)
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
def perform_destroy(self, instance):
raise MethodNotAllowed("Gift cards cannot be deleted.")
@@ -235,7 +254,7 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
return get_object_or_404(qs, pk=self.kwargs.get('giftcard'))
def get_queryset(self):
return self.giftcard.transactions.select_related('order', 'order__event')
return self.giftcard.transactions.select_related('order', 'order__event').prefetch_related('acceptor')
class TeamViewSet(viewsets.ModelViewSet):
+9 -7
View File
@@ -117,13 +117,15 @@ def oidc_validate_and_complete_config(config):
scopes=", ".join(provider_config.get("scopes_supported", []))
))
for k, v in config.items():
if k.endswith('_field') and v:
if v not in provider_config.get("claims_supported", []): # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
field=v,
fields=", ".join(provider_config.get("claims_supported", []))
))
if "claims_supported" in provider_config:
claims_supported = provider_config.get("claims_supported", [])
for k, v in config.items():
if k.endswith('_field') and v:
if v not in claims_supported: # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
field=v,
fields=", ".join(provider_config.get("claims_supported", []))
))
config['provider_config'] = provider_config
return config
+8 -5
View File
@@ -58,6 +58,7 @@ class EventDataExporter(ListExporter):
_("Short form"),
_("Shop is live"),
_("Event currency"),
_("Timezone"),
_("Event start time"),
_("Event end time"),
_("Admission time"),
@@ -75,16 +76,18 @@ class EventDataExporter(ListExporter):
for e in self.events.all():
m = e.meta_data
tz = e.timezone
yield [
str(e.name),
e.slug,
_('Yes') if e.live else _('No'),
e.currency,
date_format(e.date_from, 'SHORT_DATETIME_FORMAT'),
date_format(e.date_to, 'SHORT_DATETIME_FORMAT') if e.date_to else '',
date_format(e.date_admission, 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
date_format(e.presale_start, 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
date_format(e.presale_end, 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
str(e.timezone),
date_format(e.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
date_format(e.date_to.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_to else '',
date_format(e.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
date_format(e.presale_start.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
date_format(e.presale_end.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
str(e.location),
e.geo_lat or '',
e.geo_lon or '',
+3 -1
View File
@@ -103,7 +103,9 @@ class InvoiceExporterMixin:
qs = qs.annotate(
has_payment_with_provider=Exists(
OrderPayment.objects.filter(
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider'))
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider')),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
)
)
)
+192 -1
View File
@@ -49,18 +49,24 @@ from django.utils.timezone import get_current_timezone, now
from django.utils.translation import (
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
)
from openpyxl.cell import WriteOnlyCell
from openpyxl.comments import Comment
from openpyxl.styles import Font, PatternFill
from pretix.base.models import (
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
OrderPosition, Question,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.models.orders import (
OrderFee, OrderPayment, OrderRefund, Transaction,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
@@ -759,6 +765,181 @@ class OrderListExporter(MultiSheetListExporter):
return '{}_orders'.format(self.event.slug)
class TransactionListExporter(ListExporter):
identifier = 'transactions'
verbose_name = gettext_lazy('Order transaction data')
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all substantial changes to orders, i.e. all changes to '
'products, prices or tax rates. The information is only accurate for changes made with '
'pretix versions released after October 2021.')
@cached_property
def providers(self):
return dict(get_all_payment_providers())
@property
def additional_form_fields(self):
d = [
('date_range',
DateFrameField(
label=_('Date range'),
include_future_frames=False,
required=False,
help_text=_('Only include transactions created within this date range.')
)),
]
d = OrderedDict(d)
return d
@cached_property
def event_object_cache(self):
return {e.pk: e for e in self.events}
def get_filename(self):
if self.is_multievent:
return '{}_transactions'.format(self.organizer.slug)
else:
return '{}_transactions'.format(self.event.slug)
def iterate_list(self, form_data):
qs = Transaction.objects.filter(
order__event__in=self.events,
)
if form_data.get('date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
if dt_start:
qs = qs.filter(datetime__gte=dt_start)
if dt_end:
qs = qs.filter(datetime__lt=dt_end)
qs = qs.select_related(
'order', 'order__event', 'item', 'variation', 'subevent',
).order_by(
'datetime', 'id',
)
headers = [
_('Event'),
_('Event slug'),
_('Currency'),
_('Order code'),
_('Order date'),
_('Order time'),
_('Transaction date'),
_('Transaction time'),
_('Old data'),
_('Position ID'),
_('Quantity'),
_('Product ID'),
_('Product'),
_('Variation ID'),
_('Variation'),
_('Fee type'),
_('Internal fee type'),
pgettext('subevent', 'Date ID'),
pgettext('subevent', 'Date'),
_('Price'),
_('Tax rate'),
_('Tax rule ID'),
_('Tax rule'),
_('Tax value'),
]
if form_data.get('_format') == 'xlsx':
for i in range(len(headers)):
headers[i] = WriteOnlyCell(self.__ws, value=headers[i])
if i in (0, 12, 14, 18, 22):
headers[i].fill = PatternFill(start_color="FFB419", end_color="FFB419", fill_type="solid")
headers[i].comment = Comment(
text=_(
"This value is supplied for informational purposes, it is not part of the original transaction "
"data and might have changed since the transaction."
),
author='system'
)
headers[i].font = Font(bold=True)
yield headers
yield self.ProgressSetTotal(total=qs.count())
for t in qs.iterator():
row = [
str(t.order.event.name),
t.order.event.slug,
t.order.event.currency,
t.order.code,
t.order.datetime.astimezone(self.timezone).strftime('%Y-%m-%d'),
t.order.datetime.astimezone(self.timezone).strftime('%H:%M:%S'),
t.datetime.astimezone(self.timezone).strftime('%Y-%m-%d'),
t.datetime.astimezone(self.timezone).strftime('%H:%M:%S'),
_('Converted from legacy version') if t.migrated else '',
t.positionid,
t.count,
t.item_id,
str(t.item),
t.variation_id or '',
str(t.variation) if t.variation_id else '',
t.fee_type,
t.internal_type,
t.subevent_id or '',
str(t.subevent) if t.subevent else '',
t.price,
t.tax_rate,
t.tax_rule_id or '',
str(t.tax_rule.internal_name or t.tax_rule.name) if t.tax_rule_id else '',
t.tax_value,
]
if form_data.get('_format') == 'xlsx':
for i in range(len(row)):
if t.order.testmode:
row[i] = WriteOnlyCell(self.__ws, value=remove_invalid_excel_chars(row[i]))
row[i].fill = PatternFill(start_color="FFB419", end_color="FFB419", fill_type="solid")
yield row
def prepare_xlsx_sheet(self, ws):
self.__ws = ws
ws.freeze_panes = 'A2'
ws.column_dimensions['A'].width = 25
ws.column_dimensions['B'].width = 10
ws.column_dimensions['C'].width = 10
ws.column_dimensions['D'].width = 10
ws.column_dimensions['E'].width = 15
ws.column_dimensions['F'].width = 15
ws.column_dimensions['G'].width = 15
ws.column_dimensions['H'].width = 15
ws.column_dimensions['I'].width = 15
ws.column_dimensions['J'].width = 10
ws.column_dimensions['K'].width = 10
ws.column_dimensions['L'].width = 10
ws.column_dimensions['M'].width = 25
ws.column_dimensions['N'].width = 10
ws.column_dimensions['O'].width = 25
ws.column_dimensions['P'].width = 20
ws.column_dimensions['Q'].width = 20
ws.column_dimensions['R'].width = 10
ws.column_dimensions['S'].width = 25
ws.column_dimensions['T'].width = 15
ws.column_dimensions['U'].width = 10
ws.column_dimensions['V'].width = 10
ws.column_dimensions['W'].width = 20
ws.column_dimensions['X'].width = 15
class PaymentListExporter(ListExporter):
identifier = 'paymentlist'
verbose_name = gettext_lazy('Payments and refunds')
@@ -1165,6 +1346,16 @@ def register_multievent_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_ordertransactionlist")
def register_ordertransactionlist_exporter(sender, **kwargs):
return TransactionListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_ordertransactionlist")
def register_multievent_ordertransactionlist_exporter(sender, **kwargs):
return TransactionListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
def register_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter
+7
View File
@@ -573,6 +573,7 @@ class BaseQuestionsForm(forms.Form):
the attendee name for admission tickets, if the corresponding setting is enabled,
as well as additional questions defined by the organizer.
"""
address_validation = False
def __init__(self, *args, **kwargs):
"""
@@ -920,8 +921,14 @@ class BaseQuestionsForm(forms.Form):
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
def clean(self):
from pretix.base.addressvalidation import \
validate_address # local import to prevent impact on startup time
d = super().clean()
if self.address_validation:
self.cleaned_data = d = validate_address(d, True)
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not d.get('state'):
self.add_error('state', _('This field is required.'))
+143 -80
View File
@@ -20,6 +20,8 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import re
import unicodedata
from collections import defaultdict
from decimal import Decimal
from io import BytesIO
@@ -28,6 +30,7 @@ from typing import Tuple
import bleach
import vat_moss.exchange_rates
from bidi.algorithm import get_display
from django.contrib.staticfiles import finders
from django.db.models import Sum
from django.dispatch import receiver
@@ -53,7 +56,8 @@ from pretix.base.models import Event, Invoice, Order, OrderPayment
from pretix.base.services.currencies import SOURCE_NAMES
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ThumbnailingImageReader
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
@@ -79,7 +83,12 @@ class NumberedCanvas(Canvas):
def draw_page_number(self, page_count):
self.saveState()
self.setFont(self.font_regular, 8)
self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,))
text = pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,)
try:
text = get_display(reshaper.reshape(text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, text)
self.restoreState()
@@ -139,8 +148,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
"""
self.stylesheet = self._get_stylesheet()
self._register_fonts()
self.stylesheet = self._get_stylesheet()
def _get_stylesheet(self):
"""
@@ -148,6 +157,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='Bold', fontName=self.font_bold, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='BoldRight', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT))
stylesheet.add(ParagraphStyle(name='BoldRightNoSplit', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT,
splitLongWords=False))
stylesheet.add(ParagraphStyle(name='NormalRight', fontName=self.font_regular, fontSize=10, leading=12, alignment=TA_RIGHT))
stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
textColor=colors.white, alignment=TA_CENTER))
@@ -155,6 +168,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10))
stylesheet.add(ParagraphStyle(name='FineprintRight', fontName=self.font_regular, fontSize=8, leading=10, alignment=TA_RIGHT))
return stylesheet
def _register_fonts(self):
@@ -168,6 +182,32 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
for family, styles in get_fonts().items():
if family == self.event.settings.invoice_renderer_font:
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
self.font_regular = family
if 'italic' in styles:
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
if 'bold' in styles:
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
self.font_bold = family + ' B'
if 'bolditalic' in styles:
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
def _normalize(self, text):
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
def _upper(self, val):
# We uppercase labels, but not in every language
if get_language().startswith('el'):
@@ -247,10 +287,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None):
return bleach.clean(
return self._normalize(bleach.clean(
text,
tags=tags or []
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
class PaidMarker(Flowable):
@@ -291,7 +331,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
canvas.restoreState()
@@ -324,13 +364,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_invoice_from_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Invoice from')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice from'))))
canvas.drawText(textobject)
def _draw_invoice_to_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Invoice to')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice to'))))
canvas.drawText(textobject)
logo_width = 25 * mm
@@ -358,51 +398,51 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_metadata(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Order code')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Order code'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.order.full_code)
textobject.textLine(self._normalize(self.invoice.order.full_code))
canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8)
if self.invoice.is_cancellation:
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation number')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation number'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
textobject.textLine(self._normalize(self.invoice.number))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.refers.number)
textobject.textLine(self._normalize(self.invoice.refers.number))
else:
textobject.textLine(self._upper(pgettext('invoice', 'Invoice number')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice number'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
textobject.textLine(self._normalize(self.invoice.number))
textobject.moveCursor(0, 5)
if self.invoice.is_cancellation:
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation date')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation date'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice date')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice date'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
textobject.textLine(self._normalize(date_format(self.invoice.refers.date, "DATE_FORMAT")))
textobject.moveCursor(0, 5)
else:
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Invoice date')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice date'))))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
textobject.moveCursor(0, 5)
canvas.drawText(textobject)
@@ -415,19 +455,19 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event_label(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event'))))
canvas.drawText(textobject)
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=[]).strip()
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
return txt
@@ -453,7 +493,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else:
p_str = shorten(self.invoice.event.name)
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p = Paragraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
@@ -462,14 +502,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_footer(self, canvas):
canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
def _draw_testmode(self, canvas):
if self.invoice.order.testmode:
canvas.saveState()
canvas.setFont('OpenSansBd', 30)
canvas.setFont(self.font_bold, 30)
canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, self._normalize(gettext('TEST MODE')))
canvas.restoreState()
def _on_first_page(self, canvas: Canvas, doc):
@@ -517,22 +557,22 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference),
),
)),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_vat_id:
story.append(Paragraph(
pgettext('invoice', 'Customer VAT ID') + ': ' +
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
self._clean_text(self.invoice.invoice_to_vat_id),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(Paragraph(
pgettext('invoice', 'Beneficiary') + ':<br />' +
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal']
))
@@ -552,10 +592,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [
NextPageTemplate('FirstPage'),
Paragraph(
(
self._normalize(
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
self.stylesheet['Heading1']
),
Spacer(1, 5 * mm),
@@ -577,17 +617,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
]
if has_taxes:
tdata = [(
pgettext('invoice', 'Description'),
pgettext('invoice', 'Qty'),
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net'),
pgettext('invoice', 'Gross'),
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
)]
else:
tdata = [(
pgettext('invoice', 'Description'),
pgettext('invoice', 'Qty'),
pgettext('invoice', 'Amount'),
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
)]
def _group_key(line):
@@ -634,13 +674,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes:
tdata.append([
pgettext('invoice', 'Invoice total'), '', '', '',
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
tdata.append([
pgettext('invoice', 'Invoice total'), '',
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.65, .20, .15)]
@@ -649,12 +689,16 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum - total, self.invoice.event.currency)
])
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum, self.invoice.event.currency)
])
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)]
)
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)]
)
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
@@ -667,19 +711,24 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
).aggregate(
s=Sum('amount')
)['s'] or Decimal('0.00')
tdata.append([pgettext('invoice', 'Paid by gift card')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(giftcard_sum, self.invoice.event.currency)
])
tdata.append([pgettext('invoice', 'Remaining amount')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(total - giftcard_sum, self.invoice.event.currency)
])
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)]
)
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
)
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
elif self.invoice.payment_provider_stamp:
pm = PaidMarker(
text=self.invoice.payment_provider_stamp,
text=self._normalize(self.invoice.payment_provider_stamp),
color=colors.HexColor(self.event.settings.theme_color_success),
font=self.font_bold,
size=16
)
tdata[-1][-2] = pm
@@ -692,7 +741,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.payment_provider_text:
story.append(Paragraph(
self.invoice.payment_provider_text,
self._normalize(self.invoice.payment_provider_text),
self.stylesheet['Normal']
))
@@ -716,10 +765,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]
thead = [
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net value'),
pgettext('invoice', 'Gross value'),
pgettext('invoice', 'Tax'),
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
Paragraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
Paragraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
Paragraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
''
]
tdata = [thead]
@@ -730,7 +779,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue
tax = taxvalue_map[idx]
tdata.append([
localize(rate) + " % " + name,
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency),
@@ -749,7 +798,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
Paragraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
table
]))
@@ -766,7 +815,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax
tdata.append([
localize(rate) + " % " + name,
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), ''
])
@@ -776,12 +825,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([
Spacer(1, height=2 * mm),
Paragraph(
pgettext(
self._normalize(pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
self.stylesheet['Fineprint']
),
Spacer(1, height=3 * mm),
@@ -790,14 +839,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm))
story.append(Paragraph(
story.append(Paragraph(self._normalize(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total)),
total=fmt(foreign_total))),
self.stylesheet['Fineprint']
))
@@ -843,7 +892,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
self._clean_text(l)
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
p = Paragraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas)
@@ -859,8 +908,12 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _get_first_page_frames(self, doc):
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
if self.event.settings.invoice_renderer_highlight_order_code:
margin_top = 100 * mm
else:
margin_top = 95 * mm
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 95 * mm,
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - margin_top,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
@@ -871,25 +924,35 @@ class Modern1Renderer(ClassicInvoiceRenderer):
# the font size until it fits.
begin_top = 100 * mm
def _draw(label, value, value_size, x, width):
def _draw(label, value, value_size, x, width, bold=False, sublabel=None):
if canvas.stringWidth(value, self.font_regular, value_size) > width and value_size > 6:
return False
textobject = canvas.beginText(x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(label)
textobject.textLine(self._normalize(label))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, value_size)
textobject.textLine(value)
textobject.setFont(self.font_bold if bold else self.font_regular, value_size)
textobject.textLine(self._normalize(value))
if sublabel:
textobject.moveCursor(0, 1)
textobject.setFont(self.font_regular, 8)
textobject.textLine(self._normalize(sublabel))
return textobject
value_size = 10
while value_size >= 5:
if self.event.settings.invoice_renderer_highlight_order_code:
kwargs = dict(bold=True, sublabel=pgettext('invoice', '(Please quote at all times.)'))
else:
kwargs = {}
objects = [
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm)
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
]
p = Paragraph(
date_format(self.invoice.date, "DATE_FORMAT"),
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
)
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -920,9 +983,9 @@ class Modern1Renderer(ClassicInvoiceRenderer):
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
if self.invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation date'))
textobject.textLine(self._normalize(pgettext('invoice', 'Cancellation date')))
else:
textobject.textLine(pgettext('invoice', 'Invoice date'))
textobject.textLine(self._normalize(pgettext('invoice', 'Invoice date')))
canvas.drawText(textobject)
@@ -51,7 +51,7 @@ class Command(BaseCommand):
del options['skip_checks']
del options['print_sql']
if options['print_sql']:
if options.get('print_sql'):
connection.force_debug_cursor = True
logger = logging.getLogger("django.db.backends")
logger.setLevel(logging.DEBUG)
@@ -0,0 +1,19 @@
# Generated by Django 3.2.18 on 2023-05-04 12:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0237_question_valid_string_length'),
]
operations = [
migrations.AddField(
model_name='giftcard',
name='owner_ticket',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='owned_gift_cards', to='pretixbase.orderposition'),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 3.2.18 on 2023-05-11 11:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0238_giftcard_owner_ticket'),
]
operations = [
migrations.AddField(
model_name='giftcardtransaction',
name='acceptor',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='gift_card_transactions', to='pretixbase.organizer'),
),
migrations.AddField(
model_name='giftcardtransaction',
name='info',
field=models.JSONField(null=True),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 3.2.18 on 2023-05-16 11:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0239_giftcard_info'),
]
operations = [
migrations.AddField(
model_name='voucher',
name='all_addons_included',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='voucher',
name='all_bundles_included',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 3.2.19 on 2023-05-25 15:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0240_auto_20230516_1119'),
]
operations = [
migrations.AddField(
model_name='itemmetaproperty',
name='allowed_values',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='itemmetaproperty',
name='required',
field=models.BooleanField(default=False),
),
]
+1 -1
View File
@@ -178,7 +178,7 @@ class LoggedModel(models.Model, LoggingMixin):
return LogEntry.objects.filter(
content_type=self.logs_content_type, object_id=self.pk
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')
).select_related('user', 'event', 'event__organizer', 'oauth_application', 'api_token', 'device')
class LockModel:
+11 -18
View File
@@ -39,6 +39,7 @@ from pretix.base.models.fields import MultiStringField
from pretix.base.models.organizer import Organizer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.helpers.countries import FastCountryField
from pretix.helpers.names import build_name
class CustomerSSOProvider(LoggedModel):
@@ -171,15 +172,11 @@ class Customer(LoggedModel):
@property
def name(self):
if not self.name_parts:
return ""
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
raise TypeError("Invalid name given.")
return scheme['concatenation'](self.name_parts).strip()
return build_name(self.name_parts, fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
@property
def name_all_components(self):
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
def __str__(self):
s = f'#{self.identifier}'
@@ -302,15 +299,11 @@ class AttendeeProfile(models.Model):
@property
def attendee_name(self):
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
@property
def attendee_name_all_components(self):
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
@property
def state_name(self):
+24 -10
View File
@@ -290,19 +290,19 @@ class EventMixin:
return safe_string(json.dumps(eventdict))
@classmethod
def annotated(cls, qs, channel='web'):
def annotated(cls, qs, channel='web', voucher=None):
from pretix.base.models import Item, ItemVariation, Quota
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel).filter(
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
Q(variations__isnull=True)
& Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
sq_active_variation = ItemVariation.objects.filter(
q_variation = (
Q(active=True)
& Q(sales_channels__contains=channel)
& Q(hide_without_voucher=False)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(item__active=True)
@@ -310,10 +310,23 @@ class EventMixin:
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
& Q(item__sales_channels__contains=channel)
& Q(item__hide_without_voucher=False)
& Q(item__require_bundling=False)
& Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
)
if voucher:
if voucher.variation_id:
q_variation &= Q(pk=voucher.variation_id)
elif voucher.item_id:
q_variation &= Q(item_id=voucher.item_id)
elif voucher.quota_id:
q_variation &= Q(quotas__in=[voucher.quota_id])
if not voucher or not voucher.show_hidden_items:
q_variation &= Q(hide_without_voucher=False)
q_variation &= Q(item__hide_without_voucher=False)
sq_active_variation = ItemVariation.objects.filter(q_variation).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
@@ -625,6 +638,7 @@ class Event(EventMixin, LoggedModel):
"""
self.settings.invoice_renderer = 'modern1'
self.settings.invoice_include_expire_date = True
self.settings.invoice_renderer_highlight_order_code = True
self.settings.ticketoutput_pdf__enabled = True
self.settings.ticketoutput_passbook__enabled = True
self.settings.event_list_type = 'calendar'
@@ -1132,8 +1146,8 @@ class Event(EventMixin, LoggedModel):
irs = self.get_invoice_renderers()
return irs[self.settings.invoice_renderer]
def subevents_annotated(self, channel):
return SubEvent.annotated(self.subevents, channel)
def subevents_annotated(self, channel, voucher=None):
return SubEvent.annotated(self.subevents, channel, voucher)
def subevents_sorted(self, queryset):
ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
@@ -1453,10 +1467,10 @@ class SubEvent(EventMixin, LoggedModel):
return qs_annotated
@classmethod
def annotated(cls, qs, channel='web'):
def annotated(cls, qs, channel='web', voucher=None):
from .items import SubEventItem, SubEventItemVariation
qs = super().annotated(qs, channel)
qs = super().annotated(qs, channel, voucher=voucher)
qs = qs.annotate(
disabled_items=Coalesce(
Subquery(
+64
View File
@@ -25,7 +25,9 @@ from django.conf import settings
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Sum
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.html import format_html
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
@@ -66,6 +68,13 @@ class GiftCard(LoggedModel):
on_delete=models.PROTECT,
null=True, blank=True
)
owner_ticket = models.ForeignKey(
'OrderPosition',
related_name='owned_gift_cards',
on_delete=models.PROTECT,
null=True, blank=True,
verbose_name=_('Owned by ticket holder')
)
issuance = models.DateTimeField(
auto_now_add=True,
)
@@ -153,6 +162,61 @@ class GiftCardTransaction(models.Model):
on_delete=models.PROTECT
)
text = models.TextField(blank=True, null=True)
info = models.JSONField(
null=True, blank=True,
)
acceptor = models.ForeignKey(
'Organizer',
related_name='gift_card_transactions',
on_delete=models.PROTECT,
null=True, blank=True
)
class Meta:
ordering = ("datetime",)
def save(self, *args, **kwargs):
if not self.pk and not self.acceptor:
raise ValueError("`acceptor` should be set on all new gift card transactions.")
super().save(*args, **kwargs)
def display(self, customer_facing=True):
from ..signals import gift_card_transaction_display
for receiver, response in gift_card_transaction_display.send(self, transaction=self, customer_facing=customer_facing):
if response:
return response
if self.order_id:
if not self.text:
if not customer_facing:
return format_html(
'<a href="{}">{}</a>',
reverse(
"control:event.order",
kwargs={
"event": self.order.event.slug,
"organizer": self.order.event.organizer.slug,
"code": self.order.code,
}
),
self.order.full_code
)
return self.order.full_code
else:
return self.text
else:
if self.text:
return format_html(
'<em>{}:</em> {}',
_('Manual transaction'),
self.text,
)
else:
return _('Manual transaction')
def display_backend(self):
return self.display(customer_facing=False)
def display_presale(self):
return self.display(customer_facing=True)
+9
View File
@@ -2001,6 +2001,15 @@ class ItemMetaProperty(LoggedModel):
verbose_name=_("Name"),
)
default = models.TextField(blank=True)
required = models.BooleanField(
default=False, verbose_name=_("Required for products"),
help_text=_("If checked, this property must be set in each product. Does not apply if a default value is set.")
)
allowed_values = models.TextField(
null=True, blank=True,
verbose_name=_("Valid values"),
help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
)
class Meta:
ordering = ("name",)
+2 -10
View File
@@ -31,7 +31,7 @@ from i18nfield.fields import I18nCharField
from pretix.base.models import Customer
from pretix.base.models.base import LoggedModel
from pretix.base.models.organizer import Organizer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.helpers.names import build_name
class MembershipType(LoggedModel):
@@ -160,15 +160,7 @@ class Membership(models.Model):
@property
def attendee_name(self):
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
def is_valid(self, ev=None):
if ev:
+17 -20
View File
@@ -82,6 +82,7 @@ from pretix.base.signals import order_gracefully_delete
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import format_map
from ...helpers.names import build_name
from ._transactions import (
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
)
@@ -1451,15 +1452,11 @@ class AbstractPosition(models.Model):
@property
def attendee_name(self):
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.event.settings.name_scheme)
@property
def attendee_name_all_components(self):
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.event.settings.name_scheme)
@property
def state_name(self):
@@ -2834,8 +2831,12 @@ class CartPosition(AbstractPosition):
if self.is_bundled:
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
if bundle:
listed_price = bundle.designated_price
price_after_voucher = bundle.designated_price
if self.addon_to.voucher_id and self.addon_to.voucher.all_bundles_included:
listed_price = Decimal('0.00')
price_after_voucher = Decimal('0.00')
else:
listed_price = bundle.designated_price
price_after_voucher = bundle.designated_price
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
self.listed_price = listed_price
@@ -2980,15 +2981,11 @@ class InvoiceAddress(models.Model):
@property
def name(self):
if not self.name_parts:
return ""
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
raise TypeError("Invalid name given.")
return scheme['concatenation'](self.name_parts).strip()
return build_name(self.name_parts, fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
@property
def name_all_components(self):
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
def for_js(self):
d = {}
+8
View File
@@ -296,6 +296,14 @@ class Voucher(LoggedModel):
verbose_name=_("Shows hidden products that match this voucher"),
default=True
)
all_addons_included = models.BooleanField(
verbose_name=_("Offer all add-on products for free when redeeming this voucher"),
default=False
)
all_bundles_included = models.BooleanField(
verbose_name=_("Include all bundled products without a designated price when redeeming this voucher"),
default=False
)
objects = ScopedManager(organizer='event__organizer')
+9 -13
View File
@@ -35,9 +35,9 @@ from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import User, Voucher
from pretix.base.services.mail import SendMailException, mail, render_mail
from pretix.base.settings import PERSON_NAME_SCHEMES
from ...helpers.format import format_map
from ...helpers.names import build_name
from .base import LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation
@@ -119,7 +119,7 @@ class WaitingListEntry(LoggedModel):
def clean(self):
try:
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_duplicate(self.event, self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
except ObjectDoesNotExist:
@@ -136,15 +136,11 @@ class WaitingListEntry(LoggedModel):
@property
def name(self):
if not self.name_parts:
return None
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.name_parts).strip()
return build_name(self.name_parts, fallback_scheme=lambda: self.event.settings.name_scheme)
@property
def name_all_components(self):
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.event.settings.name_scheme)
def send_voucher(self, quota_cache=None, user=None, auth=None):
availability = (
@@ -308,9 +304,9 @@ class WaitingListEntry(LoggedModel):
raise ValidationError(_('The subevent does not belong to this event.'))
@staticmethod
def clean_duplicate(email, item, variation, subevent, pk):
def clean_duplicate(event, email, item, variation, subevent, pk):
if WaitingListEntry.objects.filter(
item=item, variation=variation, email__iexact=email, voucher__isnull=True, subevent=subevent
).exclude(pk=pk).exists():
).exclude(pk=pk).count() >= event.settings.waiting_list_limit_per_user:
raise ValidationError(_('You are already on this waiting list! We will notify '
'you as soon as we have a ticket available for you.'))
+4 -2
View File
@@ -1469,7 +1469,8 @@ class GiftCardPayment(BasePaymentProvider):
trans = gc.transactions.create(
value=-1 * payment.amount,
order=payment.order,
payment=payment
payment=payment,
acceptor=self.event.organizer,
)
payment.info_data = {
'gift_card': gc.pk,
@@ -1490,7 +1491,8 @@ class GiftCardPayment(BasePaymentProvider):
trans = gc.transactions.create(
value=refund.amount,
order=refund.order,
refund=refund
refund=refund,
acceptor=self.event.organizer,
)
refund.info_data = {
'gift_card': gc.pk,
+1 -9
View File
@@ -48,7 +48,6 @@ from functools import partial
from io import BytesIO
import jsonschema
from arabic_reshaper import ArabicReshaper
from bidi.algorithm import get_display
from django.conf import settings
from django.contrib.staticfiles import finders
@@ -57,7 +56,6 @@ from django.db.models import Max, Min
from django.dispatch import receiver
from django.utils.deconstruct import deconstructible
from django.utils.formats import date_format
from django.utils.functional import SimpleLazyObject
from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext
@@ -78,12 +76,12 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
@@ -699,12 +697,6 @@ def get_seat(op: OrderPosition):
return None
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
'delete_harakat': True,
'support_ligatures': False,
}))
class Renderer:
def __init__(self, event, layout, background_file):
+11 -5
View File
@@ -512,7 +512,10 @@ class CartManager:
if cp.is_bundled:
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
if bundle:
listed_price = bundle.designated_price or Decimal('0.00')
if cp.addon_to.voucher_id and cp.addon_to.voucher.all_bundles_included:
listed_price = Decimal('0.00')
else:
listed_price = bundle.designated_price
else:
listed_price = cp.price
price_after_voucher = listed_price
@@ -712,6 +715,11 @@ class CartManager:
else:
bundle_quotas = []
if voucher and voucher.all_bundles_included:
bundled_price = Decimal('0.00')
else:
bundled_price = bundle.designated_price
bop = self.AddOperation(
count=bundle.count,
item=bitem,
@@ -722,8 +730,8 @@ class CartManager:
subevent=subevent,
bundled=[],
seat=None,
listed_price=bundle.designated_price,
price_after_voucher=bundle.designated_price,
listed_price=bundled_price,
price_after_voucher=bundled_price,
custom_price_input=None,
custom_price_input_is_net=False,
voucher_ignored=False,
@@ -809,7 +817,6 @@ class CartManager:
quota_diff = Counter() # Quota -> Number of usages
operations = []
available_categories = defaultdict(set) # CartPos -> Category IDs to choose from
price_included = defaultdict(dict) # CartPos -> CategoryID -> bool(price is included)
toplevel_cp = self.positions.filter(
addon_to__isnull=True
).prefetch_related(
@@ -819,7 +826,6 @@ class CartManager:
# Prefill some of the cache containers
for cp in toplevel_cp:
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
price_included[cp.pk] = {iao.addon_category_id: iao.price_included for iao in cp.item.addons.all()}
cpcache[cp.pk] = cp
for a in cp.addons.all():
if not a.is_bundled:
+3 -2
View File
@@ -510,7 +510,7 @@ def send_invoices_to_organizer(sender, **kwargs):
with transaction.atomic():
qs = Invoice.objects.filter(
sent_to_organizer__isnull=True
).prefetch_related('event').select_for_update(of=OF_SELF, skip_locked=connection.features.has_select_for_update_skip_locked)
).prefetch_related('event', 'order').select_for_update(of=OF_SELF, skip_locked=connection.features.has_select_for_update_skip_locked)
for i in qs[:batch_size]:
if i.event.settings.invoice_email_organizer:
with language(i.event.settings.locale):
@@ -519,11 +519,12 @@ def send_invoices_to_organizer(sender, **kwargs):
subject=_('New invoice: {number}').format(number=i.number),
template=LazyI18nString.from_gettext(_(
'Hello,\n\n'
'a new invoice for {event} has been created, see attached.\n\n'
'a new invoice for order {order} at {event} has been created, see attached.\n\n'
'We are sending this email because you configured us to do so in your event settings.'
)),
context={
'event': str(i.event),
'order': str(i.order),
},
locale=i.event.settings.locale,
event=i.event,
+9 -1
View File
@@ -466,9 +466,17 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
for inv in invoices:
if inv.file:
try:
# We try to give the invoice a more human-readable name, e.g. "Invoice_ABC-123.pdf" instead of
# just "ABC-123.pdf", but we only do so if our currently selected language allows to do this
# as ASCII text. For example, we would not want a "فاتورة_" prefix for our filename since this
# has shown to cause deliverability problems of the email and deliverability wins.
filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf'
if not re.match("^[a-zA-Z0-9-_%./,&:# ]+$", filename):
filename = inv.number.replace(' ', '_') + '.pdf'
filename = re.sub("[^a-zA-Z0-9-_.]+", "_", filename)
with language(inv.order.locale):
email.attach(
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
filename,
inv.file.file.read(),
'application/pdf'
)
+62 -9
View File
@@ -95,7 +95,8 @@ from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
from pretix.base.signals import (
allow_ticket_download, order_approved, order_canceled, order_changed,
order_denied, order_expired, order_fee_calculation, order_paid,
order_placed, order_split, periodic_task, validate_order,
order_placed, order_split, order_valid_if_pending, periodic_task,
validate_order,
)
from pretix.celery_app import app
from pretix.helpers import OF_SELF
@@ -235,7 +236,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
for gc in position.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
gc.transactions.create(value=position.price, order=order)
gc.transactions.create(value=position.price, order=order, acceptor=order.event.organizer)
break
for m in position.granted_memberships.all():
@@ -513,7 +514,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
)
)
else:
gc.transactions.create(value=-position.price, order=order)
gc.transactions.create(value=-position.price, order=order, acceptor=order.event.organizer)
for m in position.granted_memberships.all():
m.canceled = True
@@ -922,7 +923,7 @@ def _get_fees(positions: List[CartPosition], payment_requests: List[dict], addre
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
meta_info: dict=None, sales_channel: str='web', shown_total=None,
customer=None):
customer=None, valid_if_pending=False):
payments = []
sales_channel = get_all_sales_channels()[sales_channel]
@@ -950,6 +951,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
require_approval=require_approval,
sales_channel=sales_channel.identifier,
customer=customer,
valid_if_pending=valid_if_pending,
)
if customer:
order.email_known_to_work = customer.is_verified
@@ -1094,6 +1096,20 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
customer=customer,
)
valid_if_pending = False
for recv, result in order_valid_if_pending.send(
event,
payments=payment_requests,
email=email,
positions=positions,
locale=locale,
invoice_address=addr,
meta_info=meta_info,
customer=customer,
):
if result:
valid_if_pending = True
lockfn = NoLockManager
locked = False
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2)) | Q(seat__isnull=False)).exists():
@@ -1117,7 +1133,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
shown_total=shown_total, customer=customer)
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
try:
for p in payment_objs:
if p.provider == 'free':
@@ -1442,6 +1458,16 @@ class OrderChangeManager:
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
'max_items_per_product': ngettext_lazy(
"You cannot select more than %(max)s item of the product %(product)s.",
"You cannot select more than %(max)s items of the product %(product)s.",
"max"
),
'min_items_per_product': ngettext_lazy(
"You need to select at least %(min)s item of the product %(product)s.",
"You need to select at least %(min)s items of the product %(product)s.",
"min"
),
}
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
@@ -1744,6 +1770,11 @@ class OrderChangeManager:
if self._operations:
raise ValueError("Setting addons should be the first/only operation")
# Prepare containers for min/max check of products
item_counts = Counter()
for p in self.order.positions.all():
item_counts[p.item] += 1
# Prepare various containers to hold data later
current_addons = defaultdict(lambda: defaultdict(list)) # OrderPos -> currently attached add-ons
input_addons = defaultdict(Counter) # OrderPos -> final desired set of add-ons
@@ -1861,7 +1892,7 @@ class OrderChangeManager:
input_addons[op.id][a['item'], a['variation']] = a.get('count', 1)
selected_addons[op.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
if price_included[op.pk].get(item.category_id):
if price_included[op.pk].get(item.category_id) or (op.voucher_id and op.voucher.all_addons_included):
price = TAXED_ZERO
else:
price = get_price(
@@ -1880,6 +1911,7 @@ class OrderChangeManager:
item=item, variation=variation, price=price,
addon_to=op, subevent=op.subevent, seat=None,
)
item_counts[item] += 1
# Check constraints on the add-on combinations
for op in toplevel_op:
@@ -1929,6 +1961,27 @@ class OrderChangeManager:
}
)
self.cancel(a)
item_counts[a.item] -= 1
for item, count in item_counts.items():
if count == 0:
continue
if item.max_per_order and count > item.max_per_order:
raise OrderError(
self.error_messages['max_items_per_product'] % {
'max': item.max_per_order,
'product': item.name
}
)
if item.min_per_order and count < item.min_per_order:
raise OrderError(
self.error_messages['min_items_per_product'] % {
'min': item.min_per_order,
'product': item.name
}
)
def _check_seats(self):
for seat, diff in self._seatdiff.items():
@@ -2186,7 +2239,7 @@ class OrderChangeManager:
card=gc.secret
))
else:
gc.transactions.create(value=-op.position.price, order=self.order)
gc.transactions.create(value=-op.position.price, order=self.order, acceptor=self.order.event.organizer)
for m in op.position.granted_memberships.with_usages().all():
m.canceled = True
@@ -2202,7 +2255,7 @@ class OrderChangeManager:
card=gc.secret
))
else:
gc.transactions.create(value=-opa.position.price, order=self.order)
gc.transactions.create(value=-opa.position.price, order=self.order, acceptor=self.order.event.organizer)
for m in opa.granted_memberships.with_usages().all():
m.canceled = True
@@ -2918,7 +2971,7 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
currency=sender.currency, issued_in=p, testmode=order.testmode,
expires=sender.organizer.default_gift_card_expiry,
)
gc.transactions.create(value=p.price - issued, order=order)
gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer)
any_giftcards = True
p.secret = gc.secret
p.save(update_fields=['secret'])
+2
View File
@@ -110,6 +110,8 @@ def is_included_for_free(item: Item, addon_to: AbstractPosition):
return True
except ItemAddOn.DoesNotExist:
pass
if addon_to.voucher_id and addon_to.voucher.all_addons_included:
return True
return False
+1 -1
View File
@@ -35,7 +35,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
for ir, r in enumerate(recipients):
voucher_list = []
for i in range(r['number']):
voucher_list.append(vouchers.pop())
voucher_list.append(vouchers.pop(0))
with language(event.settings.locale):
email_context = get_email_context(event=event, name=r.get('name') or '',
voucher_list=[v.code for v in voucher_list])
+98 -2
View File
@@ -96,6 +96,18 @@ def primary_font_kwargs():
}
def invoice_font_kwargs():
from pretix.presale.style import get_fonts
choices = [('Open Sans', 'Open Sans')]
choices += sorted([
(a, a) for a, v in get_fonts().items()
], key=lambda a: a[0])
return {
'choices': choices,
}
def restricted_plugin_kwargs():
from pretix.base.plugins import get_all_plugins
@@ -621,6 +633,17 @@ DEFAULTS = {
"used at most once over all of your events. This setting only affects future invoices. You can "
"use %Y (with century) %y (without century) to insert the year of the invoice, or %m and %d for "
"the day of month."),
validators=[
RegexValidator(
# We actually allow more characters than we name in the error message since some of these characters
# are in active use at the time of the introduction of this validation, so we can't really forbid
# them, but we don't think they belong in an invoice number and don't want to advertise them.
regex="^[a-zA-Z0-9-_%./,&:# ]+$",
message=lazy(lambda *args: _('Please only use the characters {allowed} in this field.').format(
allowed='A-Z, a-z, 0-9, -./:#'
), str)()
)
],
)
},
'invoice_numbers_prefix_cancellations': {
@@ -632,8 +655,42 @@ DEFAULTS = {
label=_("Invoice number prefix for cancellations"),
help_text=_("This will be prepended to invoice numbers of cancellations. If you leave this field empty, "
"the same numbering scheme will be used that you configured for regular invoices."),
validators=[
RegexValidator(
# We actually allow more characters than we name in the error message since some of these characters
# are in active use at the time of the introduction of this validation, so we can't really forbid
# them, but we don't think they belong in an invoice number and don't want to advertise them.
regex="^[a-zA-Z0-9-_%./,&:# ]+$",
message=lazy(lambda *args: _('Please only use the characters {allowed} in this field.').format(
allowed='A-Z, a-z, 0-9, -./:#'
), str)()
)
],
)
},
'invoice_renderer_highlight_order_code': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Highlight order code to make it stand out visibly"),
help_text=_("Only respected by some invoice renderers."),
)
},
'invoice_renderer_font': {
'default': 'Open Sans',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': lambda: dict(**invoice_font_kwargs()),
'form_kwargs': lambda: dict(
label=_('Font'),
help_text=_("Only respected by some invoice renderers."),
required=True,
**invoice_font_kwargs()
),
},
'invoice_renderer': {
'default': 'classic', # default for new events is 'modern1'
'type': str,
@@ -1330,6 +1387,21 @@ DEFAULTS = {
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
)
},
'waiting_list_limit_per_user': {
'default': '1',
'type': int,
'serializer_class': serializers.IntegerField,
'form_class': forms.IntegerField,
'serializer_kwargs': dict(
min_value=1,
),
'form_kwargs': dict(
label=_("Maximum number of entries per email address for the same product"),
min_value=1,
required=True,
widget=forms.NumberInput(),
)
},
'show_checkin_number_user': {
'default': 'False',
'type': bool,
@@ -1337,7 +1409,7 @@ DEFAULTS = {
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Show number of check-ins to customer"),
help_text=_('With this option enabled, your customers will be able how many times they entered '
help_text=_('With this option enabled, your customers will be able to see how many times they entered '
'the event. This is usually not necessary, but might be useful in combination with tickets '
'that are usable a specific number of times, so customers can see how many times they have '
'already been used. Exits or failed scans will not be counted, and the user will not see '
@@ -2593,6 +2665,15 @@ Your {organizer} team"""))
label=_("Use round edges"),
)
},
'widget_use_native_spinners': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Use native spinners in the widget instead of custom ones for numeric inputs such as quantity."),
)
},
'primary_font': {
'default': 'Open Sans',
'type': str,
@@ -3175,7 +3256,7 @@ def concatenation_for_salutation(d):
def get_name_parts_localized(name_parts, key):
value = name_parts.get(key, "")
if key == "salutation":
if key == "salutation" and value:
return pgettext_lazy("person_name_salutation", value)
return value
@@ -3320,6 +3401,7 @@ PERSON_NAME_SCHEMES = OrderedDict([
('full_name', _('Full name'), 2),
),
'concatenation': lambda d: str(d.get('full_name', '')),
'concatenation_all_components': lambda d: str(d.get('full_name', '')) + " (\"" + d.get('calling_name', '') + "\")",
'sample': {
'full_name': pgettext_lazy('person_name_sample', 'John Doe'),
'calling_name': pgettext_lazy('person_name_sample', 'John'),
@@ -3332,6 +3414,7 @@ PERSON_NAME_SCHEMES = OrderedDict([
('latin_transcription', _('Latin transcription'), 2),
),
'concatenation': lambda d: str(d.get('full_name', '')),
'concatenation_all_components': lambda d: str(d.get('full_name', '')) + " (" + d.get('latin_transcription', '') + ")",
'sample': {
'full_name': '庄司',
'latin_transcription': 'Shōji',
@@ -3348,6 +3431,9 @@ PERSON_NAME_SCHEMES = OrderedDict([
str(p) for p in (d.get(key, '') for key in ["given_name", "family_name"]) if p
),
'concatenation_for_salutation': concatenation_for_salutation,
'concatenation_all_components': lambda d: ' '.join(
str(p) for p in (get_name_parts_localized(d, key) for key in ["salutation", "given_name", "family_name"]) if p
),
'sample': {
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
'given_name': pgettext_lazy('person_name_sample', 'John'),
@@ -3366,6 +3452,9 @@ PERSON_NAME_SCHEMES = OrderedDict([
str(p) for p in (d.get(key, '') for key in ["title", "given_name", "family_name"]) if p
),
'concatenation_for_salutation': concatenation_for_salutation,
'concatenation_all_components': lambda d: ' '.join(
str(p) for p in (get_name_parts_localized(d, key) for key in ["salutation", "title", "given_name", "family_name"]) if p
),
'sample': {
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
'title': pgettext_lazy('person_name_sample', 'Dr'),
@@ -3390,6 +3479,13 @@ PERSON_NAME_SCHEMES = OrderedDict([
str(d.get('degree', ''))
),
'concatenation_for_salutation': concatenation_for_salutation,
'concatenation_all_components': lambda d: (
' '.join(
str(p) for p in (get_name_parts_localized(d, key) for key in ["salutation", "title", "given_name", "family_name"]) if p
) +
str((', ' if d.get('degree') else '')) +
str(d.get('degree', ''))
),
'sample': {
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
'title': pgettext_lazy('person_name_sample', 'Dr'),
+27 -2
View File
@@ -304,8 +304,7 @@ multiple events. Receivers should return a subclass of pretix.base.exporter.Base
The ``sender`` keyword argument will contain an organizer.
"""
validate_order = EventPluginSignal(
)
validate_order = EventPluginSignal()
"""
Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``,
``meta_info``, ``customer``
@@ -321,6 +320,18 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
in the future, as the ``payments`` attribute gives more information.
"""
order_valid_if_pending = EventPluginSignal()
"""
Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``,
``meta_info``, ``customer``
This signal is sent out when the user tries to confirm the order, before we actually create
the order. It allows you to set the ``valid_if_pending`` of the order even before it is
created. Whenever any plugin returns ``True``, the order will be valid if pending.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
validate_cart = EventPluginSignal()
"""
Arguments: ``positions``
@@ -578,6 +589,20 @@ All plugins that are installed may send fields for the global settings form, as
an OrderedDict of (setting name, form field).
"""
gift_card_transaction_display = django.dispatch.Signal()
"""
Arguments: ``transaction``, ``customer_facing``
To display an instance of the ``GiftCardTransaction`` model to a human user,
``pretix.base.signals.gift_card_transaction_display`` will be sent out with a ``transaction`` argument.
The ``customer_facing`` argument specifies whether the HTML will be shown to an end-user or if it is being
used in the backend.
The first received response that is not ``None`` will be used to display the log entry
to the user. The receivers are expected to return a string (that might be marked with ``mark_safe`` from Django if
it contains HTML).
"""
order_fee_calculation = EventPluginSignal()
"""
Arguments: ``positions``, ``invoice_address``, ``meta_info``, ``total``, ``gift_cards``, ``payment_requests``
@@ -9,7 +9,7 @@
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}" />
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/reloadpending.js" %}"></script>
{% endcompress %}
<meta name="viewport" content="width=device-width, initial-scale=1">
+5 -1
View File
@@ -542,6 +542,7 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'waiting_list_limit_per_user',
'max_items_per_order',
'reservation_time',
'contact_mail',
@@ -855,6 +856,8 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
'invoice_footer_text',
'invoice_eu_currencies',
'invoice_logo_image',
'invoice_renderer_highlight_order_code',
'invoice_renderer_font',
]
invoice_generate_sales_channels = forms.MultipleChoiceField(
@@ -1470,6 +1473,7 @@ class TaxRuleForm(I18nModelForm):
class WidgetCodeForm(forms.Form):
subevent = forms.ModelChoiceField(
label=pgettext_lazy('subevent', "Date"),
empty_label=pgettext_lazy('subevent', "All dates"),
required=False,
queryset=SubEvent.objects.none()
)
@@ -1670,7 +1674,7 @@ QuickSetupProductFormSet = formset_factory(
class ItemMetaPropertyForm(forms.ModelForm):
class Meta:
fields = ['name', 'default']
fields = ['name', 'default', 'required', 'allowed_values']
widgets = {
'default': forms.TextInput()
}
+2 -1
View File
@@ -483,7 +483,7 @@ class EventOrderFilterForm(OrderFilterForm):
file__isnull=False
)
qs = qs.annotate(has_answer=Exists(answers)).filter(has_answer=True)
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE) and fdata.get('answer'):
answers = QuestionAnswer.objects.filter(
question_id=q.pk,
orderposition__order_id=OuterRef('pk'),
@@ -1338,6 +1338,7 @@ class GiftCardFilterForm(FilterForm):
Q(secret__icontains=query)
| Q(transactions__text__icontains=query)
| Q(transactions__order__code__icontains=query)
| Q(owner_ticket__order__code__icontains=query)
)
if fdata.get('testmode') == 'yes':
qs = qs.filter(testmode=True)
+20 -10
View File
@@ -1102,16 +1102,26 @@ class ItemMetaValueForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.property = kwargs.pop('property')
super().__init__(*args, **kwargs)
self.fields['value'].required = False
self.fields['value'].widget.attrs['placeholder'] = self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:event.items.meta.typeahead', kwargs={
'organizer': self.property.event.organizer.slug,
'event': self.property.event.slug
}) + '?' + urlencode({
'property': self.property.name,
})
)
if self.property.allowed_values:
self.fields['value'] = forms.ChoiceField(
label=self.property.name,
choices=[(
"", _("Default ({value})").format(value=self.property.default)
if self.property.default else ""
)] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()]
)
else:
self.fields['value'].label = self.property.name
self.fields['value'].widget.attrs['placeholder'] = self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:event.items.meta.typeahead', kwargs={
'organizer': self.property.event.organizer.slug,
'event': self.property.event.slug
}) + '?' + urlencode({
'property': self.property.name,
})
)
self.fields['value'].required = self.property.required and not self.property.default
class Meta:
model = ItemMetaValue
+28 -10
View File
@@ -186,6 +186,15 @@ class OrganizerUpdateForm(OrganizerForm):
return instance
class SafeOrderPositionChoiceField(forms.ModelChoiceField):
def __init__(self, queryset, **kwargs):
queryset = queryset.model.all.none()
super().__init__(queryset, **kwargs)
def label_from_instance(self, op):
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
class EventMetaPropertyForm(forms.ModelForm):
class Meta:
model = EventMetaProperty
@@ -264,7 +273,7 @@ class DeviceForm(forms.ModelForm):
def clean(self):
d = super().clean()
if not d['all_events'] and not d['limit_events']:
if not d['all_events'] and not d.get('limit_events'):
raise ValidationError(_('Your device will not have access to anything, please select some events.'))
return d
@@ -650,23 +659,32 @@ class GiftCardCreateForm(forms.ModelForm):
class GiftCardUpdateForm(forms.ModelForm):
class Meta:
model = GiftCard
fields = ['expires', 'conditions']
fields = ['expires', 'conditions', 'owner_ticket']
field_classes = {
'expires': SplitDateTimeField
'expires': SplitDateTimeField,
'owner_ticket': SafeOrderPositionChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,
'conditions': forms.Textarea(attrs={"rows": 2})
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
organizer = self.instance.issuer
class SafeOrderPositionChoiceField(forms.ModelChoiceField):
def __init__(self, queryset, **kwargs):
queryset = queryset.model.all.none()
super().__init__(queryset, **kwargs)
def label_from_instance(self, op):
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
self.fields['owner_ticket'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
self.fields['owner_ticket'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
'organizer': organizer.slug,
}),
'data-placeholder': _('Ticket')
}
)
self.fields['owner_ticket'].widget.choices = self.fields['owner_ticket'].choices
self.fields['owner_ticket'].required = False
class ReusableMediumUpdateForm(forms.ModelForm):
+15 -12
View File
@@ -72,7 +72,8 @@ class VoucherForm(I18nModelForm):
localized_fields = '__all__'
fields = [
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
'all_bundles_included', 'budget'
]
field_classes = {
'valid_until': SplitDateTimeField,
@@ -308,7 +309,8 @@ class VoucherBulkForm(VoucherForm):
localized_fields = '__all__'
fields = [
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
'all_bundles_included', 'budget'
]
field_classes = {
'valid_until': SplitDateTimeField,
@@ -384,17 +386,18 @@ class VoucherBulkForm(VoucherForm):
def clean(self):
data = super().clean()
vouchers = self.instance.event.vouchers.annotate(
code_upper=Upper('code')
).filter(code_upper__in=[c.upper() for c in data['codes']])
if vouchers.exists():
raise ValidationError(_('A voucher with one of these codes already exists.'))
if 'codes' in data:
vouchers = self.instance.event.vouchers.annotate(
code_upper=Upper('code')
).filter(code_upper__in=[c.upper() for c in data['codes']])
if vouchers.exists():
raise ValidationError(_('A voucher with one of these codes already exists.'))
codes_seen = set()
for c in data['codes']:
if c in codes_seen:
raise ValidationError(_('The voucher code {code} appears in your list twice.').format(code=c))
codes_seen.add(c)
codes_seen = set()
for c in data['codes']:
if c in codes_seen:
raise ValidationError(_('The voucher code {code} appears in your list twice.').format(code=c))
codes_seen.add(c)
if data.get('send') and not all([data.get('send_subject'), data.get('send_message'), data.get('send_recipients')]):
raise ValidationError(_('If vouchers should be sent by email, subject, message and recipients need to be specified.'))
+15
View File
@@ -485,6 +485,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
'pretix.event.item_meta_property.added': _('A meta property has been added to this event.'),
'pretix.event.item_meta_property.deleted': _('A meta property has been removed from this event.'),
'pretix.event.item_meta_property.changed': _('A meta property has been changed on this event.'),
'pretix.event.quota.added': _('The quota has been added.'),
'pretix.event.quota.deleted': _('The quota has been deleted.'),
'pretix.event.quota.changed': _('The quota has been changed.'),
@@ -506,6 +509,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
'pretix.event.checkinlists.deleted': _('The check-in list has been deleted.'), # backwards compatibility
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
'pretix.event.settings': _('The event settings have been changed.'),
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
@@ -567,6 +571,17 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
else:
data['value'] = LazyI18nString(data['value'])
if logentry.action_type == "pretix.voucher.redeemed":
data = defaultdict(lambda: '?', data)
url = reverse('control:event.order', kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
'code': data['order_code']
})
return mark_safe(plains[logentry.action_type].format(
order_code='<a href="{}">{}</a>'.format(url, data['order_code']),
))
if logentry.action_type in plains:
data = defaultdict(lambda: '?', data)
return plains[logentry.action_type].format_map(data)
@@ -35,7 +35,7 @@
</script>
{% endif %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
{% endcompress %}
@@ -19,7 +19,7 @@
<script src="{% statici18n request.LANGUAGE_CODE %}"></script>
{% endif %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
<script type="text/javascript" src="{% static "typeahead/typeahead.bundle.js" %}"></script>
<script type="text/javascript" src="{% static "bootstrap/js/bootstrap.js" %}"></script>
@@ -54,6 +54,8 @@
{% bootstrap_field form.invoice_additional_text layout="control" %}
{% bootstrap_field form.invoice_footer_text layout="control" %}
{% bootstrap_field form.invoice_logo_image layout="control" %}
{% bootstrap_field form.invoice_renderer_font layout="control" %}
{% bootstrap_field form.invoice_renderer_highlight_order_code layout="control" %}
{% bootstrap_field form.invoice_eu_currencies layout="control" %}
</fieldset>
</div>
@@ -361,6 +361,7 @@
{% bootstrap_field sform.waiting_list_names_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %}
{% bootstrap_field sform.waiting_list_limit_per_user layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Item metadata" %}</legend>
@@ -377,41 +378,55 @@
{% bootstrap_formset_errors item_meta_property_formset %}
<div data-formset-body>
{% for form in item_meta_property_formset %}
<div class="row formset-row" data-formset-form>
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-5">
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Property" %}</h3>
</div>
<div class="col-sm-4 text-right flip">
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-5 col-lg-6">
{% bootstrap_field form.default layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 col-lg-1 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.default layout="control" %}
{% bootstrap_field form.required layout="control" %}
{% bootstrap_field form.allowed_values layout="control" %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row formset-row" data-formset-form>
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ item_meta_property_formset.empty_form.id }}
{% bootstrap_field item_meta_property_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-5">
{% bootstrap_field item_meta_property_formset.empty_form.name layout='inline' form_group_class="" %}
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Property" %}</h3>
</div>
<div class="col-sm-4 text-right flip">
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="col-md-5 col-lg-6">
{% bootstrap_field item_meta_property_formset.empty_form.default layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 col-lg-1 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
<div class="panel-body form-horizontal">
{% bootstrap_field item_meta_property_formset.empty_form.name layout="control" %}
{% bootstrap_field item_meta_property_formset.empty_form.default layout="control" %}
{% bootstrap_field item_meta_property_formset.empty_form.required layout="control" %}
{% bootstrap_field item_meta_property_formset.empty_form.allowed_values layout="control" %}
</div>
</div>
{% endescapescript %}
@@ -139,6 +139,26 @@
</div>
</div>
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
<div class="col-md-9">
{% for form in meta_forms %}
<div class="row">
<div class="col-md-4">
<label for="{{ form.value.id_for_label }}">
{{ form.property.name }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" error_types="all" %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</fieldset>
{% if form.quota_option %}
<fieldset>
@@ -134,7 +134,7 @@
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" %}
{% bootstrap_form form layout="inline" error_types="all" %}
</div>
</div>
{% endfor %}
@@ -55,7 +55,7 @@
<noscript>
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
</noscript>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" data-formset-delete-confirm-text="{% trans "If you delete an answer option, you will no longer be able to see statistical data on customers who previously selected this option, and when such customers edit their answers, they need to select a different option." %}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
@@ -511,7 +511,7 @@
<dl>
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
<dt>{% trans "Attendee name" %}</dt>
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
<dd>{% if line.attendee_name %}{{ line.attendee_name_all_components }}{% else %}
<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
@@ -631,6 +631,12 @@
</div>
<div class="clearfix"></div>
</div>
{% for gc in line.owned_gift_cards.all %}
<div class="product-row-giftcard">
<span class="fa fa-credit-card" aria-hidden="true"></span>
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">{{ gc.secret }}</a>
</div>
{% endfor %}
{% endfor %}
{% for fee in items.fees %}
<div class="row-fluid product-row {% if fee.canceled %}pos-canceled{% endif %}">
@@ -204,13 +204,21 @@
{% endblocktrans %}
</th>
<th class="text-right flip">
{% if sums.s|default_if_none:"none" != "none" %}
{{ sums.s|money:request.event.currency }}
{% if not filter_form.filtered %}
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
{% if sums.s|default_if_none:"none" != "none" %}
{{ sums.s|money:request.event.currency }}
{% endif %}
{% endif %}
</th>
<th class="text-right flip">
{% if sums.pc %}
{{ sums.pc }}
{% if not filter_form.filtered %}
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
{% if sums.pc %}
{{ sums.pc }}
{% endif %}
{% endif %}
</th>
<th></th>
@@ -45,6 +45,14 @@
{% bootstrap_field filter_form.date_until layout='inline' %}
</div>
</div>
<p class="text-danger">
{% blocktrans trimmed %}
Filtering this report by date is not recommended as it might lead to misleading information since this
report only sees the current state of any order, not any changes made to the order previously.
This date filter might be removed in the future.
Use the "Accounting report" in the export section instead.
{% endblocktrans %}
</p>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">
<span class="fa fa-filter"></span>
@@ -162,16 +162,19 @@
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
{% trans "Connect" %}</a>
{% elif d.api_token %}
{% endif %}
{% if not d.initialized or d.api_token %}
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm">
{% trans "Revoke access" %}</a>
{% endif %}
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm">
<span class="fa fa-list-alt"></span>
{% trans "Logs" %}
</a>
{% if d.initialized %}
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm">
<span class="fa fa-list-alt"></span>
{% trans "Logs" %}
</a>
{% endif %}
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
</td>
@@ -97,7 +97,13 @@
<div class="list-group large-link-group">
{% for e in c_ex %}
<a class="list-group-item" href="?identifier={{ e.identifier }}">
<h4>{{ e.verbose_name }}</h4>
<h4>
{{ e.verbose_name }}
{% if e.featured %}
<span class="fa fa-star text-success" data-toggle="tooltip"
title="{% trans "Recommended for new users" %}" aria-hidden="true"></span>
{% endif %}
</h4>
{% if e.description %}
<p>
{{ e.description }}
@@ -46,6 +46,14 @@
{{ card.issued_in.order.full_code }}</a>-{{ card.issued_in.positionid }}
</dd>
{% endif %}
{% if card.owner_ticket %}
<dt>{% trans "Owned by ticket holder" %}</dt>
<dd>
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=card.owner_ticket.order.event.slug organizer=request.organizer.slug code=card.owner_ticket.order.code %}">
{{ card.owner_ticket.order.code }}</a>-{{ card.owner_ticket.positionid }}
</dd>
{% endif %}
</dl>
</div>
</div>
@@ -62,29 +70,32 @@
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Order" %}</th>
<th>{% trans "Information" %}</th>
<th class="text-right">{% trans "Value" %}</th>
</tr>
</thead>
<tbody>
{% for t in card.transactions.all %}
{% for t in transactions %}
<tr>
<td>{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>
{% if t.order %}
<a href="{% url "control:event.order" event=t.order.event.slug organizer=t.order.event.organizer.slug code=t.order.code %}">
{{ t.order.full_code }}
</a>
{% if t.refund and t.value > 0 and t.value <= card.value %}
<button type="submit" name="revert" value="{{ t.pk }}"
class="btn btn-default btn-xs" data-toggle="tooltip"
title="{% trans "Create a payment on the respective order that cancels out with this transaction. The order will then likely be overpaid." %}">
<span class="fa fa-repeat"></span>
{% trans "Revert" %}
</button>
{% endif %}
{% else %}
<em>{% trans "Manual transaction" %}{% if t.text %}: {{ t.text }}{% endif %}</em>
{{ t.display_backend }}
{% if t.refund and t.value > 0 and t.value <= card.value %}
<button type="submit" name="revert" value="{{ t.pk }}"
class="btn btn-default btn-xs" data-toggle="tooltip"
title="{% trans "Create a payment on the respective order that cancels out with this transaction. The order will then likely be overpaid." %}">
<span class="fa fa-repeat"></span>
{% trans "Revert" %}
</button>
{% endif %}
{% if staff_session and t.info %}
<pre><code>{{ t.info|pprint }}</code></pre>
{% endif %}
{% if t.acceptor and t.acceptor != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ t.acceptor }}
</span>
{% endif %}
</td>
<td class="text-right">
@@ -14,6 +14,7 @@
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.expires layout="control" %}
{% bootstrap_field form.owner_ticket layout="control" %}
{% bootstrap_field form.conditions layout="control" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
@@ -47,7 +47,7 @@
</script>
{% endif %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/focus.js" %}"></script>
@@ -78,6 +78,8 @@
{% bootstrap_field form.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.show_hidden_items layout="control" %}
{% bootstrap_field form.all_addons_included layout="control" %}
{% bootstrap_field form.all_bundles_included layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Send out emails" %}</legend>
@@ -90,6 +90,8 @@
{% bootstrap_field form.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.show_hidden_items layout="control" %}
{% bootstrap_field form.all_addons_included layout="control" %}
{% bootstrap_field form.all_bundles_included layout="control" %}
</fieldset>
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
</div>
@@ -178,7 +178,7 @@
{% endif %}
</td>
{% if request.event.settings.waiting_list_names_asked %}
<td>{{ e.name|default:"" }}</td>
<td>{{ e.name_all_components|default:"" }}</td>
{% endif %}
<td>{{ e.email }}</td>
{% if request.event.settings.waiting_list_phones_asked %}
+1 -1
View File
@@ -430,7 +430,7 @@ class CheckinListDelete(EventPermissionRequiredMixin, CompatDeleteView):
self.object = self.get_object()
success_url = self.get_success_url()
self.object.checkins.all().delete()
self.object.log_action(action='pretix.event.checkinlists.deleted', user=request.user)
self.object.log_action(action='pretix.event.checkinlist.deleted', user=request.user)
self.object.delete()
messages.success(self.request, _('The selected list has been deleted.'))
return HttpResponseRedirect(success_url)
+15
View File
@@ -282,9 +282,19 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
if form in self.item_meta_property_formset.deleted_forms:
if not form.instance.pk:
continue
form.instance.log_action(
'pretix.event.item_meta_property.deleted',
user=self.request.user,
data=form.cleaned_data
)
form.instance.delete()
form.instance.pk = None
elif form.has_changed():
form.instance.log_action(
'pretix.event.item_meta_property.changed',
user=self.request.user,
data=form.cleaned_data
)
form.save()
for form in self.item_meta_property_formset.extra_forms:
@@ -294,6 +304,11 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
continue
form.instance.event = obj
form.save()
form.instance.log_action(
'pretix.event.item_meta_property.added',
user=self.request.user,
data=form.cleaned_data
)
@cached_property
def confirm_texts_formset(self):
+6 -1
View File
@@ -1213,7 +1213,7 @@ class MetaDataEditorMixin:
f.instance.delete()
class ItemCreate(EventPermissionRequiredMixin, CreateView):
class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
form_class = ItemCreateForm
template_name = 'pretixcontrol/item/create.html'
permission = 'can_change_items'
@@ -1274,6 +1274,11 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['meta_forms'] = self.meta_forms
return ctx
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
form_class = ItemUpdateForm
+2 -2
View File
@@ -367,7 +367,7 @@ class OrderDetail(OrderView):
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type',
'discount',
).prefetch_related(
'item__questions', 'issued_gift_cards', 'linked_media',
'item__questions', 'issued_gift_cards', 'owned_gift_cards', 'linked_media',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
Prefetch('all_checkins', queryset=Checkin.all.select_related('list').order_by('datetime')),
).order_by('positionid')
@@ -1512,7 +1512,7 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
invoice_pdf_task.apply(args=(self.invoice.pk,))
return self.get(request, *args, **kwargs)
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(self.invoice.number)
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number))
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
return resp
+13 -1
View File
@@ -1184,7 +1184,7 @@ class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.api_token:
if self.object.revoked:
messages.success(request, _('This device currently does not have access.'))
return redirect(reverse('control:organizer.devices', kwargs={
'organizer': self.request.organizer.slug,
@@ -1452,6 +1452,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
self.object.transactions.create(
value=value,
text=request.POST.get('text') or None,
acceptor=request.organizer,
)
self.object.log_action(
'pretix.giftcards.transaction.manual',
@@ -1471,6 +1472,16 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
))
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
return super().get_context_data(
**kwargs,
transactions=self.object.transactions.select_related(
'order', 'order__event', 'order__event__organizer', 'payment', 'refund'
).prefetch_related(
'acceptor'
)
)
class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/giftcard_create.html'
@@ -1497,6 +1508,7 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
form.instance.issuer = self.request.organizer
super().form_valid(form)
form.instance.transactions.create(
acceptor=self.request.organizer,
value=form.cleaned_data['value']
)
form.instance.log_action('pretix.giftcards.created', user=self.request.user, data={})
+1 -1
View File
@@ -210,7 +210,7 @@ def giftcard_select2(request, **kwargs):
return JsonResponse(doc)
@organizer_permission_required(("can_manage_reusable_media"))
@organizer_permission_required(("can_manage_reusable_media", "can_manage_gift_cards"))
def ticket_select2(request, **kwargs):
query = request.GET.get('query', '')
try:
+9 -5
View File
@@ -41,7 +41,7 @@ from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.db import connection, transaction
from django.db.models import Sum
from django.db.models import Exists, OuterRef, Sum
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
JsonResponse,
@@ -56,9 +56,12 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import (
CreateView, ListView, TemplateView, UpdateView, View,
)
from django_scopes import scopes_disabled
from pretix.base.email import get_available_placeholders
from pretix.base.models import CartPosition, LogEntry, Voucher
from pretix.base.models import (
CartPosition, LogEntry, Voucher, WaitingListEntry,
)
from pretix.base.models.vouchers import generate_codes
from pretix.base.services.locking import NoLockManager
from pretix.base.services.vouchers import vouchers_send
@@ -80,16 +83,17 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
template_name = 'pretixcontrol/vouchers/index.html'
permission = 'can_view_vouchers'
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
def get_queryset(self):
qs = Voucher.annotate_budget_used_orders(self.request.event.vouchers.filter(
waitinglistentries__isnull=True
qs = Voucher.annotate_budget_used_orders(self.request.event.vouchers.exclude(
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
).select_related(
'item', 'variation', 'seat'
))
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
return qs.distinct()
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
+28
View File
@@ -0,0 +1,28 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import re
from text_unidecode import unidecode
def safe_for_filename(val):
return re.sub('[^a-zA-Z0-9-_. ]+', '_', unidecode(val))
+38
View File
@@ -0,0 +1,38 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from pretix.base.settings import PERSON_NAME_SCHEMES
def build_name(parts, concatenation=None, fallback_scheme=None):
if not parts:
return None
if "_legacy" in parts:
return parts["_legacy"]
if "_scheme" in parts:
scheme = PERSON_NAME_SCHEMES[parts["_scheme"]]
elif fallback_scheme:
scheme = PERSON_NAME_SCHEMES[fallback_scheme() if callable(fallback_scheme) else fallback_scheme]
else:
raise TypeError("Invalid name given.")
if not concatenation or concatenation not in scheme:
concatenation = "concatenation"
return scheme[concatenation](parts).strip()
+8
View File
@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from arabic_reshaper import ArabicReshaper
from django.utils.functional import SimpleLazyObject
from PIL.Image import Resampling
from reportlab.lib.utils import ImageReader
@@ -41,3 +43,9 @@ class ThumbnailingImageReader(ImageReader):
# file handle if the file is a JPEG, and therefore does not respect the
# (smaller) size of the modified image.
return None
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
'delete_harakat': True,
'support_ligatures': False,
}))
File diff suppressed because it is too large Load Diff
+78 -68
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-26 09:13+0000\n"
"POT-Creation-Date: 2023-05-31 09:02+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -660,32 +660,32 @@ msgstr[3] "سيتم حجز العناصر لك في سلة التسوق لعدة
msgstr[4] "سيتم حجز العناصر لك في سلة التسوق لدقائق {num}."
msgstr[5] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:161
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "يحصل المنظم على %(currency) %(amount)"
#: pretix/static/pretixpresale/js/ui/main.js:160
#: pretix/static/pretixpresale/js/ui/main.js:169
msgid "You get %(currency)s %(amount)s back"
msgstr "ستسترد %(currency)%(amount)"
#: pretix/static/pretixpresale/js/ui/main.js:176
#: pretix/static/pretixpresale/js/ui/main.js:185
msgid "Please enter the amount the organizer can keep."
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
#: pretix/static/pretixpresale/js/ui/main.js:393
#: pretix/static/pretixpresale/js/ui/main.js:426
msgid "Please enter a quantity for one of the ticket types."
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
#: pretix/static/pretixpresale/js/ui/main.js:429
#: pretix/static/pretixpresale/js/ui/main.js:462
msgid "required"
msgstr "مطلوب"
#: pretix/static/pretixpresale/js/ui/main.js:532
#: pretix/static/pretixpresale/js/ui/main.js:551
#: pretix/static/pretixpresale/js/ui/main.js:565
#: pretix/static/pretixpresale/js/ui/main.js:584
msgid "Time zone:"
msgstr "المنطقة الزمنية:"
#: pretix/static/pretixpresale/js/ui/main.js:542
#: pretix/static/pretixpresale/js/ui/main.js:575
msgid "Your local time:"
msgstr "التوقيت المحلي:"
@@ -696,17 +696,27 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "Price"
msgid "Decrease quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "Increase quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "Price"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
#, fuzzy, javascript-format
#| msgid "Selected only"
msgctxt "widget"
msgid "Select %s"
msgstr "المختارة فقط"
#: pretix/static/pretixpresale/js/widget/widget.js:20
#: pretix/static/pretixpresale/js/widget/widget.js:22
#, fuzzy, javascript-format
#| msgctxt "widget"
#| msgid "See variations"
@@ -714,91 +724,91 @@ msgctxt "widget"
msgid "Select variant %s"
msgstr "أنظر إلى الاختلافات"
#: pretix/static/pretixpresale/js/widget/widget.js:21
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "Sold out"
msgstr "تم البيع بالكامل"
#: pretix/static/pretixpresale/js/widget/widget.js:22
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "Buy"
msgstr "اشتري"
#: pretix/static/pretixpresale/js/widget/widget.js:23
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Register"
msgstr "سجل"
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
msgid "Reserved"
msgstr "محجوز"
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "FREE"
msgstr "مجاني"
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr "من %(currency) s %(price)s"
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr "يشمل %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr "بالإضافة إلى %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid "incl. taxes"
msgstr "شامل الضريبة"
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
msgid "plus taxes"
msgstr "بالإضافة إلى الضرائب"
#: pretix/static/pretixpresale/js/widget/widget.js:31
#: pretix/static/pretixpresale/js/widget/widget.js:33
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr "متوفر حاليا: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "Only available with a voucher"
msgstr "متوفرة مع القسيمة فقط"
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/js/widget/widget.js:35
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr "الحد الأدنى للطلب: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:34
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Close ticket shop"
msgstr "إغلاق متجر التذاكر"
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr "لا يمكن تحميل متجر التذاكر."
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
"in a new tab to continue."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/js/widget/widget.js:40
#, fuzzy
#| msgctxt "widget"
#| msgid "Close ticket shop"
@@ -806,24 +816,24 @@ msgctxt "widget"
msgid "Open ticket shop"
msgstr "إغلاق متجر التذاكر"
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr "لا يمكن إنشاء سلة التسوق. الرجاء المحاولة مرة أخرى في وقت لاحق"
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid ""
"We could not create your cart, since there are currently too many users in "
"this ticket shop. Please click \"Continue\" to retry in a new tab."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Waiting list"
msgstr "قائمة الإنتظار"
#: pretix/static/pretixpresale/js/widget/widget.js:43
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
@@ -832,82 +842,82 @@ msgstr ""
"لديك الآن سلة تسوق مفعلة لهذا الحدث. إذا قمت باختيار منتجات أخرى سيتم "
"إضافتها إلى سلة التسوق الموجودة حالياً."
#: pretix/static/pretixpresale/js/widget/widget.js:45
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"
msgid "Resume checkout"
msgstr "استئناف الدفع"
#: pretix/static/pretixpresale/js/widget/widget.js:46
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgctxt "widget"
msgid "Redeem a voucher"
msgstr "استبدال قسيمة"
#: pretix/static/pretixpresale/js/widget/widget.js:47
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgctxt "widget"
msgid "Redeem"
msgstr "استبدال"
#: pretix/static/pretixpresale/js/widget/widget.js:48
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgctxt "widget"
msgid "Voucher code"
msgstr "رمز القسيمة"
#: pretix/static/pretixpresale/js/widget/widget.js:49
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgctxt "widget"
msgid "Close"
msgstr "إغلاق"
#: pretix/static/pretixpresale/js/widget/widget.js:50
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgctxt "widget"
msgid "Continue"
msgstr "استمرار"
#: pretix/static/pretixpresale/js/widget/widget.js:51
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgctxt "widget"
msgid "See variations"
msgstr "أنظر إلى الاختلافات"
#: pretix/static/pretixpresale/js/widget/widget.js:52
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgctxt "widget"
msgid "Choose a different event"
msgstr "اختر فعالية أخرى"
#: pretix/static/pretixpresale/js/widget/widget.js:53
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgctxt "widget"
msgid "Choose a different date"
msgstr "اختر تاريخا آخر"
#: pretix/static/pretixpresale/js/widget/widget.js:54
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgctxt "widget"
msgid "Back"
msgstr "العودة"
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgctxt "widget"
msgid "Next month"
msgstr "الشهر القادم"
#: pretix/static/pretixpresale/js/widget/widget.js:56
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgctxt "widget"
msgid "Previous month"
msgstr "الشهر السابق"
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgctxt "widget"
msgid "Next week"
msgstr "الأسبوع القادم"
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgctxt "widget"
msgid "Previous week"
msgstr "الأسبوع السابق"
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgctxt "widget"
msgid "Open seat selection"
msgstr "خيارات المقاعد"
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgctxt "widget"
msgid ""
"Some or all ticket categories are currently sold out. If you want, you can "
@@ -915,86 +925,86 @@ msgid ""
"again."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/js/widget/widget.js:63
#, fuzzy
#| msgid "Load more"
msgctxt "widget"
msgid "Load more"
msgstr "تحميل المزيد"
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "Mo"
msgstr "الإثنين"
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "Tu"
msgstr "الثلاثاء"
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "We"
msgstr "الأربعاء"
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "Th"
msgstr "الخميس"
#: pretix/static/pretixpresale/js/widget/widget.js:67
#: pretix/static/pretixpresale/js/widget/widget.js:69
msgid "Fr"
msgstr "الجمعة"
#: pretix/static/pretixpresale/js/widget/widget.js:68
#: pretix/static/pretixpresale/js/widget/widget.js:70
msgid "Sa"
msgstr "السبت"
#: pretix/static/pretixpresale/js/widget/widget.js:69
#: pretix/static/pretixpresale/js/widget/widget.js:71
msgid "Su"
msgstr "الأحد"
#: pretix/static/pretixpresale/js/widget/widget.js:72
#: pretix/static/pretixpresale/js/widget/widget.js:74
msgid "January"
msgstr "يناير"
#: pretix/static/pretixpresale/js/widget/widget.js:73
#: pretix/static/pretixpresale/js/widget/widget.js:75
msgid "February"
msgstr "فبراير"
#: pretix/static/pretixpresale/js/widget/widget.js:74
#: pretix/static/pretixpresale/js/widget/widget.js:76
msgid "March"
msgstr "مارس"
#: pretix/static/pretixpresale/js/widget/widget.js:75
#: pretix/static/pretixpresale/js/widget/widget.js:77
msgid "April"
msgstr "أبريل"
#: pretix/static/pretixpresale/js/widget/widget.js:76
#: pretix/static/pretixpresale/js/widget/widget.js:78
msgid "May"
msgstr "مايو"
#: pretix/static/pretixpresale/js/widget/widget.js:77
#: pretix/static/pretixpresale/js/widget/widget.js:79
msgid "June"
msgstr "يونيو"
#: pretix/static/pretixpresale/js/widget/widget.js:78
#: pretix/static/pretixpresale/js/widget/widget.js:80
msgid "July"
msgstr "يوليو"
#: pretix/static/pretixpresale/js/widget/widget.js:79
#: pretix/static/pretixpresale/js/widget/widget.js:81
msgid "August"
msgstr "أغسطس"
#: pretix/static/pretixpresale/js/widget/widget.js:80
#: pretix/static/pretixpresale/js/widget/widget.js:82
msgid "September"
msgstr "سبتمبر"
#: pretix/static/pretixpresale/js/widget/widget.js:81
#: pretix/static/pretixpresale/js/widget/widget.js:83
msgid "October"
msgstr "أكتوبر"
#: pretix/static/pretixpresale/js/widget/widget.js:82
#: pretix/static/pretixpresale/js/widget/widget.js:84
msgid "November"
msgstr "نوفمبر"
#: pretix/static/pretixpresale/js/widget/widget.js:83
#: pretix/static/pretixpresale/js/widget/widget.js:85
msgid "December"
msgstr "ديسمبر"
File diff suppressed because it is too large Load Diff

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