Compare commits

...
Author SHA1 Message Date
Lukas Bockstaller cf5b2b1b7b fix rst 2026-03-16 09:36:08 +01:00
c39f1bfcc2 handle gift card payment via create order api endpoint (Z#23224691) (#5968)
* adds safeguard to prevent empty giftcard transactions on giftcards of value 0.00

* implement giftcard payment via order create

* styling

* let create_transactions() handle all the mailing

* docs

* provide more context for failed transactions

* documentation lectoring

* reject duplicate gift card secrets

* make payment_provider and use_gift_cards exclusive

* handle unknown gift cards

* Apply suggestion from @pajowu

Co-authored-by: pajowu <engelhardt@pretix.eu>

* Update src/pretix/control/templates/pretixcontrol/giftcards/payment.html

Co-authored-by: pajowu <engelhardt@pretix.eu>

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-03-16 08:51:27 +01:00
Richard SchreiberandGitHub 894128deab Fix log-display for team.invite.deleted (#5988) 2026-03-16 08:21:45 +01:00
luelistaandGitHub 3352ee2bbe Limits of the time machine feature (Z#23212144) (#5952)
* Add note about limits of the time machine feature
* Always check voucher validity against real time, not time machine time
2026-03-12 18:09:16 +01:00
Martin Gross af28785fb9 Stripe: iDEAL -> iDEAL | Wero rebrand 2026-03-12 13:37:35 +01:00
Martin Gross 54e4957e89 Stripe: Update list of supported payment methods 2026-03-12 13:37:06 +01:00
Richard SchreiberandGitHub f3597f1a44 Fix orderlist export with no events (#5936) 2026-03-11 08:08:41 +01:00
2e01887e79 Invoice address: Special validation for Belgium (Z#23224796) (#5970)
* Invoice address: Special validation for Belgium (Z#23224796)

* Update src/pretix/base/invoicing/peppol.py

Co-authored-by: pajowu <engelhardt@pretix.eu>

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-03-10 09:57:44 +01:00
Raphael MichelandGitHub 5a7e7fbde3 Event lists: Show sales channels (Z#23225483) (#5967) 2026-03-10 09:56:29 +01:00
Raphael MichelandGitHub 7b296107c5 Invoice address: Fix broken autofill for Peppol ID (Z#23224796) (#5971)
* Invoice address: Fix broken autofill for Peppol ID (Z#23224796)

* Fix wrong prefix
2026-03-10 09:54:54 +01:00
Raphael MichelandGitHub 4f449ce6b4 Mail: Handle all rendering in mail.py, return values for log (#5895)
* Mail: Handle all rendering in mail.py, return values for log

* Apply suggestions from code review
2026-03-10 09:53:09 +01:00
Raphael MichelandGitHub e6ea8fb5bf Error pages: Load event theme if available (Z#23224853) (#5972) 2026-03-09 20:11:01 +01:00
Raphael MichelandGitHub 547910beec Voucher CSV download: Do not output "any product" (Z#23224795) (#5969) 2026-03-09 18:26:54 +01:00
Raphael MichelandGitHub eef1560ede Order modification: Remove warning when invoice is not yet generated (Z#23226423) (#5966) 2026-03-09 18:16:37 +01:00
Raphael MichelandGitHub 3d68bbb619 Order change manager: Recalculate tax of zero-valued positions (Z#23223874) (#5938) 2026-03-09 18:13:14 +01:00
Raphael MichelandGitHub dc4556d428 PDF editor: add file size to label (Z#23226663) (#5965) 2026-03-09 18:10:57 +01:00
Raphael Michel 5099fa16e0 Fix incorrect type annotation 2026-03-09 17:48:38 +01:00
Kara Engelhardtandpajowu f3fb1e66dc Fix waiting list availability calculation if WL vouchers have seats (Z#23226856) 2026-03-09 17:18:47 +02:00
Ruud HendrickxandRaphael Michel 99e9690d48 Translations: Update Dutch (Belgium)
Currently translated at 71.3% (4465 of 6257 strings)

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

powered by weblate
2026-03-09 14:24:17 +01:00
Hijiri UmemotoandRaphael Michel e63e82e854 Translations: Update Japanese
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-09 14:24:17 +01:00
argonimosandRaphael Michel c662e627d5 Translations: Update German
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-09 14:24:17 +01:00
Mie FrydensbjergandRaphael Michel f2121c7853 Translations: Update Danish
Currently translated at 44.7% (2800 of 6257 strings)

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

powered by weblate
2026-03-09 14:24:17 +01:00
Raphael MichelandGitHub 3ce6dbf798 Mail: Remove redundant SQL queries (#5896)
On my local test event, this saved 75 queries on sending an email due to
an N+1 query problem in the metadata querying.
2026-03-09 13:53:20 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
43b91af5e6 Update sentry-sdk requirement from ==2.53.* to ==2.54.* (#5947)
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.53.0...2.54.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.54.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>
2026-03-09 13:53:00 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
034d6b997e Bump minimatch from 3.0.4 to 3.1.5 in /src/pretix/static/npm_dir (#5937)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 13:52:50 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
345ad35fcf Update protobuf requirement from ==6.33.* to ==7.34.* (#5945)
Updates the requirements on [protobuf](https://github.com/protocolbuffers/protobuf) to permit the latest version.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-version: 7.34.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>
2026-03-09 13:52:44 +01:00
Raphael MichelandGitHub 347337e76f Invoice generation: Add way for renderers to signal they are not ready (#5905) 2026-03-09 13:52:11 +01:00
c07ba31307 API: add organizer-level orderpositions endpoint (#5848)
* initial implementation

* handle permissions

* split out organizer list endpoint

* remove left over empty lines

* revert import changes

* tidying up

* revert no longer needed test changes

* revert no longer needed test changes

* Apply suggestions from code review

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

* add event to api response

* prefetch

* handle auth

* document event

* bump querycounts for prefetches

* Use existing Permission Denied Error Message

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-03-06 11:55:38 +01:00
Ruud HendrickxandRichard Schreiber 87b3e0c417 Translations: Update Dutch (Belgium)
Currently translated at 71.0% (4446 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber d3fd031639 Translations: Update Dutch (Belgium)
Currently translated at 69.6% (4355 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Renne RochaandRichard Schreiber 9253327334 Translations: Update Portuguese (Brazil)
Currently translated at 92.9% (5813 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber 080b9cacaf Translations: Update Dutch (Belgium)
Currently translated at 63.6% (3982 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-esandRichard Schreiber 9c2cc02df1 Translations: Update Spanish
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber fceae0a2fe Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-esandRichard Schreiber 9fc3fdf751 Translations: Update French
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
André AlmeidaandRichard Schreiber 04f79b7014 Translations: Update Portuguese (Brazil)
Currently translated at 92.8% (5811 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber 9d0b9387e6 Translations: Update Dutch (Belgium)
Currently translated at 57.2% (3581 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial PérezandRichard Schreiber b25e6f598d Translations: Update Galician
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial PérezandRichard Schreiber e8e2648f7e Translations: Update Galician
Currently translated at 17.5% (1095 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber e0fac42225 Translations: Update Dutch (Belgium)
Currently translated at 53.1% (3326 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber 3e9bc7675b Translations: Update Dutch (Belgium)
Currently translated at 50.7% (3176 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Hijiri UmemotoandRichard Schreiber 1541033467 Translations: Update Japanese
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber 6b8c3ef15c Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Alberto OrtegaandRichard Schreiber 135e07c183 Translations: Update Spanish
Currently translated at 99.9% (6256 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber fe97915b36 Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Hijiri UmemotoandRichard Schreiber 233281cea4 Translations: Update Japanese
Currently translated at 99.9% (6255 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Renne RochaandRichard Schreiber 0300a44634 Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber 449d930565 Translations: Update Dutch (Belgium)
Currently translated at 46.7% (2927 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial PérezandRichard Schreiber 49f49bd8a6 Translations: Update Galician
Currently translated at 16.7% (1048 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber e896704fe0 Translations: Update Dutch (Belgium)
Currently translated at 42.9% (2689 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial PérezandRichard Schreiber cfee402a27 Translations: Update Galician
Currently translated at 16.3% (1026 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez CerdeiraandRichard Schreiber f8878e53a3 Translations: Update Galician
Currently translated at 16.3% (1026 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Renne RochaandRichard Schreiber fd6a342bc6 Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Pedro OrlandoandRichard Schreiber 865433276e Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
André AlmeidaandRichard Schreiber f616f64f47 Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber 26550887b7 Translations: Update Dutch (Belgium)
Currently translated at 30.7% (1924 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez CerdeiraandRichard Schreiber 0f3de911b8 Translations: Update Galician
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez CerdeiraandRichard Schreiber b648390dbf Translations: Update Galician
Currently translated at 15.7% (986 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez CerdeiraandRichard Schreiber 50fec0b31c Translations: Update Greek
Currently translated at 43.8% (2743 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber e44af04e43 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
André AlmeidaandRichard Schreiber 276c3177f5 Translations: Update Portuguese (Brazil)
Currently translated at 89.7% (5616 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Pedro OrlandoandRichard Schreiber 27ac004a0b Translations: Update Portuguese (Brazil)
Currently translated at 89.7% (5616 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
André AlmeidaandRichard Schreiber 6d517d4e8d Translations: Update Portuguese (Brazil)
Currently translated at 89.7% (5616 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud HendrickxandRichard Schreiber d9c3deda8a Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-esandRichard Schreiber fe6add618a Translations: Update Spanish
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-esandRichard Schreiber 3615a52cc4 Translations: Update French
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-05 07:34:01 +01:00
Kara Engelhardtandpajowu e3ae3b08bd Handle PlainHtmlAlternativeString in placeholder help text 2026-03-04 18:57:25 +02:00
959e926a67 API: validate payment_info (#5944)
* API: validate payment_info

* improve dict-check

* Apply suggestions from code review

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

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-03-02 12:28:47 +01:00
Raphael MichelandGitHub 876ddf1321 Add a log entry on manual VAT ID validation (Z#23223874) (#5939) 2026-02-27 15:22:50 +01:00
Richard Schreiber 005b1d54d3 add missing licenseheaders 2026-02-27 09:09:27 +01:00
2066471086 Fix #1907 – Obfuscate contact email addresses in public HTML (#5477)
* Include nix development enviornment

* Obfuscate contact email addresses in shop HTML and deanonymize via JavaScript

This change addresses #1907: "hide contact e-mail address in source code
of a shop".

- Contact email addresses rendered in public-facing templates are now
obfuscated in the HTML source (e.g., replacing "@" with "[at]" and "."
with "[dot]").
- A new JavaScript file is included in the relevant templates to
automatically rewrite and restore the email address for users after the
page loads.
- This approach helps protect email addresses from basic harvesting bots
and reduces spam, while keeping them accessible and user-friendly for
human visitors.
- The obfuscation and deanonymization logic is only applied to web
templates, not to emails sent via pretix.

This implementation follows the recommendations discussed in #1907,
using a standardized, maintainable approach that’s compatible with
pretix's asset pipeline and template structure.

* Undo nix development environment for merge into main

* convert complete mailto-link to HTML entities

* remove gitignore noise

* Update .gitignore

* fix gitignore noise

* Update .gitignore

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2026-02-27 08:50:33 +01:00
Richard SchreiberandGitHub a25bca7471 Fix static instance name in emails (Z#23224360) (#5914) 2026-02-25 13:19:53 +01:00
luelistaandGitHub da43984ad2 Add datasync logging (Z#23225588) (#5928)
* Fix inconsistent log messages

* Add logging for successfully synced orders

(debugging orders that might get silently skipped)
2026-02-25 09:49:52 +01:00
Martin GrossandGitHub 7cce1c9219 PPv2: Handle paypal-payments/oders in 'created' status (Z#23225625) (#5929) 2026-02-25 09:21:58 +01:00
Martin Gross cb9c4466f9 Revert "PPv2: Do not put payments in pending-state if no capture has occured yet."
This reverts commit e5c8f19984.
2026-02-24 16:55:57 +01:00
Martin Gross 3398cda74b PPv2: properly check for pending-payments in pending-renderer 2026-02-24 16:16:22 +01:00
Martin Gross e5c8f19984 PPv2: Do not put payments in pending-state if no capture has occured yet. 2026-02-24 16:07:16 +01:00
Raphael Michel 5027f6dd59 Bump version to 2026.3.0.dev0 2026-02-24 13:37:15 +01:00
Raphael Michel 787db18d72 Bump version to 2026.2.0 2026-02-24 13:37:09 +01:00
Raphael Michel aadce7be00 Remove print statement from debugging (Z#23225586)
This was reported as a security issue, but we see no security impact or
exploitation path, as the security of PKCE relies on keeping the
verifier secret, not the challenge.
2026-02-24 13:36:52 +01:00
Raphael MichelandRaphael Michel 26f296bc11 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-02-24 13:10:57 +01:00
Raphael MichelandRaphael Michel 6ae80cdd4b Translations: Update German
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-02-24 13:10:57 +01:00
Raphael Michel cb3956c994 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@pretix.eu>
2026-02-24 12:50:51 +01:00
Hijiri UmemotoandRaphael Michel b9f350bf3a Translations: Update Japanese
Currently translated at 100.0% (6247 of 6247 strings)

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

powered by weblate
2026-02-24 12:50:18 +01:00
Raphael MichelandGitHub ab447bb85f Fix HTML injection in error message (Z#23225396) (#5921)
We're not treating it as a security issue as there is no vector to
inject the HTML into other people's browser, only one's own.
2026-02-24 12:48:43 +01:00
Raphael MichelandGitHub bf33a42ae8 Validate request_id_header not to be misunderstood (Z#23225356) (#5920) 2026-02-24 12:48:25 +01:00
Lukas BockstallerandGitHub 081f975ff9 add missing slug fields (#5925) 2026-02-24 10:39:03 +01:00
126 changed files with 22524 additions and 16806 deletions
+64 -2
View File
@@ -117,6 +117,8 @@ cancellation_date datetime Time of order c
reliable for orders that have been cancelled,
reactivated and cancelled again.
plugin_data object Additional data added by plugins.
use_gift_cards list of strings List of unique gift card secrets that are used to pay
for this order.
===================================== ========================== =======================================================
@@ -156,6 +158,10 @@ plugin_data object Additional data
The ``tax_rounding_mode`` attribute has been added.
.. versionchanged:: 2026.03
The ``use_gift_cards`` attribute has been added.
.. _order-position-resource:
Order position resource
@@ -987,8 +993,6 @@ Creating orders
* does not support file upload questions
* does not support redeeming gift cards
* does not support or validate memberships
@@ -1095,6 +1099,14 @@ Creating orders
whether these emails are enabled for certain sales channels. If set to ``null``, behavior will be controlled by pretix'
settings based on the sales channels (added in pretix 4.7). Defaults to ``false``.
Used to be ``send_mail`` before pretix 3.14.
* ``use_gift_cards`` (optional) The provided gift cards will be used to pay for this order. They will be debited and
all the necessary payment records for these transactions will be created. The gift cards will be used in sequence to
pay for the order. Processing of the gift cards stops as soon as the order is payed for. All gift card transactions
are listed under ``payments`` in the response.
This option can only be used with orders that are in the pending state.
The ``use_gift_cards`` attribute can not be combined with ``payment_info`` and ``payment_provider`` fields. If the
order isn't completely paid after its creation with ``use_gift_cards``, then a subsequent request to the payment
endpoint is needed.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
to incrementing integers starting with ``1``. Then, you can reference one of these
@@ -1719,6 +1731,56 @@ List of all order positions
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/orderpositions/
Returns a list of all order positions within all events of a given organizer (with sufficient access permissions).
The supported query parameters and output format of this endpoint are almost identical to those of the list endpoint
within an event.
The only changes are that responses also contain the ``event`` attribute in each result and that the 'pdf_data'
parameter is not supported.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/orderpositions/ 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
X-Page-Generated: 2017-12-01T10:00:00Z
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id:": 23442
"event": "sampleconf",
"order": "ABC12",
"positionid": 1,
"canceled": false,
"item": 1345,
...
}
]
}
:param organizer: The ``slug`` field of the organizer 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.
Fetching individual positions
-----------------------------
+2 -2
View File
@@ -77,7 +77,7 @@ dependencies = [
"phonenumberslite==9.0.*",
"Pillow==12.1.*",
"pretix-plugin-build",
"protobuf==6.33.*",
"protobuf==7.34.*",
"psycopg2-binary",
"pycountry",
"pycparser==3.0",
@@ -92,7 +92,7 @@ dependencies = [
"redis==7.1.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.53.*",
"sentry-sdk==2.54.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
+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__ = "2026.2.0.dev0"
__version__ = "2026.3.0.dev0"
+72 -2
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/>.
#
import json
import logging
import os
from collections import Counter, defaultdict
@@ -52,7 +53,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.invoicing.transmission import get_transmission_types
from pretix.base.models import (
CachedFile, Checkin, Customer, Device, Invoice, InvoiceAddress,
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
Voucher,
@@ -61,6 +62,7 @@ from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
PrintLog, RevokedTicketSecret, Transaction,
)
from pretix.base.payment import GiftCardPayment, PaymentException
from pretix.base.pdf import get_images, get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
@@ -636,6 +638,14 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
return entry
class OrganizerOrderPositionSerializer(OrderPositionSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
class Meta(OrderPositionSerializer.Meta):
fields = OrderPositionSerializer.Meta.fields + ('event',)
read_only_fields = OrderPositionSerializer.Meta.read_only_fields + ('event',)
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
return instance.require_checkin_attention
@@ -1191,6 +1201,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
)
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
use_gift_cards = serializers.ListField(child=serializers.CharField(required=False), required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1206,7 +1217,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode')
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode', 'use_gift_cards')
def validate_payment_provider(self, pp):
if pp is None:
@@ -1215,6 +1226,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The given payment provider is not known.')
return pp
def validate_payment_info(self, info):
if info:
try:
obj = json.loads(info)
except ValueError:
raise ValidationError('payment_info must be valid JSON.')
if not isinstance(obj, dict):
# only objects are allowed
raise ValidationError('payment_info must be a JSON object.')
return info
def validate_expires(self, expires):
if expires < now():
raise ValidationError('Expiration date must be in the future.')
@@ -1289,6 +1312,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False)
simulate = validated_data.pop('simulate', False)
gift_card_secrets = validated_data.pop('use_gift_cards') if 'use_gift_cards' in validated_data else []
if (payment_provider is not None or payment_info != '{}') and len(gift_card_secrets) > 0:
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is not compatible with payment_provider or payment_info']})
if validated_data.get('status') != Order.STATUS_PENDING and len(gift_card_secrets) > 0:
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is only supported for orders that are created as pending']})
if len(set(gift_card_secrets)) != len(gift_card_secrets):
raise ValidationError({"use_gift_cards": ['Multiple copies of the same gift card secret are not allowed']})
if not validated_data.get("sales_channel"):
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
@@ -1773,6 +1804,45 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if order.total != Decimal('0.00') and order.event.currency == "XXX":
raise ValidationError('Paid products not supported without a valid currency.')
for gift_card_secret in gift_card_secrets:
try:
if order.status != Order.STATUS_PAID:
gift_card_payment_provider = GiftCardPayment(event=order.event)
gc = order.event.organizer.accepted_gift_cards.get(
secret=gift_card_secret
)
payment = order.payments.create(
amount=min(order.pending_sum, gc.value),
provider=gift_card_payment_provider.identifier,
info_data={
'gift_card': gc.pk,
'gift_card_secret': gc.secret,
'retry': True
},
state=OrderPayment.PAYMENT_STATE_CREATED
)
gift_card_payment_provider.execute_payment(request=None, payment=payment, is_early_special_case=True)
if order.pending_sum <= Decimal('0.00'):
order.status = Order.STATUS_PAID
except PaymentException:
pass
except GiftCard.DoesNotExist as e:
payment = order.payments.create(
amount=order.pending_sum,
provider=GiftCardPayment.identifier,
info_data={
'gift_card_secret': gift_card_secret,
},
state=OrderPayment.PAYMENT_STATE_CREATED
)
payment.fail(info={**payment.info_data, 'error': str(e)},
send_mail=False)
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
order.status = Order.STATUS_PAID
order.save()
+2 -1
View File
@@ -365,9 +365,10 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,
+2 -1
View File
@@ -67,6 +67,7 @@ orga_router.register(r'invoices', order.InvoiceViewSet)
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
@@ -83,7 +84,7 @@ event_router.register(r'discounts', discount.DiscountViewSet)
event_router.register(r'quotas', item.QuotaViewSet)
event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.EventOrderViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'orderpositions', order.EventOrderPositionViewSet)
event_router.register(r'transactions', order.TransactionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
+49 -12
View File
@@ -57,9 +57,10 @@ from pretix.api.serializers.order import (
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
SimulatedOrderSerializer, TransactionSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerOrderPositionSerializer,
OrganizerTransactionSerializer, PriceCalcSerializer, PrintLogSerializer,
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
TransactionSerializer,
)
from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer,
@@ -1065,8 +1066,7 @@ with scopes_disabled():
}
class OrderPositionViewSet(viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
class OrderPositionViewSetMixin:
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('order__datetime', 'positionid')
@@ -1087,8 +1087,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
ctx['pdf_data'] = False
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
return ctx
@@ -1097,9 +1096,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
qs = OrderPosition.all
else:
qs = OrderPosition.objects
qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
qs = qs.filter(order__event__organizer=self.request.organizer)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(self.request, 'event', None):
prefetch_related_objects([self.request.organizer], 'meta_properties')
prefetch_related_objects(
[self.request.event],
@@ -1154,9 +1152,9 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
'answers', 'answers__options', 'answers__question', 'order__event', 'order__event__organizer'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
'item', 'order', 'seat'
)
return qs
@@ -1168,6 +1166,45 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
return prov
raise NotFound('Unknown output provider.')
class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrganizerOrderPositionSerializer
def get_queryset(self):
qs = super().get_queryset()
perm = self.permission if self.request.method in SAFE_METHODS else self.write_permission
if isinstance(self.request.auth, (TeamAPIToken, Device)):
auth_obj = self.request.auth
elif self.request.user.is_authenticated:
auth_obj = self.request.user
else:
raise PermissionDenied("Unknown authentication scheme")
qs = qs.filter(
order__event__in=auth_obj.get_events_with_permission(perm, request=self.request).filter(
organizer=self.request.organizer
)
)
return qs
class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
return ctx
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(order__event=self.request.event)
return qs
@action(detail=True, methods=['POST'], url_name='price_calc')
def price_calc(self, request, *args, **kwargs):
"""
+2
View File
@@ -183,6 +183,7 @@ class ParametrizedGiftcardWebhookEvent(ParametrizedWebhookEvent):
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'giftcard': giftcard.pk,
'action': logentry.action_type,
}
@@ -197,6 +198,7 @@ class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'acceptor_id': logentry.parsed_data.get('acceptor_id'),
'acceptor_slug': logentry.parsed_data.get('acceptor_slug'),
'giftcard': giftcard.pk,
+5 -2
View File
@@ -216,7 +216,10 @@ class OutboundSyncProvider:
try:
mapped_objects = self.sync_order(sq.order)
if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()):
actions_taken = [res and res.sync_info.get("action", "") for res_list in mapped_objects.values() for res in res_list]
should_write_logentry = any(action not in (None, "nothing_to_do") for action in actions_taken)
logger.info('Synced order %s to %s, actions: %r, log: %r', sq.order.code, sq.sync_provider, actions_taken, should_write_logentry)
if should_write_logentry:
sq.order.log_action("pretix.event.order.data_sync.success", {
"provider": self.identifier,
"objects": {
@@ -237,7 +240,7 @@ class OutboundSyncProvider:
sq.set_sync_error("exceeded", e.messages, e.full_message)
else:
logger.info(
f"Could not sync order {sq.order.code} to {type(self).__name__} "
f"Could not sync order {sq.order.code} to {sq.sync_provider} "
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
exc_info=True,
)
+8 -6
View File
@@ -315,8 +315,9 @@ class OrderListExporter(MultiSheetListExporter):
for id, vn in payment_methods:
headers.append(_('Paid by {method}').format(method=vn))
# get meta_data labels from first cached event
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
if self.event_object_cache:
# get meta_data labels from first cached event if any
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
yield headers
full_fee_sum_cache = {
@@ -503,8 +504,9 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('External customer ID'))
headers.append(_('Payment providers'))
# get meta_data labels from first cached event
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
if self.event_object_cache:
# get meta_data labels from first cached event if any
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
yield headers
yield self.ProgressSetTotal(total=qs.count())
@@ -707,9 +709,9 @@ class OrderListExporter(MultiSheetListExporter):
_('Position order link')
]
# get meta_data labels from first cached event
meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys()
if has_subevents:
# get meta_data labels from first cached event
meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys()
headers += meta_data_labels
yield headers
+1
View File
@@ -1415,6 +1415,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not data.get(r):
raise ValidationError({r: _("This field is required for the selected type of invoice transmission.")})
transmission_type.validate_invoice_address_data(data)
self.instance.transmission_type = transmission_type.identifier
self.instance.transmission_info = transmission_type.form_data_to_transmission_info(data)
elif transmission_type.is_exclusive(self.event, data.get("country"), data.get("is_business")):
+10 -1
View File
@@ -42,6 +42,8 @@ from django.utils.html import escape
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _
from pretix.helpers.format import PlainHtmlAlternativeString
def replace_arabic_numbers(inp):
if not isinstance(inp, str):
@@ -61,11 +63,18 @@ def replace_arabic_numbers(inp):
return inp.translate(table)
def format_placeholder_help_text(placeholder_name, sample_value):
if isinstance(sample_value, PlainHtmlAlternativeString):
sample_value = sample_value.plain
title = (_("Sample: %s") % sample_value) if sample_value else ""
return ('<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(title), escape(placeholder_name)))
def format_placeholders_help_text(placeholders, event=None):
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
placeholders.sort(key=lambda x: x[0])
phs = [
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
format_placeholder_help_text(k, v)
for k, v in placeholders
]
return _('Available placeholders: {list}').format(
+9 -21
View File
@@ -33,8 +33,7 @@ from pretix.base.invoicing.transmission import (
transmission_types,
)
from pretix.base.models import Invoice, InvoiceAddress
from pretix.base.services.mail import mail, render_mail
from pretix.helpers.format import format_map
from pretix.base.services.mail import mail
@transmission_types.new()
@@ -134,9 +133,7 @@ class EmailTransmissionProvider(TransmissionProvider):
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
# Do not set to completed because that is done by the email sending task
subject = format_map(subject, context)
email_content = render_mail(template, context)
mail(
outgoing_mail = mail(
[recipient],
subject,
template,
@@ -151,19 +148,10 @@ class EmailTransmissionProvider(TransmissionProvider):
plain_text_only=True,
no_order_links=True,
)
invoice.order.log_action(
'pretix.event.order.email.invoice',
user=None,
auth=None,
data={
'subject': subject,
'message': email_content,
'position': None,
'recipient': recipient,
'invoices': [invoice.pk],
'attach_tickets': False,
'attach_ical': False,
'attach_other_files': [],
'attach_cached_files': [],
}
)
if outgoing_mail:
invoice.order.log_action(
'pretix.event.order.email.invoice',
user=None,
auth=None,
data=outgoing_mail.log_data()
)
+4
View File
@@ -148,6 +148,10 @@ class NumberedCanvas(Canvas):
self.restoreState()
class InvoiceNotReadyException(Exception):
pass
class BaseInvoiceRenderer:
"""
This is the base class for all invoice renderers.
+6
View File
@@ -204,6 +204,12 @@ class PeppolTransmissionType(TransmissionType):
}
return base | {"transmission_peppol_participant_id"}
def validate_invoice_address_data(self, address_data: dict):
# Special case Belgium: If a Belgian business ID is used as Peppol ID, it should match the VAT ID
if address_data.get("transmission_peppol_participant_id").startswith("0208:") and address_data.get("vat_id"):
if address_data["vat_id"].removeprefix("BE") != address_data["transmission_peppol_participant_id"].removeprefix("0208:"):
raise ValidationError({"transmission_peppol_participant_id": _("The Peppol participant ID does not match your VAT ID.")})
def pdf_watermark(self) -> str:
return pgettext("peppol_invoice", "Visual copy")
+2 -2
View File
@@ -24,7 +24,7 @@ from typing import Optional
from django.utils.translation import gettext_lazy as _
from django_countries.fields import Country
from pretix.base.models import Invoice, InvoiceAddress
from pretix.base.models import Invoice
from pretix.base.signals import EventPluginRegistry, Registry
@@ -89,7 +89,7 @@ class TransmissionType:
def invoice_address_form_fields_visible(self, country: Country, is_business: bool) -> set:
return set(self.invoice_address_form_fields.keys())
def validate_address(self, ia: InvoiceAddress):
def validate_invoice_address_data(self, address_data: dict):
pass
@property
+4 -1
View File
@@ -346,7 +346,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{
'user': self,
'messages': msg,
'url': build_absolute_uri('control:user.settings')
'url': build_absolute_uri('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -391,6 +392,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'user': self,
'reason': msg,
'code': code,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -430,6 +432,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
mail(
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'url': (build_absolute_uri('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
+1 -1
View File
@@ -86,7 +86,7 @@ class OrderSyncQueue(models.Model):
def set_sync_error(self, failure_mode, messages, full_message):
logger.exception(
f"Could not sync order {self.order.code} to {type(self).__name__} ({failure_mode})"
f"Could not sync order {self.order.code} to {self.sync_provider} ({failure_mode})"
)
self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", {
"provider": self.sync_provider,
+17
View File
@@ -220,3 +220,20 @@ class OutgoingMail(models.Model):
error_log_action_type = 'pretix.email.error'
log_target = None
return log_target, error_log_action_type
def log_data(self):
return {
"subject": self.subject,
"message": self.body_plain,
"to": self.to,
"cc": self.cc,
"bcc": self.bcc,
"invoices": [i.pk for i in self.should_attach_invoices.all()],
"attach_tickets": self.should_attach_tickets,
"attach_ical": self.should_attach_ical,
"attach_other_files": self.should_attach_other_files,
"attach_cached_files": [cf.filename for cf in self.should_attach_cached_files.all()],
"position": self.orderposition.positionid if self.orderposition else None,
}
+18 -42
View File
@@ -87,7 +87,6 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import FormattedString, format_map
from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var
from ._transactions import (
@@ -1167,7 +1166,7 @@ class Order(LockModel, LoggedModel):
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import mail, render_mail
from pretix.base.services.mail import mail
if not self.email and not (position and position.attendee_email):
return
@@ -1177,32 +1176,20 @@ class Order(LockModel, LoggedModel):
if position and position.attendee_email:
recipient = position.attendee_email
email_content = render_mail(template, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
outgoing_mail = mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position, auto_email=auto_email, attach_ical=attach_ical,
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
)
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'position': position.positionid if position else None,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
if outgoing_mail:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
def resend_link(self, user=None, auth=None):
with language(self.locale, self.event.settings.region):
@@ -2900,17 +2887,14 @@ class OrderPosition(AbstractPosition):
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param attach_ical: Attach relevant ICS files
"""
from pretix.base.services.mail import mail, render_mail
from pretix.base.services.mail import mail
if not self.attendee_email:
return
with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email
email_content = render_mail(template, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
outgoing_mail = mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
@@ -2919,21 +2903,13 @@ class OrderPosition(AbstractPosition):
attach_ical=attach_ical,
attach_other_files=attach_other_files,
)
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [],
}
)
if outgoing_mail:
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
def resend_link(self, user=None, auth=None):
+12 -19
View File
@@ -34,10 +34,9 @@ from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import User, Voucher
from pretix.base.services.mail import mail, render_mail
from pretix.base.services.mail import mail
from pretix.helpers import OF_SELF
from ...helpers.format import format_map
from ...helpers.names import build_name
from .base import LoggedModel
from .event import Event, SubEvent
@@ -181,10 +180,11 @@ class WaitingListEntry(LoggedModel):
block_quota=True,
item_id=self.item_id,
subevent_id=self.subevent_id,
waitinglistentries__isnull=False
waitinglistentries__isnull=False,
seat__isnull=True
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
if not free_seats:
if free_seats < 1:
raise WaitingListException(_('No seat with this product is currently available.'))
if '@' not in self.email:
@@ -272,9 +272,7 @@ class WaitingListEntry(LoggedModel):
with language(self.locale, self.event.settings.region):
recipient = self.email
email_content = render_mail(template, context)
subject = format_map(subject, context)
mail(
outgoing_mail = mail(
recipient, subject, template, context,
self.event,
self.locale,
@@ -284,18 +282,13 @@ class WaitingListEntry(LoggedModel):
attach_other_files=attach_other_files,
attach_cached_files=attach_cached_files,
)
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
if outgoing_mail:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
@staticmethod
def clean_itemvar(event, item, variation):
+25 -8
View File
@@ -1295,6 +1295,7 @@ class ManualPayment(BasePaymentProvider):
def format_map(self, order, payment):
return {
# Possible placeholder injection, we should make sure to never include user-controlled variables here
'order': order.code,
'amount': payment.amount,
'currency': self.event.currency,
@@ -1525,16 +1526,26 @@ class GiftCardPayment(BasePaymentProvider):
def payment_control_render(self, request, payment) -> str:
from .models import GiftCard
if 'gift_card' in payment.info_data:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
if any(key in payment.info_data for key in ('gift_card', 'error')):
template = get_template('pretixcontrol/giftcards/payment.html')
ctx = {
'request': request,
'event': self.event,
'gc': gc,
**({'error': payment.info_data[
'error']} if 'error' in payment.info_data else {}),
**({'gift_card_secret': payment.info_data[
'gift_card_secret']} if 'gift_card_secret' in payment.info_data else {})
}
return template.render(ctx)
try:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
ctx = {
'gc': gc,
}
except GiftCard.DoesNotExist:
pass
finally:
return template.render(ctx)
def payment_control_render_short(self, payment: OrderPayment) -> str:
d = payment.info_data
@@ -1549,12 +1560,16 @@ class GiftCardPayment(BasePaymentProvider):
try:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
except GiftCard.DoesNotExist:
return {}
return {
**({'error': payment.info_data[
'error']} if 'error' in payment.info_data else {})
}
return {
'gift_card': {
'id': gc.pk,
'secret': gc.secret,
'organizer': gc.issuer.slug
'organizer': gc.issuer.slug,
** ({'error': payment.info_data['error']} if 'error' in payment.info_data else {})
}
}
@@ -1626,6 +1641,8 @@ class GiftCardPayment(BasePaymentProvider):
raise PaymentException(_("This gift card does not support this currency."))
if not gc.accepted_by(self.event.organizer):
raise PaymentException(_("This gift card is not accepted by this event organizer."))
if gc.value <= Decimal("0.00"):
raise PaymentException(_("All credit on this gift card has been used."))
if payment.amount > gc.value:
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
if gc.testmode and not payment.order.testmode:
@@ -1655,7 +1672,7 @@ class GiftCardPayment(BasePaymentProvider):
}
)
except PaymentException as e:
payment.fail(info={'error': str(e)})
payment.fail(info={**payment.info_data, 'error': str(e)}, send_mail=not is_early_special_case)
raise e
def payment_is_valid_session(self, request: HttpRequest) -> bool:
+3 -6
View File
@@ -45,7 +45,6 @@ from pretix.base.services.tax import split_fee_for_taxes
from pretix.base.templatetags.money import money_filter
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.format import format_map
logger = logging.getLogger(__name__)
@@ -55,7 +54,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
mail(
wle.email,
format_map(subject, email_context),
str(subject),
message,
email_context,
wle.event,
@@ -73,9 +72,8 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
order=order, position_or_address=ia, event=order.event)
real_subject = format_map(subject, email_context)
order.send_mail(
real_subject, message, email_context,
subject, message, email_context,
'pretix.event.order.email.event_canceled',
user,
)
@@ -85,14 +83,13 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
real_subject = format_map(subject, email_context)
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
event=order.event,
refund_amount=refund_amount,
position_or_address=p,
order=order, position=p)
order.send_mail(
real_subject, message, email_context,
subject, message, email_context,
'pretix.event.order.email.event_canceled',
position=p,
user=user
+2 -1
View File
@@ -334,7 +334,8 @@ def _check_position_constraints(
raise CartPositionError(error_messages['voucher_invalid_subevent'])
# Voucher expired
if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt:
# (checked using real_now_dt as vouchers influence quota calculations)
if voucher and voucher.valid_until and voucher.valid_until < real_now_dt:
raise CartPositionError(error_messages['voucher_expired'])
# Subevent has been disabled
+2 -1
View File
@@ -51,6 +51,7 @@ from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language
from pretix.base.invoicing.pdf import InvoiceNotReadyException
from pretix.base.invoicing.transmission import (
get_transmission_types, transmission_providers,
)
@@ -504,7 +505,7 @@ def generate_invoice(order: Order, trigger_pdf=True):
return invoice
@app.task(base=TransactionAwareTask)
@app.task(base=TransactionAwareTask, throws=(InvoiceNotReadyException,))
def invoice_pdf_task(invoice: int):
with scopes_disabled():
i = Invoice.objects.get(pk=invoice)
+31 -5
View File
@@ -149,13 +149,13 @@ def prefix_subject(settings_holder, subject, highlight=False):
return subject
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString], template: Union[str, LazyI18nString],
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None,
plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None,
sensitive: bool=False):
sensitive: bool=False) -> Optional[OutgoingMail]:
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -335,14 +335,26 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
should_attach_other_files=attach_other_files or [],
sensitive=sensitive,
)
m._prefetched_objects_cache = {}
if invoices and not position:
m.should_attach_invoices.add(*invoices)
# Hack: For logging, we'll later make a `should_attach_invoices.all()` call. We can prevent a useless
# DB query by filling the cache
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = invoices
else:
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = Invoice.objects.none()
if attach_cached_files:
cf_list = []
for cf in attach_cached_files:
if not isinstance(cf, CachedFile):
m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf))
else:
m.should_attach_cached_files.add(cf)
cf = CachedFile.objects.get(pk=cf)
m.should_attach_cached_files.add(cf)
cf_list.append(cf)
# Hack: For logging, we'll later make a `should_attach_cached_files.all()` call. We can prevent a useless
# DB query by filling the cache
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = cf_list
else:
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = CachedFile.objects.none()
send_task = mail_send_task.si(
outgoing_mail=m.id
@@ -364,6 +376,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
lambda: chain(*task_chain).apply_async()
)
return m
class CustomEmail(EmailMultiAlternatives):
def _create_mime_attachment(self, content, mimetype):
@@ -409,6 +423,18 @@ def mail_send_task(self, **kwargs) -> bool:
outgoing_mail.inflight_since = now()
outgoing_mail.save(update_fields=["status", "inflight_since"])
# Performance optimization, saves database queries later on if we resolve the known relationships
if outgoing_mail.event_id:
assert outgoing_mail.event.organizer_id == outgoing_mail.organizer.pk
outgoing_mail.event.organizer = outgoing_mail.organizer
if outgoing_mail.order_id:
assert outgoing_mail.order.event_id == outgoing_mail.event_id
outgoing_mail.order.event = outgoing_mail.event
outgoing_mail.order.organizer = outgoing_mail.organizer
if outgoing_mail.orderposition_id:
assert outgoing_mail.orderposition.order_id == outgoing_mail.order_id
outgoing_mail.orderposition.order = outgoing_mail.order
headers = dict(outgoing_mail.headers)
headers.setdefault('X-PX-Correlation', str(outgoing_mail.guid))
email = CustomEmail(
+3 -3
View File
@@ -1799,8 +1799,6 @@ class OrderChangeManager:
tax_rule = tax_rules.get(pos.pk, pos.tax_rule)
if not tax_rule:
continue
if not pos.price:
continue
try:
new_rate = tax_rule.tax_rate_for(ia)
@@ -1817,7 +1815,9 @@ class OrderChangeManager:
override_tax_rate=new_rate, override_tax_code=new_code)
self._totaldiff_guesstimate += new_tax.gross - pos.price
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
self._invoice_dirty = True
if pos.price:
# We do not consider the invoice dirty if only 0€-valued taxes are changed
self._invoice_dirty = True
def cancel_fee(self, fee: OrderFee):
self._totaldiff_guesstimate -= fee.value
+7
View File
@@ -24,6 +24,7 @@ import logging
from datetime import timedelta
from decimal import Decimal
from django.db.models import Prefetch, prefetch_related_objects
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.html import escape, mark_safe
@@ -35,6 +36,7 @@ from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.models import EventMetaValue
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
@@ -752,6 +754,11 @@ def base_placeholders(sender, **kwargs):
name_scheme['sample'][f]
))
prefetch_related_objects(
[sender],
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related("property"), to_attr="meta_values_cached")
)
prefetch_related_objects([sender.organizer], Prefetch('meta_properties'))
for k, v in sender.meta_data.items():
ph.append(MarkdownTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
+1
View File
@@ -176,6 +176,7 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),
+3 -3
View File
@@ -39,7 +39,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
with language(event.settings.locale):
email_context = get_email_context(event=event, name=r.get('name') or '',
voucher_list=[v.code for v in voucher_list])
mail(
outgoing_mail = mail(
r['email'],
subject,
LazyI18nString(message),
@@ -60,8 +60,8 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
data={
'recipient': r['email'],
'name': r.get('name'),
'subject': subject,
'message': message,
'subject': outgoing_mail.subject,
'message': outgoing_mail.body_plain,
},
save=False
))
+1 -1
View File
@@ -363,7 +363,7 @@ class EmailAddressShredder(BaseDataShredder):
le.save(update_fields=['data', 'shredded'])
else:
shred_log_fields(le, banlist=[
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email', 'bcc', 'cc',
])
+3
View File
@@ -12,6 +12,9 @@
<meta charset="utf-8">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %}
{% if css_theme %}
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
{% endif %}
</head>
<body>
<div class="container">
@@ -13,5 +13,5 @@ Start time: {{ start_time }} (new data added after this time might not have been
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -0,0 +1,34 @@
#
# 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 import template
from django.utils.html import mark_safe
register = template.Library()
@register.filter("anon_email")
def anon_email(value):
"""Replaces @ with [at] and . with [dot] for anonymization."""
if not isinstance(value, str):
return value
value = value.replace("@", "[at]").replace(".", "[dot]")
return mark_safe(''.join(['&#{0};'.format(ord(char)) for char in value]))
+1 -1
View File
@@ -423,7 +423,7 @@ def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optio
raise ValueError(f"Invalid timeframe '{frame}'")
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[datetime], Optional[datetime]]:
"""
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes
where the first element ist the first possible datetime within the timeframe and the second
+2
View File
@@ -518,6 +518,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'The order requires approval before it can continue to be processed.'),
'pretix.event.order.approved': _('The order has been approved.'),
'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'),
'pretix.event.order.vatid.validated': _('The customer VAT ID has been verified.'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
'to "{new_email}".'),
'pretix.event.order.contact.confirmed': _(
@@ -640,6 +641,7 @@ class TeamMembershipLogEntryType(LogEntryType):
'pretix.team.member.added': _('{user} has been added to the team.'),
'pretix.team.member.removed': _('{user} has been removed from the team.'),
'pretix.team.invite.created': _('{user} has been invited to the team.'),
'pretix.team.invite.deleted': _('Invite for {user} has been deleted.'),
'pretix.team.invite.resent': _('Invite for {user} has been resent.'),
})
class CoreTeamMembershipLogEntryType(TeamMembershipLogEntryType):
@@ -9,5 +9,5 @@ Please do never give this code to another person. Our support team will never as
If this code was not requested by you, please contact us immediately.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -5,5 +5,5 @@ you requested a new password. Please go to the following page to reset your pass
{{ url }}
Best regards,
Your pretix team
{% endblocktrans %}
Your {{ instance }} team
{% endblocktrans %}
@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to a team on pretix, a platform to perform event
you have been invited to a team on {{ instance }}, a platform to perform event
ticket sales.
Organizer: {{ organizer }}
@@ -13,5 +13,5 @@ If you do not want to join, you can safely ignore or delete this email.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
this is to inform you that the account information of your pretix account has been
this is to inform you that the account information of your {{ instance }} account has been
changed. In particular, the following changes have been performed:
{{ messages }}
@@ -12,5 +12,5 @@ You can review and change your account settings here:
{{ url }}
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -2,6 +2,7 @@
{% load i18n %}
{% load urlreplace %}
{% load bootstrap3 %}
{% load static %}
{% block title %}{% trans "Events" %}{% endblock %}
{% block content %}
<h1>{% trans "Events" %}</h1>
@@ -74,6 +75,7 @@
<a href="?{% url_replace request 'ordering' 'organizer' %}"><i class="fa fa-caret-up"></i></a>
</th>
{% endif %}
<th>{% trans "Sales channels" %}</th>
<th>
{% trans "Start date" %}
<a href="?{% url_replace request 'ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
@@ -108,6 +110,21 @@
{% endfor %}
</td>
{% if not hide_orga %}<td>{{ e.organizer }}</td>{% endif %}
<td>
{% for c in e.organizer.sales_channels.all %}
{% if e.all_sales_channels or c in e.limit_sales_channels.all %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% else %}
<span class="fa fa-fw"></span>
{% endif %}
{% endfor %}
</td>
<td class="event-date-col">
{% if e.has_subevents %}
<span class="fa fa-fw- fa-calendar"></span>
@@ -3,16 +3,26 @@
<dl class="dl-horizontal">
<dt>{% trans "Gift card code" %}</dt>
<dd>
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
{{ gc.secret }}
</a>
{% if gc.issuer != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ gc.issuer }}
</span>
{% if gc %}
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
{{ gc.secret }}
</a>
{% if gc.issuer.slug != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ gc.issuer }}
</span>
{% endif %}
{% elif gift_card_secret %}
{{ gift_card_secret }}
{% endif %}
</dd>
<dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd>
{% if gc %}
<dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd>
{% endif %}
{% if error %}
<dt>{% trans "Error" %}</dt>
<dd>{{ error }}</dd>
{% endif %}
</dl>
@@ -24,7 +24,9 @@
{% if log.display %}
<br/><span class="fa fa-fw fa-comment-o"></span> {{ log.display }}
{% endif %}
{% if log.parsed_data.recipient %}
{% if log.parsed_data.to %}
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.to|join:", " }}
{% elif log.parsed_data.recipient %} {# legacy #}
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.recipient }}
{% endif %}
</p>
@@ -1,6 +1,7 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load static %}
{% block inner %}
<h1>
{% blocktrans with name=request.organizer.name %}Organizer: {{ name }}{% endblocktrans %}
@@ -62,6 +63,7 @@
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Sales channels" %}</th>
<th>
{% trans "Start date" %}
/
@@ -77,10 +79,30 @@
<td>
<strong><a
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
<br><small>{{ e.slug }}</small>
{% for k, v in e.meta_data.items %}
{% if v %}
<small class="text-muted">&middot; {{ k }}: {{ v }}</small>
<br>
<small>
{{ e.slug }}
</small>
<small class="text-muted">
{% for k, v in e.meta_data.items %}
{% if v %}
&middot; {{ k }}: {{ v }}
{% endif %}
{% endfor %}
</small>
</td>
<td>
{% for c in sales_channels %}
{% if e.all_sales_channels or c in e.limit_sales_channels.all %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% else %}
<span class="fa fa-fw"></span>
{% endif %}
{% endfor %}
</td>
@@ -264,12 +264,17 @@
The paper size will match the PDF.
{% endblocktrans %}
</p>
<p>
<p class="text-center">
<span class="btn btn-default fileinput-button background-button btn-block">
<i class="fa fa-upload"></i>
<span>{% trans "Upload PDF as background" %}</span>
<input id="fileupload" type="file" name="background" accept="application/pdf">
</span>
<small class="text-muted">
{% blocktrans trimmed with size=maxfilesize|filesizeformat %}
max. {{ size }}, smaller is better
{% endblocktrans %}
</small>
</p>
<p class="text-center">
<a class="btn btn-link background-download-button" href="{{ pdf }}" target="_blank">
+8 -4
View File
@@ -870,11 +870,15 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
)
except ValueError:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
PlaceholderValidator.error_message)
msgs[self.supported_locale[idx]] = format_html(
'<div class="alert alert-danger">{}</div>',
PlaceholderValidator.error_message
)
except KeyError as e:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
msgs[self.supported_locale[idx]] = format_html(
'<div class="alert alert-danger">{}</div>',
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]}
)
return JsonResponse({
'item': preview_item,
+6 -1
View File
@@ -67,7 +67,12 @@ class EventList(PaginationMixin, ListView):
def get_queryset(self):
qs = self.request.user.get_events_with_any_permission(self.request).prefetch_related(
'organizer', '_settings_objects', 'organizer___settings_objects', 'organizer__meta_properties',
'organizer',
'organizer__sales_channels',
'_settings_objects',
'organizer___settings_objects',
'organizer__meta_properties',
'limit_sales_channels',
Prefetch(
'meta_values',
EventMetaValue.objects.select_related('property'),
+15 -7
View File
@@ -1641,9 +1641,17 @@ class OrderCheckVATID(OrderView):
try:
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
with transaction.atomic():
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
self.order.log_action(
'pretix.event.order.vatid.validated',
data={
'vat_id': normalized_id,
},
user=self.request.user,
)
except VATIDFinalError as e:
messages.error(self.request, e.message)
except VATIDTemporaryError:
@@ -2413,9 +2421,9 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
with language(order.locale, self.request.event.settings.region):
email_context = get_email_context(event=order.event, order=order)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
email_subject = format_map(form.cleaned_data['subject'], email_context)
email_content = render_mail(email_template, email_context)
self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(order.event, escape(email_subject), highlight=True)
@@ -2477,9 +2485,9 @@ class OrderPositionSendMail(OrderSendMail):
with language(position.order.locale, self.request.event.settings.region):
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(position.order.event, escape(email_subject), highlight=True))
+4 -1
View File
@@ -207,6 +207,7 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
'organizer').prefetch_related(
'organizer', '_settings_objects', 'organizer___settings_objects',
'organizer__meta_properties',
'limit_sales_channels',
Prefetch(
'meta_values',
EventMetaValue.objects.select_related('property'),
@@ -237,6 +238,7 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
self.filter_form['meta_{}'.format(p.name)] for p in
self.organizer.meta_properties.filter(filter_allowed=True)
]
ctx['sales_channels'] = self.request.organizer.sales_channels.all()
return ctx
@@ -1039,9 +1041,10 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.request.organizer.name,
'team': instance.team.name,
+1
View File
@@ -292,6 +292,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
ctx['layout'] = json.dumps(self.get_current_layout())
ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize
return ctx
+1 -1
View File
@@ -131,7 +131,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
elif v.quota:
prod = _('Any product in quota "{quota}"').format(quota=str(v.quota.name))
else:
prod = _('Any product')
prod = ""
row = [
v.code,
v.valid_until.isoformat() if v.valid_until else "",
+3 -2
View File
@@ -280,11 +280,12 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
block_quota=True,
item_id=wle.item_id,
subevent=wle.subevent_id,
waitinglistentries__isnull=False
waitinglistentries__isnull=False,
seat__isnull=True
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
wle.availability = (
Quota.AVAILABILITY_GONE if free_seats == 0 else wle.availability[0],
Quota.AVAILABILITY_GONE if free_seats < 1 else wle.availability[0],
min(free_seats, wle.availability[1]) if wle.availability[1] is not None else free_seats,
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-20 13:01+0000\n"
"POT-Creation-Date: 2026-02-24 11:50+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"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2025-12-03 23:00+0000\n"
"Last-Translator: sandra r <sandrarial@gestiontickets.online>\n"
"PO-Revision-Date: 2026-03-02 21:00+0000\n"
"Last-Translator: Sandra Rial Pérez <sandrarial@gestiontickets.online>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
"js/gl/>\n"
"Language: gl\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.14.3\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -162,12 +162,12 @@ msgstr "Pedidos pagados"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)"
msgstr ""
msgstr "Asistentes (ordenados)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)"
msgstr ""
msgstr "Asistentes (de pago)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue"
@@ -732,8 +732,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Os artigos da túa cesta xa non están reservados para ti. Aínda podes "
"completar o teu pedido mentres estean dispoñibles."
"Os artigos do teu carro xa non están reservados para ti. Podes completar o "
"teu pedido sempre que estean dispoñibles."
#: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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