Compare commits

...
Author SHA1 Message Date
Raphael Michel 82ca50c7ff Fix templates 2025-12-09 12:42:47 +01:00
DanielandChondaen 3437b64947 Add PromptPay support (#5)
* Handle PromptPay QR flow

* Send billing email for PromptPay

* fix isort

* Update payment.py

* Update signals.py

---------

Co-authored-by: Chondaen <chondaen12@1000WA>
2025-12-09 12:28:28 +01:00
Raphael Michel 449e8dc905 Event cancel form: Add missing rich=True flag 2025-12-08 09:58:54 +01:00
Raphael MichelandRichard Schreiber c491c8232e Bank transfer: Allow dashes in event slug to be missing (Z#23216859) (#5682)
* Bank transfer: Allow dashes in event slug to be missing (Z#23216859)

* Update src/pretix/plugins/banktransfer/tasks.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Update src/pretix/plugins/banktransfer/tasks.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Apply suggestions from code review

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2025-12-05 10:54:03 +01:00
sandra r aa02cc7968 Translations: Update Galician
Currently translated at 15.5% (961 of 6172 strings)

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

powered by weblate
2025-12-05 10:36:32 +01:00
Renne Rocha cfa13d6b9d Translations: Update Portuguese (Brazil)
Currently translated at 90.2% (5572 of 6172 strings)

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

powered by weblate
2025-12-05 10:36:32 +01:00
Raphael Michel af4eabc800 URL generation: Fix bug if plugins declare both event_urls and organizer_urls (#5688)
* URL generation: Fix bug if plugins declare both event_urls and organizer_urls

* Add missing file

* Add license header
2025-12-05 10:22:28 +01:00
luelista e1f5678d7c Refactor payment QR code generation code and add SPAYD format (#5680)
Move generation of QR code contents out of the HTML template and into Python code, so it can
be reused in plugins and tested with unit tests. Add the SPAYD QR code format which is used in
Czech Republic and Slovakia [1]. Display BezahlCode QR codes only for German IBANs.

[1] https://en.wikipedia.org/wiki/Short_Payment_Descriptor
2025-12-04 14:15:29 +01:00
luelista 609b7c82ee Handle duplicate column names in CSV import (#5681)
- display a warning message to the user
- automatically rename columns by adding "__1", "__2", ... suffixes
2025-12-04 14:03:27 +01:00
Raphael Michel 8d66e1e732 Cart extension: Fix bundled product being removed from cart when sold out (#5690)
Instead, the entire bundle must be removed as it may not be sold
individually.
2025-12-04 11:48:40 +01:00
Richard Schreiber c925f094f2 Reduce item event queries in waitinglist assign 2025-12-04 11:01:30 +01:00
Richard Schreiber 5caaa8586d Fix accounting report pending payment timezone (#5698) 2025-12-04 10:59:57 +01:00
SJang1 1b1cf1557d Translations: Update Korean
Currently translated at 50.8% (3139 of 6172 strings)

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

powered by weblate
2025-12-04 10:40:16 +01:00
sandra r 35d8a7eec5 Translations: Update Galician
Currently translated at 100.0% (254 of 254 strings)

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

powered by weblate
2025-12-04 10:40:16 +01:00
sandra r d428c3e1a4 Translations: Update Galician
Currently translated at 14.0% (869 of 6172 strings)

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

powered by weblate
2025-12-04 10:40:16 +01:00
dependabot[bot] 63850f3139 Update sentry-sdk requirement from ==2.46.* to ==2.47.*
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.46.0...2.47.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.47.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 10:40:05 +01:00
Felix Rindt 04c8270d43 Update pricing.rst to fix number typo (#5691)
I think you meant to point out the difference to the values in the table above...
2025-12-04 07:27:36 +01:00
dependabot[bot] 74a960e239 Update celery requirement from ==5.5.* to ==5.6.* (#5676) 2025-12-03 17:00:53 +01:00
Raphael Michelandluelista 5a1bcae085 Invoice address: Improve VAT ID input (#5647)
* Remove unmaintained depdendency vat_moss

* VAT ID normalization: Auto-add country codes

* VAT ID: County-specific labels

* Invoice address: Allow to set VAT ID as required per country

* Fix failing tests

* Update src/pretix/base/settings.py

Co-authored-by: luelista <weller@rami.io>

* Review fixes

---------

Co-authored-by: luelista <weller@rami.io>
2025-12-03 16:48:19 +01:00
SJang1 051eb78312 Translations: Update Korean
Currently translated at 50.8% (3140 of 6172 strings)

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

powered by weblate
2025-12-03 16:29:20 +01:00
Ana Rute Pacheco Vivas 15808e55fd Translations: Update Portuguese (Portugal)
Currently translated at 83.1% (5134 of 6172 strings)

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

powered by weblate
2025-12-03 16:29:20 +01:00
David Ibáñez Cerdeira c886c0b415 Translations: Update Galician
Currently translated at 9.2% (569 of 6172 strings)

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

powered by weblate
2025-12-03 16:29:20 +01:00
David Ibáñez Cerdeira 47472447eb Translations: Update Galician
Currently translated at 9.1% (567 of 6172 strings)

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

powered by weblate
2025-12-03 16:29:20 +01:00
Richard Schreiber 1a40215e91 Fix N+1 queries in API (#5684)
* Fix N+1 query in API quotas list

* fix membership N+1

* fix vouchers N+1 budget_used

* rename and reuse Voucher.annotate_budget_used_orders to budget_used

* fix flake8
2025-12-03 15:37:40 +01:00
Raphael Michel d3fde85c39 Fix typo in CSS variable 2025-12-02 17:47:45 +01:00
Richard Schreiber 40bd66cb86 Fix PayPal2 payment patch request (#5678) 2025-12-02 13:14:12 +01:00
Raphael Michel bdd94b1f8a Add prioritization to webhook/notifications queue (#5513)
* Add prioritization to webhook/notifications queue

* Add missing code

* Missing license header

* Fix argument

* Use redis pipeline

* Update license header
2025-12-02 09:13:01 +01:00
José Manuel Silva 1c907f6a6f Translations: Update Portuguese (Portugal)
Currently translated at 83.1% (5133 of 6172 strings)

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

powered by weblate
2025-12-01 13:49:40 +01:00
José Manuel Silva 39e3ed9c25 Translations: Update Portuguese (Portugal)
Currently translated at 83.2% (5136 of 6172 strings)

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

powered by weblate
2025-12-01 13:49:40 +01:00
Richard Schreiber 4b5711253e Fix display_add_to_cart for variations 2025-12-01 13:48:02 +01:00
Raphael Michel bd554c7c29 Update remaining icon files 2025-12-01 13:41:06 +01:00
Raphael Michel 2261951b15 Peppol: Live ID validation (#5602)
* Peppol: Live ID validation

* Always check both systems

* Simplify logic
2025-11-27 19:50:53 +01:00
Raphael MichelandRichard Schreiber 0f82e1cae6 Update pretix logo to new version (#5651)
* Update pretix logo to new version

* Make favicon transparent

* Update src/pretix/static/pretixcontrol/scss/main.scss

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/static/pretixcontrol/scss/main.scss

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-11-27 16:05:30 +01:00
dependabot[bot] b0760157ce Update sentry-sdk requirement from ==2.45.* to ==2.46.*
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.45.0...2.46.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.46.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 16:05:18 +01:00
dependabot[bot] de2dec9089 Update pypdf requirement from ==6.3.* to ==6.4.* (#5659)
Updates the requirements on [pypdf](https://github.com/py-pdf/pypdf) to permit the latest version.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.3.0...6.4.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.4.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 16:05:04 +01:00
Raphael Michel 446c8e622b Bump version to 2025.11.0.dev0 2025-11-27 15:34:32 +01:00
Raphael Michel 703be2ebb8 Bump version to 2025.10.0 2025-11-27 15:34:23 +01:00
Raphael Michel a56fbc896c Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6172 of 6172 strings)

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

powered by weblate
2025-11-27 15:33:49 +01:00
Raphael Michel 7b6f5df985 Translations: Update German
Currently translated at 100.0% (6172 of 6172 strings)

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

powered by weblate
2025-11-27 15:33:49 +01:00
Raphael Michel d2087907d5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2025-11-27 14:58:19 +01:00
Mira cbc2e611a2 Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (6168 of 6173 strings)

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

powered by weblate
2025-11-27 14:57:18 +01:00
Mira 02126a48fe Translations: Update German
Currently translated at 99.9% (6168 of 6173 strings)

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

powered by weblate
2025-11-27 14:57:18 +01:00
Yasunobu YesNo Kawaguchi be9af94131 Translations: Update Japanese
Currently translated at 99.9% (6167 of 6173 strings)

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

powered by weblate
2025-11-27 14:57:18 +01:00
CVZ-es dbe1944996 Translations: Update Spanish
Currently translated at 100.0% (6173 of 6173 strings)

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

powered by weblate
2025-11-27 14:57:18 +01:00
CVZ-es 6181bdc2e9 Translations: Update French
Currently translated at 100.0% (6173 of 6173 strings)

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

powered by weblate
2025-11-27 14:57:18 +01:00
Ana Rute Pacheco Vivas fe40d1c491 Translations: Update Portuguese (Portugal)
Currently translated at 80.2% (4951 of 6173 strings)

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

powered by weblate
2025-11-27 14:57:18 +01:00
Raphael Michel 9f263fbe4f Hotfix linkified placeholders (#5663)
* Fix linkify placeholders

* Add URL test
2025-11-27 13:20:13 +01:00
Raphael Michelandluelista fdd34f387a [SECURITY] Prevent HTML injection through placeholders in emails
Co-authored-by: luelista <weller@pretix.eu>
2025-11-27 11:41:27 +01:00
Raphael Michel bfab523d83 Merge branch 'SECURITY-pw-change' into 'master'
[SECURITY] Fix old password not validated on password change

See merge request pretix/pretix!16
2025-11-26 19:39:32 +01:00
Raphael Michel 8f69cb166d [SECURITY] Fix old password not validated on password change 2025-11-26 19:39:32 +01:00
Martin Gross 2fc7c23960 Cart Fragment: Display description of OrderFee.FEE_TYPE_OTHER if description is set (as done in invoices) 2025-11-20 13:56:21 +01:00
Raphael Michel b0911c9e42 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2025-11-20 11:37:59 +01:00
Hijiri Umemoto a5aa1030e5 Translations: Update Chinese (Traditional Han script)
Currently translated at 91.9% (5675 of 6171 strings)

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

powered by weblate
2025-11-20 11:36:56 +01:00
Raphael Michel 681e682e73 Bank transfer: Consistancy in order of fields (fixes #5577) (#5625)
* Bank transfer: Consistancy in order of fields (fixes #5577)

* Delete unused template
2025-11-19 14:47:28 +01:00
Raphael Michel db7518735a Allow admins to inspect invoices (#5641)
This is helpful to debug invoice renderers or non-PDF invoices like
Peppol or other XML formats
2025-11-19 14:42:18 +01:00
Raphael Michel 9c80f3038a OIDC: Drop scopes validation (fixes #5464) (#5623)
* OIDC: Drop scopes validation (fixes #5464)

* Fix test

* Remove claims as well
2025-11-19 14:39:32 +01:00
Raphael Michel 4dc5bbae06 Invoices: Increase retry interval (#5640)
e.g. Invopop states that receipt confirmation in italy can take 24h
2025-11-19 12:30:37 +01:00
dependabot[bot] e997ca4242 Update sentry-sdk requirement from ==2.44.* to ==2.45.* (#5644)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.44.0...2.45.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.45.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 08:05:56 +01:00
Hijiri Umemoto 278b4301e5 Translations: Update Chinese (Traditional Han script)
Currently translated at 95.2% (242 of 254 strings)

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

powered by weblate
2025-11-19 08:05:42 +01:00
Hijiri Umemoto b648f9c46c Translations: Update Chinese (Traditional Han script)
Currently translated at 91.9% (5676 of 6171 strings)

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

powered by weblate
2025-11-19 08:05:42 +01:00
Hijiri Umemoto 9ce16b60d2 Translations: Update Korean
Currently translated at 99.2% (252 of 254 strings)

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

powered by weblate
2025-11-19 08:05:42 +01:00
Hijiri Umemoto f4a7604632 Translations: Update Korean
Currently translated at 49.9% (3084 of 6171 strings)

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

powered by weblate
2025-11-19 08:05:42 +01:00
Hijiri Umemoto 7cebb3e93f Translations: Update Japanese
Currently translated at 100.0% (254 of 254 strings)

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

powered by weblate
2025-11-19 08:05:42 +01:00
Hijiri Umemoto c82726e13d Translations: Update Japanese
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-19 08:05:42 +01:00
Phin Wolkwitz 2fcfc336d0 Add field length validation for invoice settings (Z#23215182) (#5639)
Limit invoice settings field lengths, add min value for counter length
2025-11-18 15:51:34 +01:00
luelista 39ff84b2e2 Use unique column names in order position export for invoice vs. attendee company name (Z#23215261) (#5638) 2025-11-18 15:47:55 +01:00
Raphael Michelandluelista 44804f05f3 Event quickstart: Fix fields being marked as optional (fixes #3504) (#5627)
* Event quickstart: Fix fields being marked as optional (fixes #3504)

* Revert accidental changes

* Update src/pretix/static/pretixcontrol/js/ui/main.js

Co-authored-by: luelista <weller@rami.io>

---------

Co-authored-by: luelista <weller@rami.io>
2025-11-18 15:46:11 +01:00
Richard Schreiber 5e828ab8af Fix tax-code keying function for tax-recalc (#5637) 2025-11-18 15:03:33 +01:00
Richard Schreiber 313f4f326b Fix program times having no item in clean (#5635)
This error occurs only when adding a program-time form in the frontend and not saving it, but removing it again and then saving the item.
2025-11-18 14:59:31 +01:00
dependabot[bot] ed43bf327e Update pypdf requirement from ==6.2.* to ==6.3.* (#5634)
Updates the requirements on [pypdf](https://github.com/py-pdf/pypdf) to permit the latest version.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.2.0...6.3.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 14:56:30 +01:00
Theodore 30aabc6253 Stripe: Update Revolut Pay presentment currency list (#5631) 2025-11-18 14:56:01 +01:00
Raphael Michel 5eade62121 Bank transfer: Use less cryptic refund references (fixes #4289) (#5626)
* Bank transfer: Use less cryptic refund references (fixes #4289)

* Add condition back in

* Fix tests
2025-11-18 14:52:44 +01:00
Raphael Michel 2669afa1f8 Webhooks: Allow longer URLs (fixes #5443) (#5622) 2025-11-18 14:42:48 +01:00
Raphael Michel d42c6f9b72 Open Fix a missing log entry type (fixes #5570) 2025-11-18 14:42:29 +01:00
Yasunobu YesNo Kawaguchi 34f064ca33 Translations: Update Japanese
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-18 14:42:00 +01:00
CVZ-es ad8d0a270c Translations: Update Spanish
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-18 14:42:00 +01:00
CVZ-es 363fcc3b56 Translations: Update French
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-18 14:42:00 +01:00
CVZ-es 9521ec2c52 Translations: Update Spanish
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-18 14:42:00 +01:00
CVZ-es 688d341baf Translations: Update French
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-18 14:42:00 +01:00
Sanny cdd4001378 Translations: Update Italian
Currently translated at 36.6% (2264 of 6171 strings)

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

powered by weblate
2025-11-18 14:42:00 +01:00
Raphael Michel d8d56ff020 Disable switching currency when orders exist (fixes #2047) 2025-11-17 17:09:17 +01:00
Raphael Michel 44b3647689 Accounting report: Allow subclasses to skip tables (#5616) 2025-11-17 17:09:06 +01:00
Richard Schreiber 818bb76e89 Fix calendar before-date to check for events (#5608) 2025-11-17 16:39:20 +01:00
Raphael Michel 8c01cad06b Stripe: Use unified wording for redirect announcement (#5613) 2025-11-17 16:20:53 +01:00
Raphael Michel 86ca7c4440 Order page: Do not show download deadline if download is disabled (fixes #3144) (#5630) 2025-11-17 15:42:20 +01:00
Richard Schreiber d7b6856322 Fix not allowing program times on event series (API/copy) (#5595)
* Fix not allowing program times on event series (API/copy)

* Return 400 when reading endpoint in event series

* add docs program times not available on event series

* fix isort
2025-11-17 15:36:53 +01:00
Raphael Michel e2d9cbb41d Add regressiont est for #1832 2025-11-14 18:20:20 +01:00
Raphael Michel 57bc7563da Fix flake8 issue 2025-11-14 18:13:48 +01:00
Raphael Michel 7741e9f936 Remove misleading helptext (fixes #3555) 2025-11-14 17:45:55 +01:00
Sanny 2f08bb465a Translations: Update Italian
Currently translated at 36.5% (2257 of 6171 strings)

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

powered by weblate
2025-11-14 16:31:58 +01:00
CVZ-es 4fb048e3a9 Translations: Update Spanish
Currently translated at 99.9% (6170 of 6171 strings)

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

powered by weblate
2025-11-14 11:12:42 +01:00
CVZ-es 82af3012bd Translations: Update French
Currently translated at 99.9% (6170 of 6171 strings)

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

powered by weblate
2025-11-14 11:12:42 +01:00
Andrii Andriiashyn 11425f21e6 Translations: Update Ukrainian
Currently translated at 57.1% (3528 of 6171 strings)

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

powered by weblate
2025-11-14 11:12:42 +01:00
Andrii Andriiashyn 55f35a998b Translations: Update Ukrainian
Currently translated at 57.0% (3523 of 6171 strings)

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

powered by weblate
2025-11-14 11:12:42 +01:00
CVZ-es 53cfce2ce7 Translations: Update Spanish
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-14 11:12:42 +01:00
CVZ-es 68ce335034 Translations: Update French
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-14 11:12:42 +01:00
dependabot[bot] 6ce5c1a26a Update sentry-sdk requirement from ==2.43.* to ==2.44.* (#5606)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.43.0...2.44.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.44.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-14 10:02:12 +01:00
dependabot[bot] ae4540acd7 Update pypdf requirement from ==6.1.* to ==6.2.* (#5604)
Updates the requirements on [pypdf](https://github.com/py-pdf/pypdf) to permit the latest version.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.1.0...6.2.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.2.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-14 10:01:58 +01:00
luelista a814d31c9b Re-check maximum order size during _perform_order (Z#23213046) (#5586)
* Re-check maximum order size during _perform_order (Z#23213046)

* Add test case
2025-11-14 10:01:51 +01:00
Raphael Michel ef9863518b Fix syntax error 2025-11-14 09:57:29 +01:00
Raphael MichelandRichard Schreiber eb740204d4 Invoice issuer address: Add state field (#5603)
* Invoice issuer address: Add state field

* Update src/pretix/base/settings.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

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

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-11-14 09:56:46 +01:00
Raphael MichelandRichard Schreiber 5583298322 Auto-verify user email addresses on accepting invites (#5609)
* Auto-verify user email addresses on accepting invites

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

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-11-14 09:55:18 +01:00
Raphael Michel 74b06435a0 Meta properties: Add helper to sort values (Z#23213668) (#5597) 2025-11-14 09:49:40 +01:00
Raphael Michel a26b0c5512 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-12 17:21:35 +01:00
Raphael Michel 095e07b3f1 Translations: Update German
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-12 17:21:35 +01:00
Raphael Michel b2eb1b6231 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-12 17:21:35 +01:00
Raphael Michel 9d838f1d9c Translations: Update German
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-12 17:21:35 +01:00
Raphael Michel cbf6bd29b0 Translations: Update German (informal) (de_Informal)
Currently translated at 99.5% (6146 of 6171 strings)

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

powered by weblate
2025-11-12 17:21:35 +01:00
Raphael Michel 0e84df9af2 Translations: Update German
Currently translated at 100.0% (6171 of 6171 strings)

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

powered by weblate
2025-11-12 17:21:35 +01:00
Raphael Michel 7feacc8a1a Translations: Update German (informal) (de_Informal)
Currently translated at 99.3% (6132 of 6171 strings)

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

powered by weblate
2025-11-12 17:21:35 +01:00
Raphael Michel 5ada22dd15 Translations: Update German
Currently translated at 99.7% (6155 of 6171 strings)

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

powered by weblate
2025-11-12 17:21:35 +01:00
Raphael Michel 6d56011695 Translations: Update wordlists 2025-11-12 17:07:42 +01:00
Raphael Michel da167eacd5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2025-11-12 14:21:28 +01:00
Ana Rute Pacheco Vivas 5df0c55daa Translations: Update Portuguese (Portugal)
Currently translated at 50.0% (127 of 254 strings)

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

powered by weblate
2025-11-12 14:20:18 +01:00
Ana Rute Pacheco Vivas b01e798b48 Translations: Update Portuguese (Portugal)
Currently translated at 80.8% (4955 of 6132 strings)

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

powered by weblate
2025-11-12 14:20:18 +01:00
luelista 0256ee76db Optionally show organizer slug in select2 (#5605) 2025-11-12 13:28:31 +01:00
Raphael MichelandRichard Schreiber e99eecb8be Product list: Show number of items currently in cart (Z#23212546) (#5599)
* Product list: Show number of items currently in cart

* Apply suggestions from code review

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Add display property

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-11-11 08:05:40 +01:00
CVZ-es d1ae579a6f Translations: Update Spanish
Currently translated at 100.0% (6132 of 6132 strings)

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

powered by weblate
2025-11-10 13:00:20 +01:00
Yasunobu YesNo Kawaguchi 90d3f50eba Translations: Update Japanese
Currently translated at 100.0% (254 of 254 strings)

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

powered by weblate
2025-11-10 13:00:20 +01:00
Yasunobu YesNo Kawaguchi c1b6d660a4 Translations: Update Japanese
Currently translated at 100.0% (6132 of 6132 strings)

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

powered by weblate
2025-11-10 13:00:20 +01:00
Linnea Thelander 0b88b63597 Translations: Update Swedish
Currently translated at 91.0% (5582 of 6132 strings)

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

powered by weblate
2025-11-10 13:00:20 +01:00
Richard Schreiber 0cc6439748 Fix API-docs missing item_program_times (#5594) 2025-11-07 13:04:04 +01:00
Richard Schreiber ad53c48d0f Fix price-column in item export for free variations 2025-11-07 11:57:06 +01:00
luelista 59a5c11ef6 Rename migration (#5592) 2025-11-07 11:40:27 +01:00
luelista 1cb2d443f9 Validation of user email addresses (#5434)
* Validation of user email addresses
* Improve email and password change forms
2025-11-07 11:17:34 +01:00
Raphael Michel a0dbf6c5db Force Django upgrade (CVE-2025-64459) 2025-11-06 15:04:03 +01:00
fd9d03786b Add program times for items (Z#23178639)
* Add program times for items

* Fix frontend date validation

* Add ical data for program times [wip]

* Improve ical data for program times

* Remove duplicate code and add comments

* Adjust migration

* Remove program times form for event series

* Add pdf placeholder [wip]

* Improve explanation text with suggestion

Co-authored-by: Raphael Michel <michel@pretix.eu>

* Fix import sorting

* Improve ical generation

* Improve ical entry description

* Fix migration

* Add copyability for program times fot items and events

* Update migration

* Add API endpoints/functions, fix isort

* Improve variable name

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Remove todo comment

* Add documentation, Change endpoint name

* Change related name

* Remove unnecessary code block

* Add program times to item API

* Fix imports

* Add log text

* Use daterange helper

* Add and update API tests

* Add another API test

* Add program times to cloning tests

* Update query count because of program times query

* Invalidate cached tickets on program time changes

* Reduce invalidation calls

* Update migration after rebase

* Apply improvements to invalidation from review

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* remove unneccessary attr=item param

* remove unnecessary kwargs for formset_factory

* fix local var name being overwritten in for-loop

* fix empty formset being saved

* Use subevent if available

* make code less verbose

* remove double event-label in ical desc

* fix unnecessary var re-assign

* fix ev vs p.subevent

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-11-06 12:24:47 +01:00
Richard Schreiber 7041d40972 Invoice-PDF: split line.description into multiple rows so table can pagebreak (#5545) 2025-11-06 09:44:23 +01:00
Richard Schreiber 0b46982e6d Allow blocked seats to be booked in backend (#5585) 2025-11-06 08:02:42 +01:00
281 changed files with 128788 additions and 112437 deletions
+1
View File
@@ -19,6 +19,7 @@ at :ref:`plugin-docs`.
item_bundles
item_add-ons
item_meta_properties
item_program_times
questions
question_options
quotas
+3
View File
@@ -22,6 +22,7 @@ invoice_from_name string Sender address:
invoice_from string Sender address: Address lines
invoice_from_zipcode string Sender address: ZIP code
invoice_from_city string Sender address: City
invoice_from_state string Sender address: State (only used in some countries)
invoice_from_country string Sender address: Country code
invoice_from_tax_id string Sender address: Local Tax ID
invoice_from_vat_id string Sender address: EU VAT ID
@@ -233,6 +234,7 @@ List of all invoices
"invoice_from": "Demo street 12",
"invoice_from_zipcode":"",
"invoice_from_city":"Demo town",
"invoice_from_state":"CA",
"invoice_from_country":"US",
"invoice_from_tax_id":"",
"invoice_from_vat_id":"",
@@ -381,6 +383,7 @@ Fetching individual invoices
"invoice_from": "Demo street 12",
"invoice_from_zipcode":"",
"invoice_from_city":"Demo town",
"invoice_from_state":"CA",
"invoice_from_country":"US",
"invoice_from_tax_id":"",
"invoice_from_vat_id":"",
+223
View File
@@ -0,0 +1,223 @@
Item program times
==================
Resource description
--------------------
Program times for products (items) that can be set in addition to event times, e.g. to display seperate schedules within an event.
Note that ``program_times`` are not available for items inside event series.
The program times resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the program time
start datetime The start date time for this program time slot.
end datetime The end date time for this program time slot.
===================================== ========================== =======================================================
.. versionchanged:: TODO
The resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/
Returns a list of all program times for a given item.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/items/11/program_times/ 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": 3,
"next": null,
"previous": null,
"results": [
{
"id": 2,
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
},
{
"id": 3,
"start": "2025-08-12T22:00:00Z",
"end": "2025-08-13T22:00:00Z"
},
{
"id": 14,
"start": "2025-08-15T22:00:00Z",
"end": "2025-08-17T22:00:00Z"
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param item: The ``id`` field of the item to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/item does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/(id)/
Returns information on one program time, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"start": "2025-08-15T22:00:00Z",
"end": "2025-10-27T23:00:00Z"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param item: The ``id`` field of the item to fetch
:param id: The ``id`` field of the program time to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/
Creates a new program time
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 17,
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a program time for
:param event: The ``slug`` field of the event to create a program time for
:param item: The ``id`` field of the item to create a program time for
:statuscode 201: no error
:statuscode 400: The program time could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/(id)/
Update a program time. 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/items/1/program_times/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"start": "2025-08-14T10:00:00Z"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"start": "2025-08-14T10:00:00Z",
"end": "2025-08-15T12:00:00Z"
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the item to modify
:param id: The ``id`` field of the program time to modify
:statuscode 200: no error
:statuscode 400: The program time could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/program_times/(id)/
Delete a program time.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the item to modify
:param id: The ``id`` field of the program time to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
+36 -10
View File
@@ -139,6 +139,10 @@ has_variations boolean Shows whether
variations list of objects A list with one object for each variation of this item.
Can be empty. Only writable during creation,
use separate endpoint to modify this later.
program_times list of objects A list with one object for each program time of this item.
Can be empty. Only writable during creation,
use separate endpoint to modify this later.
Not available for items in event series.
├ id integer Internal ID of the variation
├ value multi-lingual string The "name" of the variation
├ default_price money (string) The price set directly for this variation or ``null``
@@ -225,6 +229,10 @@ meta_data object Values set fo
The ``hidden_if_item_available_mode`` attributes has been added.
.. versionchanged:: 2025.9
The ``program_times`` attribute has been added.
Notes
-----
@@ -232,9 +240,11 @@ Please note that an item either always has variations or never has. Once created
change to an item without and vice versa. To create an item with variations ensure that you POST an item with at least
one variation.
Also note that ``variations``, ``bundles``, and ``addons`` are only supported on ``POST``. To update/delete variations,
bundles, and add-ons please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
with nested ``variations``, ``bundles`` and/or ``addons``.
Also note that ``variations``, ``bundles``, ``addons`` and ``program_times`` are only supported on ``POST``. To update/delete variations,
bundles, add-ons and program times please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
with nested ``variations``, ``bundles``, ``addons`` and/or ``program_times``.
``program_times`` is not available to items in event series.
Endpoints
---------
@@ -373,7 +383,8 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": []
}
]
}
@@ -525,7 +536,8 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": []
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -653,7 +665,13 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": [
{
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
}
]
}
**Example response**:
@@ -773,7 +791,13 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": [
{
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
}
]
}
:param organizer: The ``slug`` field of the organizer of the event to create an item for
@@ -789,8 +813,9 @@ Endpoints
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 ``has_variations``, ``variations`` and the ``addon`` field. If
you need to update/delete variations or add-ons please use the nested dedicated endpoints.
You can change all fields of the resource except the ``has_variations``, ``variations``, ``addon`` and the
``program_times`` field. If you need to update/delete variations, add-ons or program times, please use the nested
dedicated endpoints.
**Example request**:
@@ -924,7 +949,8 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": []
}
:param organizer: The ``slug`` field of the organizer to modify
+1 -1
View File
@@ -211,7 +211,7 @@ The line-based computation has a few significant advantages:
The main disadvantage is that the tax looks "wrong" when computed from the sum. Taking the sum of net prices (420.15)
and multiplying it with the tax rate (19%) yields a tax amount of 79.83 (instead of 79.85) and a gross sum of 499.98
(instead of 499.98). This becomes a problem when juristictions, data formats, or external systems expect this calculation
(instead of 500.00). This becomes a problem when juristictions, data formats, or external systems expect this calculation
to work on the level of the entire order. A prominent example is the EN 16931 standard for e-invoicing that
does not allow the computation as created by pretix.
+6 -6
View File
@@ -30,12 +30,13 @@ dependencies = [
"babel",
"BeautifulSoup4==4.14.*",
"bleach==6.2.*",
"celery==5.5.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=44.0.0",
"css-inline==0.18.*",
"defusedcsv>=1.1.0",
"Django[argon2]==4.2.*,>=4.2.24",
"dnspython==2.*",
"Django[argon2]==4.2.*,>=4.2.26",
"django-bootstrap3==25.2",
"django-compressor==4.5.1",
"django-countries==7.6.*",
@@ -61,7 +62,7 @@ dependencies = [
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.5.*",
"kombu==5.6.*",
"libsass==0.23.*",
"lxml",
"markdown==3.9", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
@@ -81,7 +82,7 @@ dependencies = [
"pycountry",
"pycparser==2.23",
"pycryptodome==3.23.*",
"pypdf==6.1.*",
"pypdf==6.4.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
@@ -91,14 +92,13 @@ dependencies = [
"redis==6.4.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.43.*",
"sentry-sdk==2.47.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2020041600",
"tqdm==4.*",
"ua-parser==1.0.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.7.*",
"zeep==4.3.*"
+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__ = "2025.10.0.dev0"
__version__ = "2025.11.0.dev0"
@@ -0,0 +1,23 @@
# Generated by Django 4.2.24 on 2025-11-14 16:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixapi", "0013_alter_webhookcallretry_retry_not_before"),
]
operations = [
migrations.AlterField(
model_name="webhook",
name="target_url",
field=models.URLField(max_length=1024),
),
migrations.AlterField(
model_name="webhookcall",
name="target_url",
field=models.URLField(max_length=1024),
),
]
+2 -2
View File
@@ -114,7 +114,7 @@ class OAuthRefreshToken(AbstractRefreshToken):
class WebHook(models.Model):
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
target_url = models.URLField(verbose_name=_("Target URL"), max_length=255)
target_url = models.URLField(verbose_name=_("Target URL"), max_length=1024)
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
comment = models.CharField(verbose_name=_("Comment"), max_length=255, null=True, blank=True)
@@ -140,7 +140,7 @@ class WebHookEventListener(models.Model):
class WebHookCall(models.Model):
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
datetime = models.DateTimeField(auto_now_add=True)
target_url = models.URLField(max_length=255)
target_url = models.URLField(max_length=1024)
action_type = models.CharField(max_length=255)
is_retry = models.BooleanField(default=False)
execution_time = models.FloatField(null=True)
+4
View File
@@ -795,6 +795,7 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_vatid_required_countries',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
@@ -820,6 +821,7 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_address_from',
'invoice_address_from_zipcode',
'invoice_address_from_city',
'invoice_address_from_state',
'invoice_address_from_country',
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
@@ -942,6 +944,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_vatid_required_countries',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
@@ -952,6 +955,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'invoice_address_from',
'invoice_address_from_zipcode',
'invoice_address_from_city',
'invoice_address_from_state',
'invoice_address_from_country',
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
+55 -6
View File
@@ -47,8 +47,9 @@ from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.fields import UploadedFileField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemProgramTime,
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
SalesChannel,
)
@@ -187,6 +188,12 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
'position', 'price_included', 'multi_allowed')
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('start', 'end')
class ItemBundleSerializer(serializers.ModelSerializer):
class Meta:
model = ItemBundle
@@ -212,6 +219,37 @@ class ItemBundleSerializer(serializers.ModelSerializer):
return data
class ItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('id', 'start', 'end')
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
start = full_data.get('start')
if not start:
raise ValidationError(_("The program start must not be empty."))
end = full_data.get('end')
if not end:
raise ValidationError(_("The program end must not be empty."))
if start > end:
raise ValidationError(_("The program end must not be before the program start."))
event = self.context['event']
if event.has_subevents:
raise ValidationError({
_("You cannot use program times on an event series.")
})
return data
class ItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
@@ -250,6 +288,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
addons = InlineItemAddOnSerializer(many=True, required=False)
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True)
meta_data = MetaDataField(required=False, source='*')
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
@@ -271,7 +310,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'addons', 'bundles', 'program_times', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
@@ -294,9 +333,9 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
def validate(self, data):
data = super().validate(data)
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
raise ValidationError(_('Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use the '
'dedicated nested endpoint.'))
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data or 'program_times' in data):
raise ValidationError(_('Updating add-ons, bundles, program times or variations via PATCH/PUT is not '
'supported. Please use the dedicated nested endpoint.'))
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
@@ -347,6 +386,13 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
return value
def validate_program_times(self, value):
if not self.instance:
for program_time_data in value:
ItemProgramTime.clean_start_end(self, start=program_time_data.get('start', None),
end=program_time_data.get('end', None))
return value
@cached_property
def item_meta_properties(self):
return {
@@ -364,6 +410,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
program_times_data = validated_data.pop('program_times') if 'program_times' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
picture = validated_data.pop('picture', None)
require_membership_types = validated_data.pop('require_membership_types', [])
@@ -398,6 +445,8 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
ItemAddOn.objects.create(base_item=item, **addon_data)
for bundle_data in bundles_data:
ItemBundle.objects.create(base_item=item, **bundle_data)
for program_time_data in program_times_data:
ItemProgramTime.objects.create(item=item, **program_time_data)
# Meta data
if meta_data is not None:
+1 -1
View File
@@ -1831,7 +1831,7 @@ class InvoiceSerializer(I18nAwareModelSerializer):
class Meta:
model = Invoice
fields = ('event', 'order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
'invoice_from_city', 'invoice_from_state', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
'invoice_to', 'invoice_to_is_business', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street',
'invoice_to_zipcode', 'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id',
'invoice_to_beneficiary', 'invoice_to_transmission_info', 'custom_field', 'date', 'refers', 'locale',
+1
View File
@@ -112,6 +112,7 @@ item_router = routers.DefaultRouter()
item_router.register(r'variations', item.ItemVariationViewSet)
item_router.register(r'addons', item.ItemAddOnViewSet)
item_router.register(r'bundles', item.ItemBundleViewSet)
item_router.register(r'program_times', item.ItemProgramTimeViewSet)
order_router = routers.DefaultRouter()
order_router.register(r'payments', order.PaymentViewSet)
+59 -6
View File
@@ -40,19 +40,19 @@ from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.response import Response
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
ItemSerializer, ItemVariationSerializer, QuestionOptionSerializer,
QuestionSerializer, QuotaSerializer,
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
Question, QuestionOption, Quota,
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
ItemVariation, Question, QuestionOption, Quota,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.dicts import merge_dicts
@@ -279,6 +279,59 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
)
class ItemProgramTimeViewSet(viewsets.ModelViewSet):
serializer_class = ItemProgramTimeSerializer
queryset = ItemProgramTime.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
ordering_fields = ('id',)
ordering = ('id',)
permission = None
write_permission = 'can_change_items'
@cached_property
def item(self):
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
def get_queryset(self):
if self.request.event.has_subevents:
raise ValidationError('You cannot use program times on an event series.')
return self.item.program_times.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['item'] = self.item
return ctx
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
serializer.save(item=item)
item.log_action(
'pretix.event.item.program_times.added',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.item.log_action(
'pretix.event.item.program_times.changed',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.item.log_action(
'pretix.event.item.program_times.removed',
user=self.request.user,
auth=self.request.auth,
data={'start': instance.start, 'end': instance.end}
)
class ItemAddOnViewSet(viewsets.ModelViewSet):
serializer_class = ItemAddOnSerializer
queryset = ItemAddOn.objects.none()
@@ -514,7 +567,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
write_permission = 'can_change_items'
def get_queryset(self):
return self.request.event.quotas.all()
return self.request.event.quotas.select_related('subevent').prefetch_related('items', 'variations').all()
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset()).distinct()
+1 -1
View File
@@ -721,7 +721,7 @@ class MembershipViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Membership.objects.filter(
customer__organizer=self.request.organizer
)
).select_related('customer')
def get_serializer_context(self):
ctx = super().get_serializer_context()
+7 -1
View File
@@ -19,6 +19,7 @@
# 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 django.db import transaction
from django.db.models import F, Q
from django.utils.timezone import now
@@ -64,8 +65,13 @@ class VoucherViewSet(viewsets.ModelViewSet):
permission = 'can_view_vouchers'
write_permission = 'can_change_vouchers'
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
def get_queryset(self):
return self.request.event.vouchers.select_related('seat').all()
return Voucher.annotate_budget_used(
self.request.event.vouchers
).select_related(
'item', 'quota', 'seat', 'variation'
)
@transaction.atomic()
def create(self, request, *args, **kwargs):
+5 -1
View File
@@ -43,6 +43,7 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import periodic_task
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.celery import get_task_priority
logger = logging.getLogger(__name__)
_ALL_EVENTS = None
@@ -474,7 +475,10 @@ def notify_webhooks(logentry_ids: list):
)
for wh in webhooks:
send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk))
send_webhook.apply_async(
args=(logentry.id, notification_type.action_type, wh.pk),
priority=get_task_priority("notifications", logentry.organizer_id),
)
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=60, acks_late=True, autoretry_for=(DatabaseError,),)
-17
View File
@@ -112,23 +112,6 @@ def oidc_validate_and_complete_config(config):
scope="openid",
))
for scope in config["scope"].split(" "):
if scope not in provider_config.get("scopes_supported", []):
raise ValidationError(_('You are requesting scope "{scope}" but provider only supports these: {scopes}.').format(
scope=scope,
scopes=", ".join(provider_config.get("scopes_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", []))
))
if "token_endpoint_auth_methods_supported" in provider_config:
token_endpoint_auth_methods_supported = provider_config.get("token_endpoint_auth_methods_supported",
["client_secret_basic"])
+20 -5
View File
@@ -24,6 +24,7 @@ from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
import bleach
import css_inline
from django.conf import settings
from django.core.mail.backends.smtp import EmailBackend
@@ -34,7 +35,10 @@ from django.utils.translation import get_language, gettext_lazy as _
from pretix.base.models import Event
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.templatetags.rich_text import (
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
markdown_compile_email, truelink_callback,
)
from pretix.helpers.format import SafeFormatter, format_map
from pretix.base.services.placeholders import ( # noqa
@@ -133,13 +137,24 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def compile_markdown(self, plaintext):
return markdown_compile_email(plaintext)
def compile_markdown(self, plaintext, context=None):
return markdown_compile_email(plaintext, context=context)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
body_md = self.compile_markdown(plain_body)
body_md = self.compile_markdown(plain_body, context)
if context:
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
linker = bleach.Linker(
url_re=URL_RE,
email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True
)
body_md = format_map(
body_md,
context=context,
mode=SafeFormatter.MODE_RICH_TO_HTML,
linkifier=linker
)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
+2
View File
@@ -209,6 +209,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Invoice sender:') + ' ' + _('Address'),
_('Invoice sender:') + ' ' + _('ZIP code'),
_('Invoice sender:') + ' ' + _('City'),
_('Invoice sender:') + ' ' + pgettext('address', 'State'),
_('Invoice sender:') + ' ' + _('Country'),
_('Invoice sender:') + ' ' + _('Tax ID'),
_('Invoice sender:') + ' ' + _('VAT ID'),
@@ -291,6 +292,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
i.invoice_from,
i.invoice_from_zipcode,
i.invoice_from_city,
i.invoice_from_state,
i.invoice_from_country,
i.invoice_from_tax_id,
i.invoice_from_vat_id,
+1 -1
View File
@@ -149,7 +149,7 @@ class ItemDataExporter(ListExporter):
row += [
_("Yes") if i.active and v.active else "",
", ".join([str(sn.label) for sn in sales_channels]),
v.default_price or i.default_price,
v.default_price if v.default_price is not None else i.default_price,
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
_("Yes") if i.admission else "",
+2 -2
View File
@@ -610,7 +610,7 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Attendee name') + ': ' + str(label))
headers += [
_('Attendee email'),
_('Company'),
_('Attendee company'),
_('Address'),
_('ZIP code'),
_('City'),
@@ -650,7 +650,7 @@ class OrderListExporter(MultiSheetListExporter):
options[q.pk].append(o)
headers.append(str(q.question))
headers += [
_('Company'),
_('Invoice address company'),
_('Invoice address name'),
]
if name_scheme and len(name_scheme['fields']) > 1:
+29 -9
View File
@@ -214,21 +214,38 @@ class PasswordRecoverForm(forms.Form):
error_messages = {
'pw_mismatch': _("Please enter the same password twice"),
}
email = forms.EmailField(
max_length=255,
disabled=True,
label=_("Your email address"),
widget=forms.EmailInput(
attrs={'autocomplete': 'username'},
),
)
password = forms.CharField(
label=_('Password'),
widget=forms.PasswordInput,
widget=forms.PasswordInput(attrs={
'autocomplete': 'new-password',
}),
max_length=4096,
required=True
)
password_repeat = forms.CharField(
label=_('Repeat password'),
widget=forms.PasswordInput,
widget=forms.PasswordInput(attrs={
'autocomplete': 'new-password',
}),
max_length=4096,
)
def __init__(self, user_id=None, *args, **kwargs):
self.user_id = user_id
super().__init__(*args, **kwargs)
initial = kwargs.pop('initial', {})
try:
self.user = User.objects.get(id=user_id)
initial['email'] = self.user.email
except User.DoesNotExist:
self.user = None
super().__init__(*args, initial=initial, **kwargs)
def clean(self):
password1 = self.cleaned_data.get('password', '')
@@ -243,11 +260,7 @@ class PasswordRecoverForm(forms.Form):
def clean_password(self):
password1 = self.cleaned_data.get('password', '')
try:
user = User.objects.get(id=self.user_id)
except User.DoesNotExist:
user = None
if validate_password(password1, user=user) is not None:
if validate_password(password1, user=self.user) is not None:
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1
@@ -307,3 +320,10 @@ class ReauthForm(forms.Form):
self.error_messages['inactive'],
code='inactive',
)
class ConfirmationCodeForm(forms.Form):
code = forms.IntegerField(
label=_('Confirmation code'),
widget=forms.NumberInput(attrs={'class': 'confirmation-code-input', 'inputmode': 'numeric', 'type': 'text'}),
)
+18 -6
View File
@@ -83,7 +83,7 @@ from pretix.base.invoicing.transmission import (
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
from pretix.base.models.tax import ask_for_vat_id
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
VATIDFinalError, VATIDTemporaryError, normalize_vat_id, validate_vat_id,
)
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
@@ -1165,13 +1165,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['vat_id'].help_text = '<br/>'.join([
str(_('Optional, but depending on the country you reside in we might need to charge you '
'additional taxes if you do not enter it.')),
str(_('If you are registered in Switzerland, you can enter your UID instead.')),
])
else:
self.fields['vat_id'].help_text = '<br/>'.join([
str(_('Optional, but it might be required for you to claim tax benefits on your invoice '
'depending on your and the sellers country of residence.')),
str(_('If you are registered in Switzerland, you can enter your UID instead.')),
])
transmission_type_choices = [
@@ -1358,13 +1356,24 @@ class BaseInvoiceAddressForm(forms.ModelForm):
"transmission method.")}
)
vat_id_applicable = (
'vat_id' in self.fields and
data.get('is_business') and
ask_for_vat_id(data.get('country'))
)
vat_id_required = vat_id_applicable and str(data.get('country')) in self.event.settings.invoice_address_vatid_required_countries
if vat_id_required and not data.get('vat_id'):
raise ValidationError({
"vat_id": _("This field is required.")
})
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and ask_for_vat_id(data.get('country')) and data.get('vat_id'):
pass # Skip re-validation if it is validated
elif self.validate_vat_id and vat_id_applicable:
try:
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = True
self.instance.vat_id = normalized_id
self.instance.vat_id = data['vat_id'] = normalized_id
except VATIDFinalError as e:
if self.all_optional:
self.instance.vat_id_validated = False
@@ -1372,6 +1381,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
else:
raise ValidationError({"vat_id": e.message})
except VATIDTemporaryError as e:
# We couldn't check it online, but we can still normalize it
normalized_id = normalize_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id = data['vat_id'] = normalized_id
self.instance.vat_id_validated = False
if self.request and self.vat_warning:
messages.warning(self.request, e.message)
+81 -67
View File
@@ -39,37 +39,16 @@ from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
)
from django.db.models import Q
from django.urls.base import reverse
from django.utils.translation import gettext_lazy as _
from pytz import common_timezones
from pretix.base.models import User
from pretix.control.forms import SingleLanguageWidget
from pretix.helpers.format import format_map
class UserSettingsForm(forms.ModelForm):
error_messages = {
'duplicate_identifier': _("There already is an account associated with this email address. "
"Please choose a different one."),
'pw_current': _("Please enter your current password if you want to change your email address "
"or password."),
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
'pw_equal': _("Please choose a password different to your current one.")
}
old_pw = forms.CharField(max_length=255,
required=False,
label=_("Your current password"),
widget=forms.PasswordInput())
new_pw = forms.CharField(max_length=255,
required=False,
label=_("New password"),
widget=forms.PasswordInput())
new_pw_repeat = forms.CharField(max_length=255,
required=False,
label=_("Repeat new password"),
widget=forms.PasswordInput())
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
@@ -93,16 +72,63 @@ class UserSettingsForm(forms.ModelForm):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['email'].required = True
if self.user.auth_backend != 'native':
del self.fields['old_pw']
del self.fields['new_pw']
del self.fields['new_pw_repeat']
self.fields['email'].disabled = True
self.fields['email'].disabled = True
self.fields['email'].help_text = format_map('<a href="{link}"><span class="fa fa-edit"></span> {text}</a>', {
'text': _("Change email address"),
'link': reverse('control:user.settings.email.change')
})
class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
))
class UserPasswordChangeForm(forms.Form):
error_messages = {
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
'pw_equal': _("Please choose a password different to your current one.")
}
email = forms.EmailField(max_length=255,
disabled=True,
label=_("Your email address"),
widget=forms.EmailInput(
attrs={'autocomplete': 'username'},
))
old_pw = forms.CharField(max_length=255,
required=True,
label=_("Your current password"),
widget=forms.PasswordInput(
attrs={'autocomplete': 'current-password'},
))
new_pw = forms.CharField(max_length=255,
required=True,
label=_("New password"),
widget=forms.PasswordInput(
attrs={'autocomplete': 'new-password'},
))
new_pw_repeat = forms.CharField(max_length=255,
required=True,
label=_("Repeat new password"),
widget=forms.PasswordInput(
attrs={'autocomplete': 'new-password'},
))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
initial = kwargs.pop('initial', {})
initial['email'] = self.user.email
super().__init__(*args, initial=initial, **kwargs)
def clean_old_pw(self):
old_pw = self.cleaned_data.get('old_pw')
if old_pw and settings.HAS_REDIS:
if settings.HAS_REDIS:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.incr('pretix_pwchange_%s' % self.user.pk)
@@ -113,7 +139,7 @@ class UserSettingsForm(forms.ModelForm):
code='rate_limit',
)
if old_pw and not check_password(old_pw, self.user.password):
if not check_password(old_pw, self.user.password):
raise forms.ValidationError(
self.error_messages['pw_current_wrong'],
code='pw_current_wrong',
@@ -121,59 +147,47 @@ class UserSettingsForm(forms.ModelForm):
return old_pw
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.instance.pk)).exists():
raise forms.ValidationError(
self.error_messages['duplicate_identifier'],
code='duplicate_identifier',
)
return email
def clean_new_pw(self):
password1 = self.cleaned_data.get('new_pw', '')
if password1 and validate_password(password1, user=self.user) is not None:
if validate_password(password1, user=self.user) is not None:
raise forms.ValidationError(
_(password_validators_help_texts()),
code='pw_invalid'
)
if self.user.check_password(password1):
raise forms.ValidationError(
self.error_messages['pw_equal'],
code='pw_equal',
)
return password1
def clean_new_pw_repeat(self):
password1 = self.cleaned_data.get('new_pw')
password2 = self.cleaned_data.get('new_pw_repeat')
if password1 and password1 != password2:
if password1 != password2:
raise forms.ValidationError(
self.error_messages['pw_mismatch'],
code='pw_mismatch'
)
def clean(self):
password1 = self.cleaned_data.get('new_pw')
email = self.cleaned_data.get('email')
old_pw = self.cleaned_data.get('old_pw')
if (password1 or email != self.user.email) and not old_pw:
class UserEmailChangeForm(forms.Form):
error_messages = {
'duplicate_identifier': _("There already is an account associated with this email address. "
"Please choose a different one."),
}
old_email = forms.EmailField(label=_('Old email address'), disabled=True)
new_email = forms.EmailField(label=_('New email address'))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
def clean_new_email(self):
email = self.cleaned_data['new_email']
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
raise forms.ValidationError(
self.error_messages['pw_current'],
code='pw_current'
self.error_messages['duplicate_identifier'],
code='duplicate_identifier',
)
if password1 and password1 == old_pw:
raise forms.ValidationError(
self.error_messages['pw_equal'],
code='pw_equal'
)
if password1:
self.instance.set_password(password1)
return self.cleaned_data
class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
))
return email
+93 -15
View File
@@ -23,6 +23,7 @@ import datetime
import logging
import math
import re
import textwrap
import unicodedata
from collections import defaultdict
from decimal import Decimal
@@ -31,7 +32,6 @@ from itertools import groupby
from typing import Tuple
import bleach
import vat_moss.exchange_rates
from bidi import get_display
from django.contrib.staticfiles import finders
from django.db.models import Sum
@@ -752,11 +752,59 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return dt.astimezone(tz).date()
total = Decimal('0.00')
if has_taxes:
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
colwidths = [a * doc.width for a in (.65, .20, .15)]
for (description, tax_rate, tax_name, net_value, gross_value, subevent, period_start, period_end), lines in addon_aware_groupby(
all_lines,
key=_group_key,
is_addon=lambda l: l.description.startswith(" +"),
):
# split description into multiple Paragraphs so each fits in a table cell on a single page
# otherwise PDF-build fails
description_p_list = []
# normalize linebreaks to newlines instead of HTML so we can safely substring
description = description.replace('<br>', '<br />').replace('<br />\n', '\n').replace('<br />', '\n')
# start first line with different settings than the rest of the description
curr_description = description.split("\n", maxsplit=1)[0]
cellpadding = 6 # default cellpadding is only set on right side of column
max_width = colwidths[0] - cellpadding
max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal']
for __ in range(1000):
p = FontFallbackParagraph(
self._clean_text(curr_description, tags=['br']),
p_style
)
h = p.wrap(max_width, doc.height)[1]
if h <= max_height:
description_p_list.append(p)
if curr_description == description:
break
description = description[len(curr_description):].lstrip()
curr_description = description.split("\n", maxsplit=1)[0]
# use different settings for all except first line
max_width = sum(colwidths[0:3 if has_taxes else 2]) - cellpadding
max_height = self.stylesheet['Fineprint'].leading * 8
p_style = self.stylesheet['Fineprint']
continue
if not description_p_list:
# first "manual" line is larger than 5 "real" lines => only allow one line and set rest in Fineprint
max_height = self.stylesheet['Normal'].leading
if h > max_height * 1.1:
# quickly bring the text-length down to a managable length to then stepwise reduce
wrap_to = math.ceil(len(curr_description) * max_height * 1.1 / h)
else:
# trim to 95% length, but at most 10 chars to not have strangely short lines in the middle of a paragraph
wrap_to = max(len(curr_description) - 10, math.ceil(len(curr_description) * 0.95))
curr_description = textwrap.wrap(curr_description, wrap_to, replace_whitespace=False, drop_whitespace=False)[0]
# Try to be clever and figure out when organizers would want to show the period. This heuristic is
# not perfect and the only "fully correct" way would be to include the period on every line always,
# however this will cause confusion (a) due to useless repetition of the same date all over the invoice
@@ -810,7 +858,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice
request_show_service_date = period_line
elif period_line:
description += "\n" + period_line
description_p_list.append(FontFallbackParagraph(
period_line,
self.stylesheet['Fineprint']
))
lines = list(lines)
if has_taxes:
@@ -819,13 +870,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency),
)
description = description + "\n" + single_price_line
description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append((
FontFallbackParagraph(
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
description_p_list.pop(0),
str(len(lines)),
localize(tax_rate) + " %",
FontFallbackParagraph(
@@ -837,23 +888,52 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
self.stylesheet['NormalRight']
),
))
for p in description_p_list:
tdata.append((p, "", "", "", ""))
tstyledata.append((
'SPAN',
(0, len(tdata) - 1),
(2, len(tdata) - 1),
))
else:
if len(lines) > 1:
single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency),
)
description = description + "\n" + single_price_line
description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append((
FontFallbackParagraph(
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
description_p_list.pop(0),
str(len(lines)),
FontFallbackParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
))
for p in description_p_list:
tdata.append((p, "", ""))
tstyledata.append((
'SPAN',
(0, len(tdata) - 1),
(1, len(tdata) - 1),
))
tstyledata += [
(
'BOTTOMPADDING',
(0, len(tdata) - len(description_p_list)),
(-1, len(tdata) - 2),
0
),
(
'TOPPADDING',
(0, len(tdata) - len(description_p_list)),
(-1, len(tdata) - 1),
0
),
]
taxvalue_map[tax_rate, tax_name] += (gross_value - net_value) * len(lines)
grossvalue_map[tax_rate, tax_name] += gross_value * len(lines)
total += gross_value * len(lines)
@@ -863,13 +943,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
FontFallbackParagraph(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([
FontFallbackParagraph(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)]
if not self.invoice.is_cancellation:
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
@@ -980,7 +1058,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def fmt(val):
try:
return vat_moss.exchange_rates.format(val, self.invoice.foreign_currency_display)
return money_filter(val, self.invoice.foreign_currency_display)
except ValueError:
return localize(val) + ' ' + self.invoice.foreign_currency_display
+31 -1
View File
@@ -19,8 +19,11 @@
# 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 base64
import hashlib
import re
import dns.resolver
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _, pgettext
@@ -123,6 +126,9 @@ class PeppolIdValidator:
"9959": ".*",
}
def __init__(self, validate_online=False):
self.validate_online = validate_online
def __call__(self, value):
if ":" not in value:
raise ValidationError(_("A Peppol participant ID always starts with a prefix, followed by a colon (:)."))
@@ -136,6 +142,28 @@ class PeppolIdValidator:
raise ValidationError(_("The Peppol participant ID does not match the validation rules for the prefix "
"%(number)s. Please reach out to us if you are sure this ID is correct."),
params={"number": prefix})
if self.validate_online:
base_hostnames = ['edelivery.tech.ec.europa.eu', 'acc.edelivery.tech.ec.europa.eu']
smp_id = base64.b32encode(hashlib.sha256(value.lower().encode()).digest()).decode().rstrip("=")
for base_hostname in base_hostnames:
smp_domain = f'{smp_id}.iso6523-actorid-upis.{base_hostname}'
resolver = dns.resolver.Resolver()
try:
answers = resolver.resolve(smp_domain, 'NAPTR', lifetime=1.0)
if answers:
return value
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
# ID not registered, do not set found=True
pass
except Exception: # noqa
# Error likely on our end or infrastructure is down, allow user to proceed
return value
raise ValidationError(
_("The Peppol participant ID is not registered on the Peppol network."),
)
return value
@@ -155,7 +183,9 @@ class PeppolTransmissionType(TransmissionType):
"transmission_peppol_participant_id": forms.CharField(
label=_("Peppol participant ID"),
validators=[
PeppolIdValidator(),
PeppolIdValidator(
validate_online=True,
),
]
),
}
@@ -0,0 +1,25 @@
# Generated by Django 4.2.19 on 2025-08-11 10:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0293_cartposition_price_includes_rounding_correction_and_more'),
]
operations = [
migrations.CreateModel(
name='ItemProgramTime',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
('item',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_times',
to='pretixbase.item')),
],
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.23 on 2025-09-04 16:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0294_item_program_time"),
]
operations = [
migrations.AddField(
model_name="user",
name="is_verified",
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.24 on 2025-11-10 16:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0295_user_is_verified"),
]
operations = [
migrations.AddField(
model_name="invoice",
name="invoice_from_state",
field=models.CharField(max_length=190, null=True),
),
]
+14
View File
@@ -47,6 +47,19 @@ class DataImportError(LazyLocaleException):
super().__init__(msg)
def rename_duplicates(values):
used = set()
had_duplicates = False
for i, value in enumerate(values):
c = 0
while values[i] in used:
c += 1
values[i] = f'{value}__{c}'
had_duplicates = True
used.add(values[i])
return had_duplicates
def parse_csv(file, length=None, mode="strict", charset=None):
file.seek(0)
data = file.read(length)
@@ -70,6 +83,7 @@ def parse_csv(file, length=None, mode="strict", charset=None):
return None
reader = csv.DictReader(io.StringIO(data), dialect=dialect)
reader._had_duplicates = rename_duplicates(reader.fieldnames)
return reader
+3 -2
View File
@@ -36,8 +36,9 @@ from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
SubEventItem, SubEventItemVariation, itempicture_upload_to,
ItemProgramTime, ItemVariation, ItemVariationMetaValue, Question,
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
itempicture_upload_to,
)
from .log import LogEntry
from .media import ReusableMedium
+75
View File
@@ -35,6 +35,7 @@
import binascii
import json
import operator
import secrets
from datetime import timedelta
from functools import reduce
@@ -44,6 +45,7 @@ from django.contrib.auth.models import (
)
from django.contrib.auth.tokens import default_token_generator
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import BadRequest, PermissionDenied
from django.db import IntegrityError, models, transaction
from django.db.models import Q
from django.utils.crypto import get_random_string, salted_hmac
@@ -239,9 +241,11 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
MAX_CONFIRMATION_CODE_ATTEMPTS = 10
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
verbose_name=_('Email'), max_length=190)
is_verified = models.BooleanField(default=False, verbose_name=_('Verified email address'))
fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name'))
is_active = models.BooleanField(default=True,
@@ -353,6 +357,77 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
except SendMailException:
pass # Already logged
def send_confirmation_code(self, session, reason, email=None, state=None):
"""
Sends a confirmation code via email to the user. The code is only valid for the action specified by `reason`.
The email is either sent to the email address currently on file for the user, or to the one given in the optional `email` parameter.
A `state` value can be provided which is bound to this confirmation code, and returned on successfully checking the code.
:param session: the user's request session
:param reason: the action which should be confirmed using this confirmation code (currently, only `email_change` is allowed)
:param email: optional, the email address to send the confirmation code to
:param state: optional
"""
from pretix.base.services.mail import mail
with language(self.locale):
if reason == 'email_change':
msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format(
old_email=self.email, new_email=email,
))
elif reason == 'email_verify':
msg = str(_('to confirm that your email address {email} belongs to your pretix account, use the following code:').format(
email=self.email,
))
else:
raise Exception('Invalid confirmation code reason')
code = "%07d" % secrets.SystemRandom().randint(0, 9999999)
session['user_confirmation_code:' + reason] = {
'code': code,
'state': state,
'attempts': 0,
}
mail(
email or self.email,
_('pretix confirmation code'),
'pretixcontrol/email/confirmation_code.txt',
{
'user': self,
'reason': msg,
'code': code,
},
event=None,
user=self,
locale=self.locale
)
def check_confirmation_code(self, session, reason, code):
"""
Checks a confirmation code entered by the user against the valid code stored in the session.
If the code is correct, an optional state bound to the code is returned.
If the code is incorrect, PermissionDenied is raised. If the code could not be validated, either because no
code for the given reason is stored, or the number of input attempts is exceeded, BadRequest is raised.
:param session: the user's request session
:param reason: the action which should be confirmed using this confirmation code
:param code: the code entered by the user
:return: optional state bound to this code using the state parameter of send_confirmation_code, None otherwise
"""
stored = session.get('user_confirmation_code:' + reason)
if not stored:
raise BadRequest
if stored['attempts'] > User.MAX_CONFIRMATION_CODE_ATTEMPTS:
raise BadRequest
if int(stored['code']) == int(code):
del session['user_confirmation_code:' + reason]
return stored['state']
else:
stored['attempts'] += 1
session['user_confirmation_code:' + reason] = stored
raise PermissionDenied
def send_password_reset(self):
from pretix.base.services.mail import mail
+9 -2
View File
@@ -31,6 +31,7 @@ from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from pretix.helpers.celery import get_task_priority
from pretix.helpers.json import CustomJSONEncoder
@@ -131,9 +132,15 @@ class LoggingMixin:
logentry.save()
if logentry.notification_type:
notify.apply_async(args=(logentry.pk,))
notify.apply_async(
args=(logentry.pk,),
priority=get_task_priority("notifications", logentry.organizer_id),
)
if logentry.webhook_type:
notify_webhooks.apply_async(args=(logentry.pk,))
notify_webhooks.apply_async(
args=(logentry.pk,),
priority=get_task_priority("notifications", logentry.organizer_id),
)
return logentry
+7 -1
View File
@@ -847,7 +847,7 @@ class Event(EventMixin, LoggedModel):
from ..signals import event_copy_data
from . import (
Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue,
ItemVariationMetaValue, Question, Quota,
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
)
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
@@ -990,6 +990,12 @@ class Event(EventMixin, LoggedModel):
ia.bundled_variation = variation_map[ia.bundled_variation.pk]
ia.save(force_insert=True)
if not self.has_subevents and not other.has_subevents:
for ipt in ItemProgramTime.objects.filter(item__event=other).prefetch_related('item'):
ipt.pk = None
ipt.item = item_map[ipt.item.pk]
ipt.save(force_insert=True)
quota_map = {}
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
quota_map[q.pk] = q
+28 -2
View File
@@ -142,6 +142,7 @@ class Invoice(models.Model):
invoice_from_name = models.CharField(max_length=190, null=True)
invoice_from_zipcode = models.CharField(max_length=190, null=True)
invoice_from_city = models.CharField(max_length=190, null=True)
invoice_from_state = models.CharField(max_length=190, null=True)
invoice_from_country = FastCountryField(null=True)
invoice_from_tax_id = models.CharField(max_length=190, null=True)
invoice_from_vat_id = models.CharField(max_length=190, null=True)
@@ -218,10 +219,23 @@ class Invoice(models.Model):
taxidrow = "ABN: %s" % self.invoice_from_tax_id
else:
taxidrow = pgettext("invoice", "Tax ID: %s") % self.invoice_from_tax_id
state_name = ""
if self.invoice_from_state:
state_name = self.invoice_from_state
if str(self.invoice_from_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_from_country)][1] == 'long':
try:
state_name = pycountry.subdivisions.get(
code='{}-{}'.format(self.invoice_from_country, self.invoice_from_state)
).name
except:
pass
parts = [
self.invoice_from_name,
self.invoice_from,
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
((self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or "") + " " + (state_name or "")).strip(),
self.invoice_from_country.name if self.invoice_from_country else "",
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
taxidrow,
@@ -230,10 +244,22 @@ class Invoice(models.Model):
@property
def address_invoice_from(self):
state_name = ""
if self.invoice_from_state:
state_name = self.invoice_from_state
if str(self.invoice_from_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_from_country)][1] == 'long':
try:
state_name = pycountry.subdivisions.get(
code='{}-{}'.format(self.invoice_from_country, self.invoice_from_state)
).name
except:
pass
parts = [
self.invoice_from_name,
self.invoice_from,
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
" ".join(s for s in [self.invoice_from_zipcode, self.invoice_from_city, state_name] if s),
self.invoice_from_country.name if self.invoice_from_country else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])
+27 -2
View File
@@ -505,8 +505,7 @@ class Item(LoggedModel):
verbose_name=_("Free price input"),
help_text=_("If this option is active, your users can choose the price themselves. The price configured above "
"is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect "
"additional donations for your event. This is currently not supported for products that are "
"bought as an add-on to other products.")
"additional donations for your event.")
)
free_price_suggestion = models.DecimalField(
verbose_name=_("Suggested price"),
@@ -2294,3 +2293,29 @@ class ItemVariationMetaValue(LoggedModel):
class Meta:
unique_together = ('variation', 'property')
class ItemProgramTime(models.Model):
"""
This model can be used to add a program time to an item.
:param item: The item the program time applies to
:type item: Item
:param start: The date and time this program time starts
:type start: datetime
:param end: The date and time this program time ends
:type end: datetime
"""
item = models.ForeignKey('Item', related_name='program_times', on_delete=models.CASCADE)
start = models.DateTimeField(verbose_name=_("Start"))
end = models.DateTimeField(verbose_name=_("End"))
def clean(self):
if hasattr(self, 'item') and self.item and self.item.event.has_subevents:
raise ValidationError(_("You cannot use program times on an event series."))
self.clean_start_end(start=self.start, end=self.end)
super().clean()
def clean_start_end(self, start: datetime = None, end: datetime = None):
if start and end and start > end:
raise ValidationError(_("The program end must not be before the program start."))
+17 -2
View File
@@ -35,11 +35,14 @@
import json
import logging
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import connections, models
from django.utils.functional import cached_property
from pretix.helpers.celery import get_task_priority
class VisibleOnlyManager(models.Manager):
def get_queryset(self):
@@ -186,7 +189,19 @@ class LogEntry(models.Model):
to_notify = [o.id for o in objects if o.notification_type]
if to_notify:
notify.apply_async(args=(to_notify,))
organizer_ids = set(o.organizer_id for o in objects if o.notification_type)
notify.apply_async(
args=(to_notify,),
priority=settings.PRIORITY_CELERY_HIGHEST_FUNC(
get_task_priority("notifications", oid) for oid in organizer_ids
),
)
to_wh = [o.id for o in objects if o.webhook_type]
if to_wh:
notify_webhooks.apply_async(args=(to_wh,))
organizer_ids = set(o.organizer_id for o in objects if o.webhook_type)
notify_webhooks.apply_async(
args=(to_wh,),
priority=settings.PRIORITY_CELERY_HIGHEST_FUNC(
get_task_priority("notifications", oid) for oid in organizer_ids
),
)
+2 -2
View File
@@ -280,13 +280,13 @@ class Seat(models.Model):
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None,
sales_channel='web',
ignore_distancing=False, distance_ignore_cart_id=None):
ignore_distancing=False, distance_ignore_cart_id=None, always_allow_blocked=False):
from .orders import Order
from .organizer import SalesChannel
if isinstance(sales_channel, SalesChannel):
sales_channel = sales_channel.identifier
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
if not always_allow_blocked and self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
return False
opqs = self.orderposition_set.filter(
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID],
+2 -2
View File
@@ -623,7 +623,7 @@ class Voucher(LoggedModel):
return max(1, self.min_usages - self.redeemed)
@classmethod
def annotate_budget_used_orders(cls, qs):
def annotate_budget_used(cls, qs):
opq = OrderPosition.objects.filter(
voucher_id=OuterRef('pk'),
voucher_budget_use__isnull=False,
@@ -632,7 +632,7 @@ class Voucher(LoggedModel):
Order.STATUS_PENDING
]
).order_by().values('voucher_id').annotate(s=Sum('voucher_budget_use')).values('s')
return qs.annotate(budget_used_orders=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=13, decimal_places=2)), Decimal('0.00')))
return qs.annotate(budget_used=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=13, decimal_places=2)), Decimal('0.00')))
def budget_used(self):
ops = OrderPosition.objects.filter(
+17
View File
@@ -84,6 +84,7 @@ 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.daterange import datetimerange
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
from pretix.presale.style import get_fonts
@@ -490,6 +491,12 @@ DEFAULT_VARIABLES = OrderedDict((
"TIME_FORMAT"
) if op.valid_until else ""
}),
("program_times", {
"label": _("Program times: date and time"),
"editor_sample": _(
"2017-05-31 10:00 12:00\n2017-05-31 14:00 16:00\n2017-05-31 14:00 2017-06-01 14:00"),
"evaluate": lambda op, order, ev: get_program_times(op, ev)
}),
("medium_identifier", {
"label": _("Reusable Medium ID"),
"editor_sample": "ABC1234DEF4567",
@@ -734,6 +741,16 @@ def get_seat(op: OrderPosition):
return None
def get_program_times(op: OrderPosition, ev: Event):
return '\n'.join([
datetimerange(
pt.start.astimezone(ev.timezone),
pt.end.astimezone(ev.timezone),
as_html=False
) for pt in op.item.program_times.all()
])
def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int)
addons = [p for p in (
+5
View File
@@ -1361,6 +1361,11 @@ class CartManager:
deleted_positions.add(op.position.pk)
addons.delete()
op.position.delete()
if op.position.is_bundled:
deleted_positions |= {a.pk for a in op.position.addon_to.addons.all()}
deleted_positions.add(op.position.addon_to.pk)
op.position.addon_to.addons.all().delete()
op.position.addon_to.delete()
else:
raise AssertionError("ExtendOperation cannot affect more than one item")
elif isinstance(op, self.VoucherOperation):
+4 -1
View File
@@ -93,6 +93,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
invoice.invoice_from_city = invoice.event.settings.get('invoice_address_from_city')
invoice.invoice_from_state = invoice.event.settings.get('invoice_address_from_state')
invoice.invoice_from_country = invoice.event.settings.get('invoice_address_from_country')
invoice.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id')
invoice.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id')
@@ -459,6 +460,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
cancellation.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
cancellation.invoice_from_city = invoice.event.settings.get('invoice_address_from_city')
cancellation.invoice_from_state = invoice.event.settings.get('invoice_address_from_state')
cancellation.invoice_from_country = invoice.event.settings.get('invoice_address_from_country')
cancellation.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id')
cancellation.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id')
@@ -562,6 +564,7 @@ def build_preview_invoice_pdf(event):
invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
invoice.invoice_from_city = invoice.event.settings.get('invoice_address_from_city')
invoice.invoice_from_state = invoice.event.settings.get('invoice_address_from_state')
invoice.invoice_from_country = invoice.event.settings.get('invoice_address_from_country')
invoice.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id')
invoice.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id')
@@ -693,7 +696,7 @@ def retry_stuck_invoices(sender, **kwargs):
with transaction.atomic():
qs = Invoice.objects.filter(
transmission_status=Invoice.TRANSMISSION_STATUS_INFLIGHT,
transmission_date__lte=now() - timedelta(hours=24),
transmission_date__lte=now() - timedelta(hours=48),
).select_for_update(
of=OF_SELF, skip_locked=connection.features.has_select_for_update_skip_locked
)
+5 -4
View File
@@ -222,7 +222,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
'invoice_company': ''
})
renderer = ClassicMailRenderer(None, organizer)
content_plain = body_plain = render_mail(template, context)
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
subject = str(subject).format_map(TolerantDict(context))
sender = (
sender or
@@ -316,6 +316,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
with override(timezone):
try:
content_plain = render_mail(template, context, placeholder_mode=None)
if plain_text_only:
body_html = None
elif 'context' in inspect.signature(renderer.render).parameters:
@@ -751,11 +752,11 @@ def mail_send(*args, **kwargs):
mail_send_task.apply_async(args=args, kwargs=kwargs)
def render_mail(template, context):
def render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN):
if isinstance(template, LazyI18nString):
body = str(template)
if context:
body = format_map(body, context, mode=SafeFormatter.MODE_IGNORE_RICH)
if context and placeholder_mode:
body = format_map(body, context, mode=placeholder_mode)
else:
tpl = get_template(template)
body = tpl.render(context)
+9 -2
View File
@@ -32,6 +32,7 @@ from pretix.base.services.mail import mail_send_task
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import notification
from pretix.celery_app import app
from pretix.helpers.celery import get_task_priority
from pretix.helpers.urls import build_absolute_uri
@@ -88,12 +89,18 @@ def notify(logentry_ids: list):
for um, enabled in notify_specific.items():
user, method = um
if enabled:
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
send_notification.apply_async(
args=(logentry.id, notification_type.action_type, user.pk, method),
priority=get_task_priority("notifications", logentry.organizer_id),
)
for um, enabled in notify_global.items():
user, method = um
if enabled and um not in notify_specific:
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
send_notification.apply_async(
args=(logentry.id, notification_type.action_type, user.pk, method),
priority=get_task_priority("notifications", logentry.organizer_id),
)
notification.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)
+12 -2
View File
@@ -146,6 +146,10 @@ error_messages = {
'race_condition': gettext_lazy("This order was changed by someone else simultaneously. Please check if your "
"changes are still accurate and try again."),
'empty': gettext_lazy("Your cart is empty."),
'max_items': ngettext_lazy(
"You cannot select more than %s item per order.",
"You cannot select more than %s items per order."
),
'max_items_per_product': ngettext_lazy(
"You cannot select more than %(max)s item of the product %(product)s. We removed the surplus items from your cart.",
"You cannot select more than %(max)s items of the product %(product)s. We removed the surplus items from your cart.",
@@ -763,6 +767,11 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
shared_lock_objects=[event]
)
# Check maximum order size
limit = min(int(event.settings.max_items_per_order), settings.PRETIX_MAX_ORDER_SIZE)
if sum(1 for cp in sorted_positions if not cp.addon_to) > limit:
err = err or (error_messages['max_items'] % limit)
# Check availability
for i, cp in enumerate(sorted_positions):
if cp.pk in deleted_positions:
@@ -1670,13 +1679,14 @@ class OrderChangeManager:
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True):
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
self.order = order
self.user = user
self.auth = auth
self.event = order.event
self.split_order = None
self.reissue_invoice = reissue_invoice
self.allow_blocked_seats = allow_blocked_seats
self._committed = False
self._totaldiff_guesstimate = 0
self._quotadiff = Counter()
@@ -2197,7 +2207,7 @@ class OrderChangeManager:
for seat, diff in self._seatdiff.items():
if diff <= 0:
continue
if not seat.is_available(sales_channel=self.order.sales_channel, ignore_distancing=True) or diff > 1:
if not seat.is_available(sales_channel=self.order.sales_channel, ignore_distancing=True, always_allow_blocked=self.allow_blocked_seats) or diff > 1:
raise OrderError(self.error_messages['seat_unavailable'].format(seat=seat.name))
if self.event.has_subevents:
+52 -18
View File
@@ -26,7 +26,7 @@ from decimal import Decimal
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.html import escape, mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -123,6 +123,10 @@ class BaseRichTextPlaceholder(BaseTextPlaceholder):
def identifier(self):
return self._identifier
@property
def allowed_in_plain_content(self):
return False
@property
def required_context(self):
return self._args
@@ -194,6 +198,33 @@ class SimpleButtonPlaceholder(BaseRichTextPlaceholder):
return f'{text}: {url}'
class MarkdownTextPlaceholder(BaseRichTextPlaceholder):
def __init__(self, identifier, args, func, sample, inline):
super().__init__(identifier, args)
self._func = func
self._sample = sample
self._snippet = inline
@property
def allowed_in_plain_content(self):
return self._snippet
def render_plain(self, **context):
return self._func(**{k: context[k] for k in self._args})
def render_html(self, **context):
return mark_safe(markdown_compile_email(self.render_plain(**context), snippet=self._snippet))
def render_sample_plain(self, event):
if callable(self._sample):
return self._sample(event)
else:
return self._sample
def render_sample_html(self, event):
return mark_safe(markdown_compile_email(self.render_sample_plain(event), snippet=self._snippet))
class PlaceholderContext(SafeFormatter):
"""
Holds the contextual arguments and corresponding list of available placeholders for formatting
@@ -574,7 +605,7 @@ def base_placeholders(sender, **kwargs):
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
_('Sample Corporation')
),
SimpleFunctionalTextPlaceholder(
MarkdownTextPlaceholder(
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
@@ -604,6 +635,7 @@ def base_placeholders(sender, **kwargs):
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
]
),
inline=False,
),
SimpleFunctionalTextPlaceholder(
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
@@ -618,12 +650,13 @@ def base_placeholders(sender, **kwargs):
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalTextPlaceholder(
MarkdownTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
'68CYU2H6ZTP3WLK5 \n7MB94KKPVEPSMVF2',
inline=False,
),
SimpleFunctionalTextPlaceholder(
MarkdownTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
@@ -638,6 +671,7 @@ def base_placeholders(sender, **kwargs):
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
]),
inline=False,
),
SimpleFunctionalTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
@@ -656,13 +690,13 @@ def base_placeholders(sender, **kwargs):
'comment', ['comment'], lambda comment: comment,
_('An individual text with a reason can be inserted here.'),
),
SimpleFunctionalTextPlaceholder(
MarkdownTextPlaceholder(
'payment_info', ['order', 'payments'], _placeholder_payments,
_('The amount has been charged to your card.'),
_('The amount has been charged to your card.'), inline=False,
),
SimpleFunctionalTextPlaceholder(
MarkdownTextPlaceholder(
'payment_info', ['payment_info'], lambda payment_info: payment_info,
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
_('Please transfer money to this bank account: 9999-9999-9999-9999'), inline=False,
),
SimpleFunctionalTextPlaceholder(
'attendee_name', ['position'], lambda position: position.attendee_name,
@@ -719,13 +753,13 @@ def base_placeholders(sender, **kwargs):
))
for k, v in sender.meta_data.items():
ph.append(SimpleFunctionalTextPlaceholder(
ph.append(MarkdownTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
v, inline=True,
))
ph.append(SimpleFunctionalTextPlaceholder(
ph.append(MarkdownTextPlaceholder(
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
v
v, inline=True,
))
return ph
@@ -753,7 +787,7 @@ def get_available_placeholders(event, base_parameters, rich=False):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if isinstance(v, BaseRichTextPlaceholder) and not rich:
if isinstance(v, BaseRichTextPlaceholder) and not rich and not v.allowed_in_plain_content:
continue
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
@@ -775,13 +809,13 @@ def get_sample_context(event, context_parameters, rich=True):
)
)
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
context_dict[k] = '<div class="placeholder" title="{}">{}</div>'.format(
context_dict[k] = mark_safe('<div class="placeholder" title="{}">{}</div>'.format(
lbl,
markdown_compile_email(str(sample))
)
))
else:
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
context_dict[k] = mark_safe('<span class="placeholder" title="{}">{}</span>'.format(
lbl,
escape(sample)
)
))
return context_dict
+1 -1
View File
@@ -231,7 +231,7 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_keep
"""
def _key(line):
return (line.tax_rate, line.tax_code)
return (line.tax_rate, line.tax_code or "")
places = settings.CURRENCY_PLACES.get(currency, 2)
minimum_unit = Decimal('1') / 10 ** places
+186 -13
View File
@@ -27,7 +27,6 @@ from decimal import Decimal
from xml.etree import ElementTree
import requests
import vat_moss.id
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from zeep import Client, Transport
@@ -42,14 +41,142 @@ logger = logging.getLogger(__name__)
error_messages = {
'unavailable': _(
'Your VAT ID could not be checked, as the VAT checking service of '
'your country is currently not available. We will therefore '
'need to charge VAT on your invoice. You can get the tax amount '
'back via the VAT reimbursement process.'
'your country is currently not available. We will therefore need to '
'charge you the same tax rate as if you did not enter a VAT ID.'
),
'invalid': _('This VAT ID is not valid. Please re-check your input.'),
'country_mismatch': _('Your VAT ID does not match the selected country.'),
}
VAT_ID_PATTERNS = {
# Patterns generated by consulting the following URLs:
#
# - http://en.wikipedia.org/wiki/VAT_identification_number
# - http://ec.europa.eu/taxation_customs/vies/faq.html
# - https://euipo.europa.eu/tunnel-web/secure/webdav/guest/document_library/Documents/COSME/VAT%20numbers%20EU.pdf
# - http://www.skatteetaten.no/en/International-pages/Felles-innhold-benyttes-i-flere-malgrupper/Brochure/Guide-to-value-added-tax-in-Norway/?chapter=7159
'AT': { # Austria
'regex': '^U\\d{8}$',
'country_code': 'AT'
},
'BE': { # Belgium
'regex': '^(1|0?)\\d{9}$',
'country_code': 'BE'
},
'BG': { # Bulgaria
'regex': '^\\d{9,10}$',
'country_code': 'BG'
},
'CH': { # Switzerland
'regex': '^\\dE{9}$',
'country_code': 'CH'
},
'CY': { # Cyprus
'regex': '^\\d{8}[A-Z]$',
'country_code': 'CY'
},
'CZ': { # Czech Republic
'regex': '^\\d{8,10}$',
'country_code': 'CZ'
},
'DE': { # Germany
'regex': '^\\d{9}$',
'country_code': 'DE'
},
'DK': { # Denmark
'regex': '^\\d{8}$',
'country_code': 'DK'
},
'EE': { # Estonia
'regex': '^\\d{9}$',
'country_code': 'EE'
},
'EL': { # Greece
'regex': '^\\d{9}$',
'country_code': 'GR'
},
'ES': { # Spain
'regex': '^[A-Z0-9]\\d{7}[A-Z0-9]$',
'country_code': 'ES'
},
'FI': { # Finland
'regex': '^\\d{8}$',
'country_code': 'FI'
},
'FR': { # France
'regex': '^[A-Z0-9]{2}\\d{9}$',
'country_code': 'FR'
},
'GB': { # United Kingdom
'regex': '^(GD\\d{3}|HA\\d{3}|\\d{9}|\\d{12})$',
'country_code': 'GB'
},
'HR': { # Croatia
'regex': '^\\d{11}$',
'country_code': 'HR'
},
'HU': { # Hungary
'regex': '^\\d{8}$',
'country_code': 'HU'
},
'IE': { # Ireland
'regex': '^(\\d{7}[A-Z]{1,2}|\\d[A-Z+*]\\d{5}[A-Z])$',
'country_code': 'IE'
},
'IT': { # Italy
'regex': '^\\d{11}$',
'country_code': 'IT'
},
'LT': { # Lithuania
'regex': '^(\\d{9}|\\d{12})$',
'country_code': 'LT'
},
'LU': { # Luxembourg
'regex': '^\\d{8}$',
'country_code': 'LU'
},
'LV': { # Latvia
'regex': '^\\d{11}$',
'country_code': 'LV'
},
'MT': { # Malta
'regex': '^\\d{8}$',
'country_code': 'MT'
},
'NL': { # Netherlands
'regex': '^\\d{9}B\\d{2}$',
'country_code': 'NL'
},
'NO': { # Norway
'regex': '^\\d{9}MVA$',
'country_code': 'NO'
},
'PL': { # Poland
'regex': '^\\d{10}$',
'country_code': 'PL'
},
'PT': { # Portugal
'regex': '^\\d{9}$',
'country_code': 'PT'
},
'RO': { # Romania
'regex': '^\\d{2,10}$',
'country_code': 'RO'
},
'SE': { # Sweden
'regex': '^\\d{12}$',
'country_code': 'SE'
},
'SI': { # Slovenia
'regex': '^\\d{8}$',
'country_code': 'SI'
},
'SK': { # Slovakia
'regex': '^\\d{10}$',
'country_code': 'SK'
},
}
class VATIDError(Exception):
def __init__(self, message):
@@ -64,13 +191,57 @@ class VATIDTemporaryError(VATIDError):
pass
def normalize_vat_id(vat_id, country_code):
"""
Accepts a VAT ID and normaizes it, getting rid of spaces, periods, dashes
etc and converting it to upper case.
Original function from https://github.com/wbond/vat_moss-python
Copyright (c) 2015 Will Bond <will@wbond.net>
MIT License
"""
if not vat_id:
return None
if not isinstance(vat_id, str):
raise TypeError('VAT ID is not a string')
if len(vat_id) < 3:
raise ValueError('VAT ID must be at least three character long')
# Normalize the ID for simpler regexes
vat_id = re.sub('\\s+', '', vat_id)
vat_id = vat_id.replace('-', '')
vat_id = vat_id.replace('.', '')
vat_id = vat_id.upper()
# Clean the different shapes a number can take in Switzerland depending on purpse
if country_code == "CH":
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
# Fix people using GR prefix for Greece
if vat_id[0:2] == "GR" and country_code == "GR":
vat_id = "EL" + vat_id[2:]
# Check if we already have a valid country prefix. If not, we try to figure out if we can
# add one, since in some countries (e.g. Italy) it's very custom to enter it without the prefix
if vat_id[:2] in VAT_ID_PATTERNS and re.match(VAT_ID_PATTERNS[vat_id[0:2]]['regex'], vat_id[2:]):
# Prefix set and prefix matches pattern, nothing to do
pass
elif re.match(VAT_ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], vat_id):
# Prefix not set but adding it fixes pattern
vat_id = cc_to_vat_prefix(country_code) + vat_id
else:
# We have no idea what this is
pass
return vat_id
def _validate_vat_id_NO(vat_id, country_code):
# Inspired by vat_moss library
if not vat_id.startswith("NO"):
# prefix is not usually used in Norway, but expected by vat_moss library
vat_id = "NO" + vat_id
try:
vat_id = vat_moss.id.normalize(vat_id)
vat_id = normalize_vat_id(vat_id, country_code)
except ValueError:
raise VATIDFinalError(error_messages['invalid'])
@@ -104,7 +275,7 @@ def _validate_vat_id_NO(vat_id, country_code):
def _validate_vat_id_EU(vat_id, country_code):
# Inspired by vat_moss library
try:
vat_id = vat_moss.id.normalize(vat_id)
vat_id = normalize_vat_id(vat_id, country_code)
except ValueError:
raise VATIDFinalError(error_messages['invalid'])
@@ -112,11 +283,10 @@ def _validate_vat_id_EU(vat_id, country_code):
raise VATIDFinalError(error_messages['invalid'])
number = vat_id[2:]
if vat_id[:2] != cc_to_vat_prefix(country_code):
raise VATIDFinalError(error_messages['country_mismatch'])
if not re.match(vat_moss.id.ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], number):
if not re.match(VAT_ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], number):
raise VATIDFinalError(error_messages['invalid'])
# We are relying on the country code of the normalized VAT-ID and not the user/InvoiceAddress-provided
@@ -175,9 +345,12 @@ def _validate_vat_id_EU(vat_id, country_code):
def _validate_vat_id_CH(vat_id, country_code):
if vat_id[:3] != 'CHE':
raise VATIDFinalError(_('Your VAT ID does not match the selected country.'))
raise VATIDFinalError(error_messages['country_mismatch'])
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
try:
vat_id = normalize_vat_id(vat_id, country_code)
except ValueError:
raise VATIDFinalError(error_messages['invalid'])
try:
transport = Transport(
cache=SqliteCache(os.path.join(settings.CACHE_DIR, "validate_vat_id_ch_zeep_cache.db")),
+5
View File
@@ -113,6 +113,11 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
lock_objects(quotas, shared_lock_objects=[event])
for wle in qs:
# add this event to wle.item as it is not yet cached and is needed in check_quotas
wle.item.event = event
if wle.variation:
wle.variation.item = wle.item
if (wle.item_id, wle.variation_id, wle.subevent_id) in gone:
continue
ev = (wle.subevent or event)
+75 -6
View File
@@ -40,6 +40,7 @@ from datetime import datetime
from decimal import Decimal
from typing import Any
import pycountry
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -628,13 +629,40 @@ DEFAULTS = {
'form_kwargs': dict(
label=_("Ask for VAT ID"),
help_text=format_lazy(
_("Only works if an invoice address is asked for. VAT ID is never required and only requested from "
"business customers in the following countries: {countries}"),
_("Only works if an invoice address is asked for. VAT ID is only requested from business customers "
"in the following countries: {countries}."),
countries=lazy(lambda *args: ', '.join(sorted(gettext(Country(cc).name) for cc in VAT_ID_COUNTRIES)), str)()
),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
)
},
'invoice_address_vatid_required_countries': {
'default': ['IT', 'GR'],
'type': list,
'form_class': forms.MultipleChoiceField,
'serializer_class': serializers.MultipleChoiceField,
'serializer_kwargs': dict(
choices=lazy(
lambda *args: sorted([(cc, gettext(Country(cc).name)) for cc in VAT_ID_COUNTRIES], key=lambda c: c[1]),
list
)(),
),
'form_kwargs': dict(
label=_("Require VAT ID in"),
choices=lazy(
lambda *args: sorted([(cc, gettext(Country(cc).name)) for cc in VAT_ID_COUNTRIES], key=lambda c: c[1]),
list
)(),
help_text=format_lazy(
_("VAT ID is optional by default, because not all businesses are assigned a VAT ID in all countries. "
"VAT ID will be required for all business addresses in the selected countries."),
),
widget=forms.CheckboxSelectMultiple(attrs={
"class": "scrolling-multiple-choice",
'data-display-dependency': '#id_invoice_address_vatid'
}),
)
},
'invoice_address_explanation_text': {
'default': '',
'type': LazyI18nString,
@@ -689,6 +717,7 @@ DEFAULTS = {
label=_("Minimum length of invoice number after prefix"),
help_text=_("The part of your invoice number after your prefix will be filled up with leading zeros up to this length, e.g. INV-001 or INV-00001."),
max_value=12,
min_value=1,
required=True,
)
},
@@ -724,8 +753,9 @@ DEFAULTS = {
message=lazy(lambda *args: _('Please only use the characters {allowed} in this field.').format(
allowed='A-Z, a-z, 0-9, -./:#'
), str)()
)
),
],
max_length=155,
)
},
'invoice_numbers_prefix_cancellations': {
@@ -746,8 +776,9 @@ DEFAULTS = {
message=lazy(lambda *args: _('Please only use the characters {allowed} in this field.').format(
allowed='A-Z, a-z, 0-9, -./:#'
), str)()
)
),
],
max_length=155,
)
},
'invoice_renderer_highlight_order_code': {
@@ -1202,6 +1233,7 @@ DEFAULTS = {
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
'form_kwargs': dict(
max_length=190,
label=_("Company name"),
)
},
@@ -1215,6 +1247,7 @@ DEFAULTS = {
'placeholder': '12345'
}),
label=_("ZIP code"),
max_length=190,
)
},
'invoice_address_from_city': {
@@ -1227,15 +1260,35 @@ DEFAULTS = {
'placeholder': _('Random City')
}),
label=_("City"),
max_length=190,
)
},
'invoice_address_from_state': {
'default': '',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': {
'choices': [('', '')],
},
'form_kwargs': {
"label": pgettext_lazy('address', 'State'),
'choices': [('', '')],
},
},
'invoice_address_from_country': {
'default': '',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': lambda: dict(**country_choice_kwargs()),
'form_kwargs': lambda: dict(label=_('Country'), **country_choice_kwargs()),
'form_kwargs': lambda: dict(
label=_('Country'),
widget=forms.Select(attrs={
'data-trigger-address-info': 'on',
}),
**country_choice_kwargs()
),
},
'invoice_address_from_tax_id': {
'default': '',
@@ -1244,7 +1297,8 @@ DEFAULTS = {
'serializer_class': serializers.CharField,
'form_kwargs': dict(
label=_("Domestic tax ID"),
help_text=_("e.g. tax number in Germany, ABN in Australia, …")
help_text=_("e.g. tax number in Germany, ABN in Australia, …"),
max_length=190,
)
},
'invoice_address_from_vat_id': {
@@ -1254,6 +1308,7 @@ DEFAULTS = {
'serializer_class': serializers.CharField,
'form_kwargs': dict(
label=_("EU VAT ID"),
max_length=190,
)
},
'invoice_introductory_text': {
@@ -3971,6 +4026,20 @@ def validate_event_settings(event, settings_dict):
raise ValidationError({
'invoice_address_company_required': _('You have to require invoice addresses to require for company names.')
})
if settings_dict.get('invoice_address_from_state') and settings_dict.get('invoice_address_from_country'):
cc = str(settings_dict.get('invoice_address_from_country'))
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(
{'invoice_address_from_state': ['States are not supported in country "{}".'.format(cc)]}
)
if not pycountry.subdivisions.get(code=cc + '-' + settings_dict.get('invoice_address_from_state')):
raise ValidationError(
{'invoice_address_from_state': [
'"{}" is not a known subdivision of the country "{}".'.format(
settings_dict.get('invoice_address_from_state'), cc
)
]}
)
payment_term_last = settings_dict.get('payment_term_last')
if payment_term_last and event.presale_end:
+34 -14
View File
@@ -44,6 +44,7 @@ from django.conf import settings
from django.core import signing
from django.urls import reverse
from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
from markdown import Extension
@@ -52,6 +53,8 @@ from markdown.postprocessors import Postprocessor
from markdown.treeprocessors import UnescapeTreeprocessor
from tlds import tld_set
from pretix.helpers.format import SafeFormatter, format_map
register = template.Library()
@@ -321,27 +324,44 @@ class LinkifyAndCleanExtension(Extension):
)
def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes=ALLOWED_ATTRIBUTES):
def markdown_compile_email(source, allowed_tags=None, allowed_attributes=ALLOWED_ATTRIBUTES, snippet=False, context=None):
if allowed_tags is None:
allowed_tags = ALLOWED_TAGS_SNIPPET if snippet else ALLOWED_TAGS
context_callbacks = []
if context:
# This is a workaround to fix placeholders in URL targets
def context_callback(attrs, new=False):
if (None, "href") in attrs and "{" in attrs[None, "href"]:
# Do not use MODE_RICH_TO_HTML to avoid recursive linkification
attrs[None, "href"] = escape(format_map(attrs[None, "href"], context=context, mode=SafeFormatter.MODE_RICH_TO_PLAIN))
return attrs
context_callbacks.append(context_callback)
linker = bleach.Linker(
url_re=URL_RE,
email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
callbacks=context_callbacks + DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True
)
exts = [
'markdown.extensions.sane_lists',
'markdown.extensions.tables',
EmailNl2BrExtension(),
LinkifyAndCleanExtension(
linker,
tags=set(allowed_tags),
attributes=allowed_attributes,
protocols=ALLOWED_PROTOCOLS,
strip=snippet,
)
]
if snippet:
exts.append(SnippetExtension())
return markdown.markdown(
source,
extensions=[
'markdown.extensions.sane_lists',
'markdown.extensions.tables',
EmailNl2BrExtension(),
LinkifyAndCleanExtension(
linker,
tags=set(allowed_tags),
attributes=allowed_attributes,
protocols=ALLOWED_PROTOCOLS,
strip=False,
)
]
extensions=exts
)
+29 -2
View File
@@ -22,7 +22,7 @@
import pycountry
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import pgettext
from django.utils.translation import gettext, pgettext, pgettext_lazy
from django_countries.fields import Country
from django_scopes import scope
@@ -36,6 +36,22 @@ from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
)
VAT_ID_LABELS = {
# VAT ID is a EU concept and Switzerland has a distinct, but differently-named concept
"CH": pgettext_lazy("tax_id_swiss", "UID"), # Translators: Only translate to French (IDE) and Italien (IDI), otherwise keep the same
# Awareness around VAT IDs differes by EU country. For example, in Germany the VAT ID is assigned
# separately to each company and only used in cross-country transactions. Therefore, it makes sense
# to call it just "VAT ID" on the form, and people will either know their VAT ID or they don't.
# In contrast, in Italy the EU-compatible VAT ID is not separately assigned, but is just "IT" + the national tax
# number (Partita IVA) and also used on domestic transactions. So someone who never purchased something international
# for their company, might still know the value, if we call it the right way and not just "VAT ID".
"IT": pgettext_lazy("tax_id_italy", "VAT ID / P.IVA"), # Translators: Translate to only "P.IVA" in Italian, keep second part as-is in other languages
"GR": pgettext_lazy("tax_id_greece", "VAT ID / TIN"), # Translators: Translate to only "ΑΦΜ" in Greek
"ES": pgettext_lazy("tax_id_spain", "VAT ID / NIF"), # Translators: Translate to only "NIF" in Spanish
"PT": pgettext_lazy("tax_id_portugal", "VAT ID / NIF"), # Translators: Translate to only "NIF" in Portuguese
}
def _info(cc):
info = {
@@ -47,7 +63,12 @@ def _info(cc):
'required': 'if_any' if cc in COUNTRIES_WITH_STATE_IN_ADDRESS else False,
'label': COUNTRY_STATE_LABEL.get(cc, pgettext('address', 'State')),
},
'vat_id': {'visible': cc in VAT_ID_COUNTRIES, 'required': False},
'vat_id': {
'visible': cc in VAT_ID_COUNTRIES,
'required': False,
'label': VAT_ID_LABELS.get(cc, gettext("VAT ID")),
'helptext_visible': True,
},
}
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return {'data': [], **info}
@@ -124,4 +145,10 @@ def address_form(request):
"required": transmission_type.identifier == selected_transmission_type and k in required
}
if is_business and country in event.settings.invoice_address_vatid_required_countries and info["vat_id"]["visible"]:
info["vat_id"]["required"] = True
if info["vat_id"]["required"]:
# The help text explains that it is optional, so we want to hide that if it is required
info["vat_id"]["helptext_visible"] = False
return JsonResponse(info)
+37 -6
View File
@@ -42,7 +42,6 @@ import pycountry
from django import forms
from django.conf import settings
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.core.validators import MaxValueValidator
from django.db.models import Prefetch, Q, prefetch_related_objects
from django.forms import formset_factory, inlineformset_factory
from django.urls import reverse
@@ -67,8 +66,9 @@ from pretix.base.models.tax import TAX_CODE_LISTS
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, DEFAULTS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS, ROUNDING_MODES, validate_event_settings,
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL, DEFAULTS,
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, ROUNDING_MODES,
validate_event_settings,
)
from pretix.base.validators import multimail_validate
from pretix.control.forms import (
@@ -373,6 +373,13 @@ class EventUpdateForm(I18nModelForm):
super().__init__(*args, **kwargs)
if not self.change_slug:
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
if self.instance.orders.exists():
self.fields['currency'].disabled = True
self.fields['currency'].help_text = _(
'The currency cannot be changed because orders already exist.'
)
self.fields['location'].widget.attrs['rows'] = '3'
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany'
@@ -920,6 +927,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_vatid_required_countries',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
@@ -945,6 +953,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
'invoice_address_from',
'invoice_address_from_zipcode',
'invoice_address_from_city',
'invoice_address_from_state',
'invoice_address_from_country',
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
@@ -991,8 +1000,6 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
self.fields['invoice_generate_sales_channels'].choices = (
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
)
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
pps = [str(pp.verbose_name) for pp in event.get_payment_providers().values() if pp.requires_invoice_immediately]
if pps:
generate_paid_help_text = _('An invoice will be issued before payment if the customer selects one of the following payment methods: {list}').format(
@@ -1017,6 +1024,26 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
(a, a) for a in get_fonts(event, pdf_support_required=True).keys()
]
if 'invoice_address_from_country' in self.data:
cc = str(self.data['invoice_address_from_country'])
elif 'invoice_address_from_country' in self.initial:
cc = str(self.initial['invoice_address_from_country'])
else:
cc = self.obj.settings.invoice_address_from_country
c = [('', '---')]
state_label = pgettext_lazy('address', 'State')
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
if cc in COUNTRY_STATE_LABEL:
state_label = COUNTRY_STATE_LABEL[cc]
elif 'invoice_address_from_state' in self.data:
self.data = self.data.copy()
del self.data['invoice_address_from_state']
self.fields['invoice_address_from_state'].choices = c
self.fields['invoice_address_from_state'].label = state_label
def contains_web_channel_validate(val):
if "web" not in val:
@@ -1838,7 +1865,11 @@ class QuickSetupForm(I18nForm):
self.fields['payment_banktransfer_bank_details'].required = False
for f in self.fields.values():
if 'data-required-if' in f.widget.attrs:
del f.widget.attrs['data-required-if']
f.widget.attrs['data-required-if'] += ",#id_payment_banktransfer__enabled"
self.fields['payment_banktransfer_bank_details'].widget.attrs["data-required-if"] = (
"#id_payment_banktransfer_bank_details_type_1,#id_payment_banktransfer__enabled"
)
def clean(self):
cleaned_data = super().clean()
+50 -1
View File
@@ -56,7 +56,8 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
Item, ItemCategory, ItemProgramTime, ItemVariation, Question,
QuestionOption, Quota,
)
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.signals import item_copy_data
@@ -572,6 +573,8 @@ class ItemCreateForm(I18nModelForm):
for b in self.cleaned_data['copy_from'].bundles.all():
instance.bundles.create(bundled_item=b.bundled_item, bundled_variation=b.bundled_variation,
count=b.count, designated_price=b.designated_price)
for pt in self.cleaned_data['copy_from'].program_times.all():
instance.program_times.create(start=pt.start, end=pt.end)
item_copy_data.send(sender=self.event, source=self.cleaned_data['copy_from'], target=instance)
@@ -1321,3 +1324,49 @@ class ItemMetaValueForm(forms.ModelForm):
widgets = {
'value': forms.TextInput()
}
class ItemProgramTimeFormSet(I18nFormSet):
template = "pretixcontrol/item/include_program_times.html"
title = _('Program times')
def _construct_form(self, i, **kwargs):
kwargs['event'] = self.event
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
self.is_valid()
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
use_required_attribute=False,
locales=self.locales,
event=self.event
)
self.add_fields(form, None)
return form
class ItemProgramTimeForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['end'].widget.attrs['data-date-after'] = '#id_{prefix}-start_0'.format(prefix=self.prefix)
class Meta:
model = ItemProgramTime
localized_fields = '__all__'
fields = [
'start',
'end',
]
field_classes = {
'start': forms.SplitDateTimeField,
'end': forms.SplitDateTimeField,
}
widgets = {
'start': SplitDateTimePickerWidget(),
'end': SplitDateTimePickerWidget(),
}
+2 -2
View File
@@ -974,7 +974,7 @@ class EventCancelForm(FormPlaceholderMixin, forms.Form):
self._set_field_placeholders('send_subject', ['event_or_subevent', 'refund_amount', 'position_or_address',
'order', 'event'])
self._set_field_placeholders('send_message', ['event_or_subevent', 'refund_amount', 'position_or_address',
'order', 'event'])
'order', 'event'], rich=True)
self.fields['send_waitinglist_subject'] = I18nFormField(
label=_("Subject"),
required=True,
@@ -998,7 +998,7 @@ class EventCancelForm(FormPlaceholderMixin, forms.Form):
))
)
self._set_field_placeholders('send_waitinglist_subject', ['event_or_subevent', 'event'])
self._set_field_placeholders('send_waitinglist_message', ['event_or_subevent', 'event'])
self._set_field_placeholders('send_waitinglist_message', ['event_or_subevent', 'event'], rich=True)
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
+1
View File
@@ -69,6 +69,7 @@ class UserEditForm(forms.ModelForm):
'email',
'require_2fa',
'is_active',
'is_verified',
'is_staff',
'needs_password_change',
'last_login'
+3 -3
View File
@@ -308,8 +308,8 @@ class VoucherBulkForm(VoucherForm):
)
Recipient = namedtuple('Recipient', 'email number name tag')
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.instance.event, base_parameters)
def _set_field_placeholders(self, fn, base_parameters, rich=False):
placeholders = get_available_placeholders(self.instance.event, base_parameters, rich=rich)
ht = format_placeholders_help_text(placeholders, self.instance.event)
if self.fields[fn].help_text:
@@ -345,7 +345,7 @@ class VoucherBulkForm(VoucherForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._set_field_placeholders('send_subject', ['event', 'name'])
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'])
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'], rich=True)
with language(self.instance.event.settings.locale, self.instance.event.settings.region):
for f in ("send_subject", "send_message"):
+12
View File
@@ -582,6 +582,7 @@ class CoreOrderLogEntryType(OrderLogEntryType):
'The voucher has been set to expire because the recipient removed themselves from the waiting list.'),
'pretix.voucher.changed': _('The voucher has been changed.'),
'pretix.voucher.deleted': _('The voucher has been deleted.'),
'pretix.voucher.carts.deleted': _('Cart positions including the voucher have been deleted.'),
'pretix.voucher.added.waitinglist': _('The voucher has been assigned to {email} through the waiting list.'),
})
class CoreVoucherLogEntryType(VoucherLogEntryType):
@@ -667,6 +668,14 @@ class UserSettingsChangedLogEntryType(LogEntryType):
return text
@log_entry_types.new_from_dict({
'pretix.user.email.changed': _('Your email address has been changed from {old_email} to {email}.'),
'pretix.user.email.confirmed': _('Your email address {email} has been confirmed.'),
})
class UserEmailChangedLogEntryType(LogEntryType):
pass
class UserImpersonatedLogEntryType(LogEntryType):
def display(self, logentry, data):
return self.plain.format(data['other_email'])
@@ -882,6 +891,9 @@ class EventPluginStateLogEntryType(EventLogEntryType):
'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.program_times.added': _('A program time has been added to this product.'),
'pretix.event.item.program_times.changed': _('A program time has been changed on this product.'),
'pretix.event.item.program_times.removed': _('A program time has been removed from this product.'),
})
class CoreItemLogEntryType(ItemLogEntryType):
pass
+2 -2
View File
@@ -72,7 +72,7 @@ class PermissionMiddleware:
)
EXCEPTIONS_FORCED_PW_CHANGE = (
"user.settings",
"user.settings.password.change",
"auth.logout"
)
@@ -139,7 +139,7 @@ class PermissionMiddleware:
return redirect_to_url(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
except SessionPasswordChangeRequired:
if url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
return redirect_to_url(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
return redirect_to_url(reverse('control:user.settings.password.change') + '?next=' + quote(request.get_full_path()))
except Session2FASetupRequired:
if url_name not in self.EXCEPTIONS_2FA:
return redirect_to_url(reverse('control:user.settings.2fa'))
@@ -7,6 +7,7 @@
<h3>{% trans "Set new password" %}</h3>
{% csrf_token %}
{% bootstrap_form_errors form type='all' layout='inline' %}
{% bootstrap_field form.email %}
{% bootstrap_field form.password %}
{% bootstrap_field form.password_repeat %}
<div class="form-group buttons">
@@ -126,7 +126,9 @@
{% endif %}
<a class="navbar-brand" href="{% url "control:index" %}">
<img src="{% static "pretixbase/img/pretix-icon-white-mini.svg" %}" />
{{ settings.PRETIX_INSTANCE_NAME }}
<span>
{{ settings.PRETIX_INSTANCE_NAME }}
</span>
</a>
</div>
<ul class="nav navbar-nav navbar-top-links navbar-left flip hidden-xs">
@@ -0,0 +1,13 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
{{ reason }}
{{ code }}
Please do never give this code to another person. Our support team will never ask for this code.
If this code was not requested by you, please contact us immediately.
Best regards,
Your pretix team
{% endblocktrans %}
@@ -43,19 +43,21 @@
{% bootstrap_field form.invoice_name_required layout="control" %}
{% bootstrap_field form.invoice_address_company_required layout="control" %}
{% bootstrap_field form.invoice_address_vatid layout="control" %}
{% bootstrap_field form.invoice_address_vatid_required_countries layout="control" %}
{% bootstrap_field form.invoice_address_beneficiary layout="control" %}
{% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
{% bootstrap_field form.invoice_address_custom_field layout="control" %}
{% bootstrap_field form.invoice_address_custom_field_helptext layout="control" %}
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
</fieldset>
<fieldset>
<fieldset data-address-information-url="{% url "js_helpers.address_form" %}">
<legend>{% trans "Issuer details" %}</legend>
{% bootstrap_field form.invoice_address_from_name layout="control" %}
{% bootstrap_field form.invoice_address_from layout="control" %}
{% bootstrap_field form.invoice_address_from_zipcode layout="control" %}
{% bootstrap_field form.invoice_address_from_city layout="control" %}
{% bootstrap_field form.invoice_address_from_country layout="control" %}
{% bootstrap_field form.invoice_address_from_state layout="control" %}
{% bootstrap_field form.invoice_address_from_tax_id layout="control" %}
{% bootstrap_field form.invoice_address_from_vat_id layout="control" %}
</fieldset>
@@ -0,0 +1,70 @@
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
<p>
{% blocktrans trimmed %}
With program times, you can set specific dates and times for this product.
This is useful if this product represents access to parts of your event that happen at different times than your event in general.
This will not affect access control, but will affect calendar invites and ticket output.
{% endblocktrans %}
</p>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<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="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Program time" %}</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.start layout="control" %}
{% bootstrap_field form.end layout="control" %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Program time" %}</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_field formset.empty_form.start layout="control" %}
{% bootstrap_field formset.empty_form.end layout="control" %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a program time" %}</button>
</p>
</div>
@@ -362,6 +362,11 @@
</form>
{% endif %}
{% endif %}
{% if staff_session %}
<a class="btn btn-default btn-xs admin-only" href="{% url "control:event.order.inspect" event=request.event.slug organizer=request.event.organizer.slug code=order.code id=i.pk %}">
{% trans "Inspect" %}
</a>
{% endif %}
{% if forloop.revcounter0 > 0 %}
<br/>
{% endif %}
@@ -91,6 +91,8 @@
<div class="col-sm-12">
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new value" %}</button>
<button type="button" class="btn btn-default" data-formset-sort>
<i class="fa fa-sort-alpha-asc"></i> {% trans "Sort alphabetically" %}</button>
</div>
</div>
</div>
@@ -0,0 +1,29 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Change login email address" %}{% endblock %}
{% block content %}
<form action="" method="post" class="form centered-form">
<h1>
{% trans "Change login email address" %}
</h1>
{% csrf_token %}
{% bootstrap_form_errors form %}
<p class="text-muted">
{% trans "This changes the email address used to login to your account, as well as where we send email notifications." %}
</p>
{% bootstrap_field form.old_email %}
{% bootstrap_field form.new_email %}
<p>
{% trans "We will send a confirmation code to your new email address, which you need to enter in the next step to confirm the email address is correct." %}
</p>
<div class="form-group submit-group">
<a href="{% url "control:user.settings" %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save btn-lg">
{% trans "Continue" %}
</button>
</div>
</form>
{% endblock %}
@@ -0,0 +1,24 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Change password" %}{% endblock %}
{% block content %}
<form action="" method="post" class="form centered-form">
<h1>
{% trans "Change password" %}
</h1>
<br>
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.email %}
{% bootstrap_field form.old_pw %}
{% bootstrap_field form.new_pw %}
{% bootstrap_field form.new_pw_repeat %}
<div class="form-group submit-group">
<a href="{% url "control:user.settings" %}" class="btn btn-default btn-cancel">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-primary btn-save btn-lg">
{% trans "Change password" %}
</button>
</div>
</form>
{% endblock %}
@@ -0,0 +1,21 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Enter confirmation code" %}{% endblock %}
{% block content %}
<form action="" method="post" class="form centered-form">
<h1>
{% trans "Enter confirmation code" %}
</h1>
{% csrf_token %}
{% bootstrap_form_errors form type='all' layout='inline' %}
<p>{{ message }}</p>
{% bootstrap_field form.code %}
<div class="form-group submit-group">
<a href="{{ cancel_url }}" class="btn btn-default btn-cancel">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-primary btn-save btn-lg">
{% trans "Continue" %}
</button>
</div>
</form>
{% endblock %}
@@ -3,8 +3,26 @@
{% load bootstrap3 %}
{% block title %}{% trans "Account settings" %}{% endblock %}
{% block content %}
{% if not user.is_verified %}
<div class="alert alert-info">
<p>
{% blocktrans trimmed %}
Your email address is not confirmed yet. To secure your account, please confirm your email address using
a confirmation code we will send to your email address.
{% endblocktrans %}
</p>
<p>
<form action="{% url "control:user.settings.email.send_verification_code" %}" method="post" class="form-horizontal">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
{% trans "Send confirmation email" %}
</button>
</form>
</p>
</div>
{% endif %}
<h1>{% trans "Account settings" %}</h1>
<form action="" method="post" class="form-horizontal">
<form action="" method="post" class="form-horizontal" data-testid="usersettingsform">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
@@ -13,7 +31,7 @@
{% bootstrap_field form.locale layout='horizontal' %}
{% bootstrap_field form.timezone layout='horizontal' %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Notifications" %}</label>
<label class="col-md-3 control-label">{% trans "Notifications" %}</label>
<div class="col-md-9 static-form-row">
{% if request.user.notifications_send and request.user.notification_settings.exists %}
<span class="label label-success">
@@ -41,8 +59,18 @@
{% bootstrap_field form.new_pw layout='horizontal' %}
{% bootstrap_field form.new_pw_repeat layout='horizontal' %}
{% endif %}
{% if user.auth_backend == 'native' %}
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Password" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.password.change" %}">
<span class="fa fa-edit"></span> {% trans "Change password" %}
</a>
</div>
</div>
{% endif %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Two-factor authentication" %}</label>
<label class="col-md-3 control-label">{% trans "Two-factor authentication" %}</label>
<div class="col-md-9 static-form-row">
{% if user.require_2fa %}
<span class="label label-success">{% trans "Enabled" %}</span> &nbsp;
@@ -58,7 +86,7 @@
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="">{% trans "Authorized applications" %}</label>
<label class="col-md-3 control-label">{% trans "Authorized applications" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.oauth.list" %}">
<span class="fa fa-plug"></span>
@@ -67,7 +95,7 @@
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="">{% trans "Account history" %}</label>
<label class="col-md-3 control-label">{% trans "Account history" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.history" %}">
<span class="fa fa-history"></span>
@@ -56,6 +56,7 @@
{% if form.new_pw %}
{% bootstrap_field form.new_pw layout='control' %}
{% bootstrap_field form.new_pw_repeat layout='control' %}
{% bootstrap_field form.is_verified layout='control' %}
{% endif %}
{% bootstrap_field form.last_login layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}
@@ -165,7 +165,7 @@
{% if v.budget|default_if_none:"NONE" != "NONE" %}
<br>
<small class="text-muted">
{{ v.budget_used_orders|money:request.event.currency }} / {{ v.budget|money:request.event.currency }}
{{ v.budget_used|money:request.event.currency }} / {{ v.budget|money:request.event.currency }}
</small>
{% endif %}
</td>
+6
View File
@@ -110,6 +110,10 @@ urlpatterns = [
name='user.settings.2fa.confirm.webauthn'),
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
name='user.settings.2fa.delete'),
re_path(r'^settings/email/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'),
re_path(r'^settings/email/change$', user.UserEmailChangeView.as_view(), name='user.settings.email.change'),
re_path(r'^settings/email/verify', user.UserEmailVerifyView.as_view(), name='user.settings.email.send_verification_code'),
re_path(r'^settings/password/change$', user.UserPasswordChangeView.as_view(), name='user.settings.password.change'),
re_path(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
re_path(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
re_path(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),
@@ -387,6 +391,8 @@ urlpatterns = [
name='event.order.retransmitinvoice'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/invoices/(?P<id>\d+)/reissue$', orders.OrderInvoiceReissue.as_view(),
name='event.order.reissueinvoice'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/invoices/(?P<id>\d+)/inspect$', orders.OrderInvoiceInspect.as_view(),
name='event.order.inspect'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/download/(?P<position>\d+)/(?P<output>[^/]+)/$',
orders.OrderDownload.as_view(),
name='event.order.download.ticket'),
+5 -1
View File
@@ -254,6 +254,9 @@ def invite(request, token):
return redirect('control:index')
else:
with transaction.atomic():
if request.user.email.lower() == inv.email.lower():
request.user.is_verified = True
request.user.save(update_fields=['is_verified'])
inv.team.members.add(request.user)
inv.team.log_action(
'pretix.team.member.joined', data={
@@ -274,7 +277,8 @@ def invite(request, token):
user = User.objects.create_user(
form.cleaned_data['email'], form.cleaned_data['password'],
locale=request.LANGUAGE_CODE,
timezone=request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE
timezone=request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE,
is_verified=form.cleaned_data['email'].lower() == inv.email.lower()
)
user = authenticate(request=request, email=user.email, password=form.cleaned_data['password'])
user.log_action('pretix.control.auth.user.created', user=user)
+31 -8
View File
@@ -60,13 +60,14 @@ from django.views.generic.detail import DetailView, SingleObjectMixin
from django_countries.fields import Country
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemBundleSerializer, ItemVariationSerializer,
ItemAddOnSerializer, ItemBundleSerializer, ItemProgramTimeSerializer,
ItemVariationSerializer,
)
from pretix.base.forms import I18nFormSet
from pretix.base.models import (
CartPosition, Item, ItemCategory, ItemVariation, Order, OrderPosition,
Question, QuestionAnswer, QuestionOption, Quota, SeatCategoryMapping,
Voucher,
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
@@ -75,9 +76,9 @@ from pretix.base.services.tickets import invalidate_cache
from pretix.base.signals import quota_availability
from pretix.control.forms.item import (
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemUpdateForm,
ItemVariationForm, ItemVariationsFormSet, QuestionForm, QuestionOptionForm,
QuotaForm,
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm,
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
)
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required,
@@ -1431,7 +1432,8 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
form.instance.position = i
setattr(form.instance, attr, self.get_object())
created = not form.instance.pk
form.save()
if form.has_changed():
form.save()
if form.has_changed() and any(a for a in form.changed_data if a != 'ORDER'):
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
if key == 'variations':
@@ -1497,6 +1499,16 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
'bundles', 'bundles', 'base_item', order=False,
serializer=ItemBundleSerializer
)
elif k == 'program_times':
self.save_formset(
'program_times', 'program_times', order=False,
serializer=ItemProgramTimeSerializer
)
if not change_data:
for f in v.forms:
if (f in v.deleted_forms and f.instance.pk) or f.has_changed():
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'item': self.object.pk})
break
else:
v.save()
@@ -1559,9 +1571,20 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
queryset=ItemBundle.objects.filter(base_item=self.get_object()),
event=self.request.event, item=self.item, prefix="bundles"
)),
('program_times', inlineformset_factory(
Item, ItemProgramTime,
form=ItemProgramTimeForm, formset=ItemProgramTimeFormSet,
can_order=False, can_delete=True, extra=0
)(
self.request.POST if self.request.method == "POST" else None,
queryset=ItemProgramTime.objects.filter(item=self.get_object()),
event=self.request.event, prefix="program_times"
)),
])
if not self.object.has_variations:
del f['variations']
if self.item.event.has_subevents:
del f['program_times']
i = 0
for rec, resp in item_formsets.send(sender=self.request.event, item=self.item, request=self.request):
+11 -2
View File
@@ -146,7 +146,7 @@ class BaseProcessView(AsyncAction, FormView):
else:
charset = None
try:
return parse_csv(self.file.file, 1024 * 1024, charset=charset)
reader = parse_csv(self.file.file, 1024 * 1024, charset=charset)
except UnicodeDecodeError:
messages.warning(
self.request,
@@ -155,7 +155,16 @@ class BaseProcessView(AsyncAction, FormView):
"Some characters were replaced with a placeholder."
)
)
return parse_csv(self.file.file, 1024 * 1024, "replace", charset=charset)
reader = parse_csv(self.file.file, 1024 * 1024, "replace", charset=charset)
if reader._had_duplicates:
messages.warning(
self.request,
_(
"Multiple columns of the CSV file have the same name and were renamed automatically. We "
"recommend that you rename these in your source file to avoid problems during import."
)
)
return reader
@cached_property
def parsed_list(self):
+25 -2
View File
@@ -131,13 +131,16 @@ from pretix.control.forms.orders import (
ReactivateOrderForm,
)
from pretix.control.forms.rrule import RRuleForm
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, EventPermissionRequiredMixin,
)
from pretix.control.signals import order_search_forms
from pretix.control.views import PaginationMixin
from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.hierarkey import clean_filename
from pretix.helpers.json import CustomJSONEncoder
from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields
@@ -1752,6 +1755,25 @@ class OrderInvoiceReissue(OrderView):
return HttpResponseNotAllowed(['POST'])
class OrderInvoiceInspect(AdministratorPermissionRequiredMixin, OrderView):
def get(self, *args, **kwargs): # NOQA
inv = get_object_or_404(self.order.invoices, pk=kwargs.get('id'))
d = {"lines": []}
for f in inv._meta.fields:
v = getattr(inv, f.name)
d[f.name] = v
for il in inv.lines.all():
line = {}
for f in il._meta.fields:
v = getattr(il, f.name)
line[f.name] = v
d["lines"].append(line)
return JsonResponse(d, encoder=CustomJSONEncoder)
class OrderResendLink(OrderView):
permission = 'can_change_orders'
@@ -2146,7 +2168,8 @@ class OrderChange(OrderView):
self.order,
user=self.request.user,
notify=notify,
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True,
allow_blocked_seats=True,
)
form_valid = (self._process_add_fees(ocm) and
self._process_add_positions(ocm) and
+6 -1
View File
@@ -44,7 +44,9 @@ from pypdf.errors import PdfReadError
from reportlab.lib.units import mm
from pretix.base.i18n import language
from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition
from pretix.base.models import (
CachedFile, InvoiceAddress, ItemProgramTime, OrderPosition,
)
from pretix.base.pdf import get_images, get_variables
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -95,6 +97,9 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
description=_("Sample product description"))
item2 = self.request.event.items.create(name=_("Sample workshop"), default_price=Decimal('23.40'))
ItemProgramTime.objects.create(start=now(), end=now(), item=item)
ItemProgramTime.objects.create(start=now(), end=now(), item=item2)
from pretix.base.models import Order
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
+2 -1
View File
@@ -820,12 +820,13 @@ def organizer_select2(request):
total = qs.count()
pagesize = 20
offset = (page - 1) * pagesize
display_slug = 'display_slug' in request.GET
doc = {
"results": [
{
'id': o.pk,
'text': str(o.name)
'text': '{}{}'.format(o.slug, o.name) if display_slug else str(o.name)
} for o in qs[offset:offset + pagesize]
],
"pagination": {
+164 -21
View File
@@ -44,11 +44,13 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import BadRequest, PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -60,8 +62,11 @@ from django_scopes import scopes_disabled
from webauthn.helpers import generate_challenge, generate_user_handle
from pretix.base.auth import get_auth_backends
from pretix.base.forms.auth import ReauthForm
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
from pretix.base.forms.auth import ConfirmationCodeForm, ReauthForm
from pretix.base.forms.user import (
User2FADeviceAddForm, UserEmailChangeForm, UserPasswordChangeForm,
UserSettingsForm,
)
from pretix.base.models import (
Event, LogEntry, NotificationSetting, U2FDevice, User, WebAuthnDevice,
)
@@ -237,25 +242,7 @@ class UserSettings(UpdateView):
data = {}
for k in form.changed_data:
if k not in ('old_pw', 'new_pw_repeat'):
if 'new_pw' == k:
data['new_pw'] = True
else:
data[k] = form.cleaned_data[k]
msgs = []
if 'new_pw' in form.changed_data:
self.request.user.needs_password_change = False
msgs.append(_('Your password has been changed.'))
if 'email' in form.changed_data:
msgs.append(_('Your email address has been changed to {email}.').format(email=form.cleaned_data['email']))
if msgs:
self.request.user.send_security_notice(msgs, email=form.cleaned_data['email'])
if self._old_email != form.cleaned_data['email']:
self.request.user.send_security_notice(msgs, email=self._old_email)
data[k] = form.cleaned_data[k]
sup = super().form_valid(form)
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data=data)
@@ -834,3 +821,159 @@ class EditStaffSession(StaffMemberRequiredMixin, UpdateView):
return get_object_or_404(StaffSession, pk=self.kwargs['id'])
else:
return get_object_or_404(StaffSession, pk=self.kwargs['id'], user=self.request.user)
class UserPasswordChangeView(FormView):
max_time = 300
form_class = UserPasswordChangeForm
template_name = 'pretixcontrol/user/change_password.html'
def get_form_kwargs(self):
if self.request.user.auth_backend != 'native':
raise PermissionDenied
return {
**super().get_form_kwargs(),
"user": self.request.user,
}
def form_valid(self, form):
with transaction.atomic():
self.request.user.set_password(form.cleaned_data['new_pw'])
self.request.user.needs_password_change = False
self.request.user.save()
msgs = []
msgs.append(_('Your password has been changed.'))
self.request.user.send_security_notice(msgs)
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data={'new_pw': True})
update_session_auth_hash(self.request, self.request.user)
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
def get_success_url(self):
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
return self.request.GET.get("next")
return reverse('control:user.settings')
class UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView):
max_time = 300
form_class = UserEmailChangeForm
template_name = 'pretixcontrol/user/change_email.html'
def get_form_kwargs(self):
if self.request.user.auth_backend != 'native':
raise PermissionDenied
return {
**super().get_form_kwargs(),
"user": self.request.user,
}
def get_initial(self):
return {
"old_email": self.request.user.email
}
def form_valid(self, form):
self.request.user.send_confirmation_code(
session=self.request.session,
reason='email_change',
email=form.cleaned_data['new_email'],
state=form.cleaned_data['new_email'],
)
self.request.session['email_confirmation_destination'] = form.cleaned_data['new_email']
return redirect(reverse('control:user.settings.email.confirm', kwargs={}) + '?reason=email_change')
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class UserEmailVerifyView(View):
def post(self, request, *args, **kwargs):
if self.request.user.is_verified:
messages.success(self.request, _('Your email address was already verified.'))
return redirect(reverse('control:user.settings', kwargs={}))
self.request.user.send_confirmation_code(
session=self.request.session,
reason='email_verify',
email=self.request.user.email,
state=self.request.user.email,
)
self.request.session['email_confirmation_destination'] = self.request.user.email
return redirect(reverse('control:user.settings.email.confirm', kwargs={}) + '?reason=email_verify')
class UserEmailConfirmView(FormView):
form_class = ConfirmationCodeForm
template_name = 'pretixcontrol/user/confirmation_code_dialog.html'
def get_context_data(self, **kwargs):
return {
**super().get_context_data(**kwargs),
"cancel_url": reverse('control:user.settings', kwargs={}),
"message": format_html(
_("Please enter the confirmation code we sent to your email address <strong>{email}</strong>."),
email=self.request.session.get('email_confirmation_destination', ''),
),
}
@transaction.atomic()
def form_valid(self, form):
reason = self.request.GET['reason']
if reason not in ('email_change', 'email_verify'):
raise PermissionDenied
try:
new_email = self.request.user.check_confirmation_code(
session=self.request.session,
reason=reason,
code=form.cleaned_data['code'],
)
except PermissionDenied:
return self.form_invalid(form)
except BadRequest:
messages.error(self.request, _(
'We were unable to verify your confirmation code. Please try again.'
))
return redirect(reverse('control:user.settings', kwargs={}))
log_data = {
'email': new_email,
'email_verified': True,
}
if reason == 'email_change':
msgs = []
msgs.append(_('Your email address has been changed to {email}.').format(email=new_email))
log_data['old_email'] = old_email = self.request.user.email
self.request.user.send_security_notice(msgs, email=old_email)
self.request.user.send_security_notice(msgs, email=new_email)
log_action = 'pretix.user.email.changed'
else:
log_action = 'pretix.user.email.confirmed'
self.request.user.email = new_email
self.request.user.is_verified = True
self.request.user.save()
self.request.user.log_action(log_action, user=self.request.user, data=log_data)
update_session_auth_hash(self.request, self.request.user)
if reason == 'email_change':
messages.success(self.request, _('Your email address has been changed successfully.'))
else:
messages.success(self.request, _('Your email address has been confirmed successfully.'))
return redirect(reverse('control:user.settings', kwargs={}))
def form_invalid(self, form):
messages.error(self.request, _('The entered confirmation code is not correct. Please try again.'))
return super().form_invalid(form)
+1 -1
View File
@@ -87,7 +87,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
@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.exclude(
qs = Voucher.annotate_budget_used(self.request.event.vouchers.exclude(
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
).select_related(
'item', 'variation', 'seat'
+62
View File
@@ -0,0 +1,62 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 django.conf import settings
THRESHOLD_DOWNGRADE_TO_MID = 50
THRESHOLD_DOWNGRADE_TO_LOW = 250
def get_task_priority(shard, organizer_id):
"""
This is an attempt to build a simple "fair-use" policy for webhooks and notifications. The problem is that when
one organizer creates e.g. 20,000 orders through the API, that might schedule 20,000 webhooks and every other
organizer will need to wait for these webhooks to go through.
We try to fix that by building three queues: high-prio, mid-prio, and low-prio. Every organizer starts in the
high-prio queue, and all their tasks are routed immediately. Once an organizer submits more than X jobs of a
certain type per minute, they get downgraded to the mid-prio queue, and then if they submit even more to the
low-prio queue. That way, if another organizer has "regular usage", they are prioritized over the organizer with
high load.
"""
from django_redis import get_redis_connection
if not settings.HAS_REDIS:
return settings.PRIORITY_CELERY_HIGH
# We use redis directly instead of the Django cache API since the Django cache API does not support INCR for
# nonexistant keys
rc = get_redis_connection("redis")
cache_key = f"pretix:task_priority:{shard}:{organizer_id}"
# Make sure counters expire after a while when not used
p = rc.pipeline()
p.incr(cache_key)
p.expire(cache_key, 60)
new_counter = p.execute()[0]
if new_counter >= THRESHOLD_DOWNGRADE_TO_LOW:
return settings.PRIORITY_CELERY_LOW
elif new_counter >= THRESHOLD_DOWNGRADE_TO_MID:
return settings.PRIORITY_CELERY_MID
else:
return settings.PRIORITY_CELERY_HIGH
+21 -13
View File
@@ -22,6 +22,8 @@
import logging
from string import Formatter
from django.utils.html import conditional_escape
logger = logging.getLogger(__name__)
@@ -40,14 +42,14 @@ class SafeFormatter(Formatter):
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
"""
MODE_IGNORE_RICH = 0
MODE_RICH_TO_PLAIN = 1
MODE_RICH_TO_HTML = 2
def __init__(self, context, raise_on_missing=False, mode=MODE_IGNORE_RICH):
def __init__(self, context, raise_on_missing=False, mode=MODE_RICH_TO_PLAIN, linkifier=None):
self.context = context
self.raise_on_missing = raise_on_missing
self.mode = mode
self.linkifier = linkifier
def get_field(self, field_name, args, kwargs):
return self.get_value(field_name, args, kwargs), field_name
@@ -55,22 +57,28 @@ class SafeFormatter(Formatter):
def get_value(self, key, args, kwargs):
if not self.raise_on_missing and key not in self.context:
return '{' + str(key) + '}'
r = self.context[key]
if isinstance(r, PlainHtmlAlternativeString):
if self.mode == self.MODE_IGNORE_RICH:
return '{' + str(key) + '}'
elif self.mode == self.MODE_RICH_TO_PLAIN:
return r.plain
return self.context[key]
def _prepare_value(self, value):
if isinstance(value, PlainHtmlAlternativeString):
if self.mode == self.MODE_RICH_TO_PLAIN:
return value.plain
elif self.mode == self.MODE_RICH_TO_HTML:
return r.html
return r
return value.html
else:
value = str(value)
if self.mode == self.MODE_RICH_TO_HTML:
value = conditional_escape(value)
if self.linkifier:
value = self.linkifier.linkify(value)
return value
def format_field(self, value, format_spec):
# Ignore format_spec
return super().format_field(value, '')
return super().format_field(self._prepare_value(value), '')
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_IGNORE_RICH):
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None):
if not isinstance(template, str):
template = str(template)
return SafeFormatter(context, raise_on_missing, mode=mode).format(template)
return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
+4 -1
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from django.core.files import File
from django_countries.fields import Country
from i18nfield.utils import I18nJSONEncoder
from phonenumber_field.phonenumber import PhoneNumber
@@ -36,7 +37,9 @@ class CustomJSONEncoder(I18nJSONEncoder):
return obj.name
elif isinstance(obj, LazyI18nStringList):
return [s.data for s in obj.data]
if isinstance(obj, PhoneNumber):
elif isinstance(obj, PhoneNumber):
return str(obj)
elif isinstance(obj, Country):
return str(obj)
else:
return super().default(obj)
+210
View File
@@ -0,0 +1,210 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 urllib.parse import quote, urlencode
import text_unidecode
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
def dotdecimal(value):
return str(value).replace(",", ".")
def commadecimal(value):
return str(value).replace(".", ",")
def generate_payment_qr_codes(
event,
code,
amount,
bank_details_sepa_bic,
bank_details_sepa_name,
bank_details_sepa_iban,
):
out = []
for method in [
swiss_qrbill,
czech_spayd,
euro_epc_qr,
euro_bezahlcode,
]:
data = method(
event,
code,
amount,
bank_details_sepa_bic,
bank_details_sepa_name,
bank_details_sepa_iban,
)
if data:
out.append(data)
return out
def euro_epc_qr(
event,
code,
amount,
bank_details_sepa_bic,
bank_details_sepa_name,
bank_details_sepa_iban,
):
if event.currency != 'EUR' or not bank_details_sepa_iban:
return
return {
"id": "girocode",
"label": "EPC-QR",
"qr_data": "\n".join(text_unidecode.unidecode(str(d or '')) for d in [
"BCD", # Service Tag: BCD
"002", # Version: V2
"2", # Character set: ISO 8859-1
"SCT", # Identification code: SCT
bank_details_sepa_bic, # AT-23 BIC of the Beneficiary Bank
bank_details_sepa_name, # AT-21 Name of the Beneficiary
bank_details_sepa_iban, # AT-20 Account number of the Beneficiary
f"{event.currency}{dotdecimal(amount)}", # AT-04 Amount of the Credit Transfer in Euro
"", # AT-44 Purpose of the Credit Transfer
"", # AT-05 Remittance Information (Structured)
code, # AT-05 Remittance Information (Unstructured)
"", # Beneficiary to originator information
"",
]),
}
def euro_bezahlcode(
event,
code,
amount,
bank_details_sepa_bic,
bank_details_sepa_name,
bank_details_sepa_iban,
):
if not bank_details_sepa_iban or bank_details_sepa_iban[:2] != 'DE':
return
if event.currency != 'EUR':
return
qr_data = "bank://singlepaymentsepa?" + urlencode({
"name": str(bank_details_sepa_name),
"iban": str(bank_details_sepa_iban),
"bic": str(bank_details_sepa_bic),
"amount": commadecimal(amount),
"reason": str(code),
"currency": str(event.currency),
}, quote_via=quote)
return {
"id": "bezahlcode",
"label": "BezahlCode",
"qr_data": mark_safe(qr_data),
"link": qr_data,
"link_aria_label": _("Open BezahlCode in your banking app to start the payment process."),
}
def swiss_qrbill(
event,
code,
amount,
bank_details_sepa_bic,
bank_details_sepa_name,
bank_details_sepa_iban,
):
if not bank_details_sepa_iban or not bank_details_sepa_iban[:2] in ('CH', 'LI'):
return
if event.currency not in ('EUR', 'CHF'):
return
if not event.settings.invoice_address_from or not event.settings.invoice_address_from_country:
return
data_fields = [
'SPC',
'0200',
'1',
bank_details_sepa_iban,
'K',
bank_details_sepa_name[:70],
event.settings.invoice_address_from.replace('\n', ', ')[:70],
(event.settings.invoice_address_from_zipcode + ' ' + event.settings.invoice_address_from_city)[:70],
'',
'',
str(event.settings.invoice_address_from_country),
'', # rfu
'', # rfu
'', # rfu
'', # rfu
'', # rfu
'', # rfu
'', # rfu
str(amount),
event.currency,
'', # debtor address
'', # debtor address
'', # debtor address
'', # debtor address
'', # debtor address
'', # debtor address
'', # debtor address
'NON',
'', # structured reference
code,
'EPD',
]
data_fields = [text_unidecode.unidecode(d or '') for d in data_fields]
qr_data = '\r\n'.join(data_fields)
return {
"id": "qrbill",
"label": "QR-bill",
"html_prefix": mark_safe(
'<svg class="banktransfer-swiss-cross" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.8 19.8">'
'<path stroke="#fff" stroke-width="1.436" d="M.7.7h18.4v18.4H.7z"/><path fill="#fff" d="M8.3 4h3.3v11H8.3z"/>'
'<path fill="#fff" d="M4.4 7.9h11v3.3h-11z"/></svg>'
),
"qr_data": qr_data,
"css_class": "banktransfer-swiss-cross-overlay",
}
def czech_spayd(
event,
code,
amount,
bank_details_sepa_bic,
bank_details_sepa_name,
bank_details_sepa_iban,
):
if not bank_details_sepa_iban or not bank_details_sepa_iban[:2] in ('CZ', 'SK'):
return
if event.currency not in ('EUR', 'CZK'):
return
qr_data = f"SPD*1.0*ACC:{bank_details_sepa_iban}*AM:{dotdecimal(amount)}*CC:{event.currency}*MSG:{code}"
return {
"id": "spayd",
"label": "SPAYD",
"qr_data": qr_data,
}
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-29 07:55+0000\n"
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -344,8 +344,8 @@ msgstr ""
msgid "close"
msgstr ""
#: pretix/static/pretixbase/js/addressform.js:90
#: pretix/static/pretixpresale/js/ui/main.js:525
#: pretix/static/pretixbase/js/addressform.js:98
#: pretix/static/pretixpresale/js/ui/main.js:529
msgid "required"
msgstr ""
@@ -622,56 +622,56 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:292
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:296
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:300
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:418
#: pretix/static/pretixcontrol/js/ui/main.js:438
#: pretix/static/pretixcontrol/js/ui/main.js:443
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:436
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:437
#: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:441
#: pretix/static/pretixcontrol/js/ui/main.js:466
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:814
#: pretix/static/pretixcontrol/js/ui/main.js:839
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:817
#: pretix/static/pretixcontrol/js/ui/main.js:842
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:975
#: pretix/static/pretixcontrol/js/ui/main.js:1000
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1015
#: pretix/static/pretixcontrol/js/ui/main.js:1040
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1096
#: pretix/static/pretixcontrol/js/ui/main.js:1121
msgid "You have unsaved changes!"
msgstr ""
@@ -748,7 +748,7 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:570
#: pretix/static/pretixpresale/js/ui/main.js:577
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-29 07:55+0000\n"
"POT-Creation-Date: 2025-11-20 10:37+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/"
@@ -359,8 +359,8 @@ msgstr "لا"
msgid "close"
msgstr "إغلاق"
#: pretix/static/pretixbase/js/addressform.js:90
#: pretix/static/pretixpresale/js/ui/main.js:525
#: pretix/static/pretixbase/js/addressform.js:98
#: pretix/static/pretixpresale/js/ui/main.js:529
msgid "required"
msgstr "مطلوب"
@@ -656,13 +656,13 @@ msgstr "توليد الرسائل …"
msgid "Unknown error."
msgstr "خطأ غير معروف."
#: pretix/static/pretixcontrol/js/ui/main.js:292
#: pretix/static/pretixcontrol/js/ui/main.js:309
#, fuzzy
#| msgid "Your color has great contrast and is very easy to read!"
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr "اللون يتمتع بتباين كبير وتسهل قراءته!"
#: pretix/static/pretixcontrol/js/ui/main.js:296
#: pretix/static/pretixcontrol/js/ui/main.js:313
#, fuzzy
#| msgid "Your color has decent contrast and is probably good-enough to read!"
msgid ""
@@ -670,46 +670,46 @@ msgid ""
"requirements."
msgstr "اللون يحظى بتباين معقول ويمكن أن يكون مناسب للقراءة!"
#: pretix/static/pretixcontrol/js/ui/main.js:300
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:418
#: pretix/static/pretixcontrol/js/ui/main.js:438
#: pretix/static/pretixcontrol/js/ui/main.js:443
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "Search query"
msgstr "البحث في الاستفسارات"
#: pretix/static/pretixcontrol/js/ui/main.js:436
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr "الكل"
#: pretix/static/pretixcontrol/js/ui/main.js:437
#: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None"
msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:441
#: pretix/static/pretixcontrol/js/ui/main.js:466
msgid "Selected only"
msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js:814
#: pretix/static/pretixcontrol/js/ui/main.js:839
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:817
#: pretix/static/pretixcontrol/js/ui/main.js:842
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:975
#: pretix/static/pretixcontrol/js/ui/main.js:1000
msgid "Use a different name internally"
msgstr "قم باستخدم اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:1015
#: pretix/static/pretixcontrol/js/ui/main.js:1040
msgid "Click to close"
msgstr "اضغط لاغلاق الصفحة"
#: pretix/static/pretixcontrol/js/ui/main.js:1096
#: pretix/static/pretixcontrol/js/ui/main.js:1121
msgid "You have unsaved changes!"
msgstr "لم تقم بحفظ التعديلات!"
@@ -806,7 +806,7 @@ msgstr "ستسترد %(currency)%(amount)"
msgid "Please enter the amount the organizer can keep."
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
#: pretix/static/pretixpresale/js/ui/main.js:570
#: pretix/static/pretixpresale/js/ui/main.js:577
msgid "Your local time:"
msgstr "التوقيت المحلي:"
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-29 07:55+0000\n"
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -344,8 +344,8 @@ msgstr ""
msgid "close"
msgstr ""
#: pretix/static/pretixbase/js/addressform.js:90
#: pretix/static/pretixpresale/js/ui/main.js:525
#: pretix/static/pretixbase/js/addressform.js:98
#: pretix/static/pretixpresale/js/ui/main.js:529
msgid "required"
msgstr ""
@@ -622,56 +622,56 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:292
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:296
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:300
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:418
#: pretix/static/pretixcontrol/js/ui/main.js:438
#: pretix/static/pretixcontrol/js/ui/main.js:443
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:436
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:437
#: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:441
#: pretix/static/pretixcontrol/js/ui/main.js:466
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:814
#: pretix/static/pretixcontrol/js/ui/main.js:839
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:817
#: pretix/static/pretixcontrol/js/ui/main.js:842
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:975
#: pretix/static/pretixcontrol/js/ui/main.js:1000
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1015
#: pretix/static/pretixcontrol/js/ui/main.js:1040
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1096
#: pretix/static/pretixcontrol/js/ui/main.js:1121
msgid "You have unsaved changes!"
msgstr ""
@@ -748,7 +748,7 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:570
#: pretix/static/pretixpresale/js/ui/main.js:577
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -7,11 +7,11 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-29 07:55+0000\n"
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
"PO-Revision-Date: 2025-10-31 17:00+0000\n"
"Last-Translator: Núria Masclans <nuriamasclansserrat@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/"
"pretix-js/ca/>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ca/>\n"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -345,8 +345,8 @@ msgstr "No"
msgid "close"
msgstr "Tanca"
#: pretix/static/pretixbase/js/addressform.js:90
#: pretix/static/pretixpresale/js/ui/main.js:525
#: pretix/static/pretixbase/js/addressform.js:98
#: pretix/static/pretixpresale/js/ui/main.js:529
msgid "required"
msgstr "requerit"
@@ -634,11 +634,11 @@ msgstr "Generant missatges…"
msgid "Unknown error."
msgstr "Error desconegut."
#: pretix/static/pretixcontrol/js/ui/main.js:292
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr "El teu color té molt contrast i garanteix bona accessibilitat."
#: pretix/static/pretixcontrol/js/ui/main.js:296
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
@@ -646,7 +646,7 @@ msgstr ""
"El teu color té un contrast acceptable i compleix els requisits mínims "
"daccessibilitat."
#: pretix/static/pretixcontrol/js/ui/main.js:300
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
@@ -654,40 +654,40 @@ msgstr ""
"El color no té prou contrast amb el blanc i pot afectar a l'accessibilitat "
"del lloc web."
#: pretix/static/pretixcontrol/js/ui/main.js:418
#: pretix/static/pretixcontrol/js/ui/main.js:438
#: pretix/static/pretixcontrol/js/ui/main.js:443
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "Search query"
msgstr "Consulta de cerca"
#: pretix/static/pretixcontrol/js/ui/main.js:436
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr "Tots"
#: pretix/static/pretixcontrol/js/ui/main.js:437
#: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None"
msgstr "Cap"
#: pretix/static/pretixcontrol/js/ui/main.js:441
#: pretix/static/pretixcontrol/js/ui/main.js:466
msgid "Selected only"
msgstr "Només seleccionats"
#: pretix/static/pretixcontrol/js/ui/main.js:814
#: pretix/static/pretixcontrol/js/ui/main.js:839
msgid "Enter page number between 1 and %(max)s."
msgstr "Introdueix un número de pàgina entre 1 i %(max)s."
#: pretix/static/pretixcontrol/js/ui/main.js:817
#: pretix/static/pretixcontrol/js/ui/main.js:842
msgid "Invalid page number."
msgstr "Número de pàgina no vàlid."
#: pretix/static/pretixcontrol/js/ui/main.js:975
#: pretix/static/pretixcontrol/js/ui/main.js:1000
msgid "Use a different name internally"
msgstr "Utilitza un nom diferent internament"
#: pretix/static/pretixcontrol/js/ui/main.js:1015
#: pretix/static/pretixcontrol/js/ui/main.js:1040
msgid "Click to close"
msgstr "Prem per tancar"
#: pretix/static/pretixcontrol/js/ui/main.js:1096
#: pretix/static/pretixcontrol/js/ui/main.js:1121
msgid "You have unsaved changes!"
msgstr "Tens canvis sense desar!"
@@ -768,7 +768,7 @@ msgstr "Rebràs %(currency)s %(amount)s"
msgid "Please enter the amount the organizer can keep."
msgstr "Introdueix limport que es queda lorganitzador."
#: pretix/static/pretixpresale/js/ui/main.js:570
#: pretix/static/pretixpresale/js/ui/main.js:577
msgid "Your local time:"
msgstr "La teva hora local:"
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-29 07:55+0000\n"
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
"PO-Revision-Date: 2025-09-08 18:57+0000\n"
"Last-Translator: Alois Pospíšil <alois.pospisil@gmail.com>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -345,8 +345,8 @@ msgstr "Ne"
msgid "close"
msgstr "zavřít"
#: pretix/static/pretixbase/js/addressform.js:90
#: pretix/static/pretixpresale/js/ui/main.js:525
#: pretix/static/pretixbase/js/addressform.js:98
#: pretix/static/pretixpresale/js/ui/main.js:529
msgid "required"
msgstr "povinný"
@@ -647,57 +647,57 @@ msgstr "Vytváření zpráv…"
msgid "Unknown error."
msgstr "Neznámá chyba."
#: pretix/static/pretixcontrol/js/ui/main.js:292
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr "Tato barva má velmi dobrý kontrast a je velmi dobře čitelná."
#: pretix/static/pretixcontrol/js/ui/main.js:296
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
"Tato barva má slušný kontrast a pravděpodobně je dostatečně dobře čitelná."
#: pretix/static/pretixcontrol/js/ui/main.js:300
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:418
#: pretix/static/pretixcontrol/js/ui/main.js:438
#: pretix/static/pretixcontrol/js/ui/main.js:443
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "Search query"
msgstr "Hledaný výraz"
#: pretix/static/pretixcontrol/js/ui/main.js:436
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr "Všechny"
#: pretix/static/pretixcontrol/js/ui/main.js:437
#: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None"
msgstr "Žádný"
#: pretix/static/pretixcontrol/js/ui/main.js:441
#: pretix/static/pretixcontrol/js/ui/main.js:466
msgid "Selected only"
msgstr "Pouze vybrané"
#: pretix/static/pretixcontrol/js/ui/main.js:814
#: pretix/static/pretixcontrol/js/ui/main.js:839
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:817
#: pretix/static/pretixcontrol/js/ui/main.js:842
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:975
#: pretix/static/pretixcontrol/js/ui/main.js:1000
msgid "Use a different name internally"
msgstr "Interně používat jiný název"
#: pretix/static/pretixcontrol/js/ui/main.js:1015
#: pretix/static/pretixcontrol/js/ui/main.js:1040
msgid "Click to close"
msgstr "Kliknutím zavřete"
#: pretix/static/pretixcontrol/js/ui/main.js:1096
#: pretix/static/pretixcontrol/js/ui/main.js:1121
msgid "You have unsaved changes!"
msgstr "Máte neuložené změny!"
@@ -787,7 +787,7 @@ msgstr "Dostanete %(currency)s %(amount)s zpět"
msgid "Please enter the amount the organizer can keep."
msgstr "Zadejte částku, kterou si organizátor může ponechat."
#: pretix/static/pretixpresale/js/ui/main.js:570
#: pretix/static/pretixpresale/js/ui/main.js:577
msgid "Your local time:"
msgstr "Místní čas:"
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-29 07:55+0000\n"
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -345,8 +345,8 @@ msgstr ""
msgid "close"
msgstr ""
#: pretix/static/pretixbase/js/addressform.js:90
#: pretix/static/pretixpresale/js/ui/main.js:525
#: pretix/static/pretixbase/js/addressform.js:98
#: pretix/static/pretixpresale/js/ui/main.js:529
msgid "required"
msgstr ""
@@ -623,56 +623,56 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:292
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:296
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:300
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:418
#: pretix/static/pretixcontrol/js/ui/main.js:438
#: pretix/static/pretixcontrol/js/ui/main.js:443
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:436
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:437
#: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:441
#: pretix/static/pretixcontrol/js/ui/main.js:466
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:814
#: pretix/static/pretixcontrol/js/ui/main.js:839
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:817
#: pretix/static/pretixcontrol/js/ui/main.js:842
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:975
#: pretix/static/pretixcontrol/js/ui/main.js:1000
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1015
#: pretix/static/pretixcontrol/js/ui/main.js:1040
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1096
#: pretix/static/pretixcontrol/js/ui/main.js:1121
msgid "You have unsaved changes!"
msgstr ""
@@ -749,7 +749,7 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:570
#: pretix/static/pretixpresale/js/ui/main.js:577
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-29 07:55+0000\n"
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
"PO-Revision-Date: 2024-07-10 15:00+0000\n"
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -357,8 +357,8 @@ msgstr "Nej"
msgid "close"
msgstr "Luk"
#: pretix/static/pretixbase/js/addressform.js:90
#: pretix/static/pretixpresale/js/ui/main.js:525
#: pretix/static/pretixbase/js/addressform.js:98
#: pretix/static/pretixpresale/js/ui/main.js:529
#, fuzzy
#| msgid "Cart expired"
msgid "required"
@@ -672,56 +672,56 @@ msgstr "Opretter beskeder …"
msgid "Unknown error."
msgstr "Ukendt fejl."
#: pretix/static/pretixcontrol/js/ui/main.js:292
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:296
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:300
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:418
#: pretix/static/pretixcontrol/js/ui/main.js:438
#: pretix/static/pretixcontrol/js/ui/main.js:443
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:436
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:437
#: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None"
msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:441
#: pretix/static/pretixcontrol/js/ui/main.js:466
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:814
#: pretix/static/pretixcontrol/js/ui/main.js:839
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:817
#: pretix/static/pretixcontrol/js/ui/main.js:842
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:975
#: pretix/static/pretixcontrol/js/ui/main.js:1000
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1015
#: pretix/static/pretixcontrol/js/ui/main.js:1040
msgid "Click to close"
msgstr "Klik for at lukke"
#: pretix/static/pretixcontrol/js/ui/main.js:1096
#: pretix/static/pretixcontrol/js/ui/main.js:1121
msgid "You have unsaved changes!"
msgstr "Du har ændringer, der ikke er gemt!"
@@ -818,7 +818,7 @@ msgstr "fra %(currency)s %(price)s"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:570
#: pretix/static/pretixpresale/js/ui/main.js:577
msgid "Your local time:"
msgstr "Din lokaltid:"

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