Compare commits

...
Author SHA1 Message Date
robbi5 488746044b Don't select last added element on load 2026-09-24 18:53:17 +02:00
robbi5 81d7dfbbc4 Select the new element right after adding it in the pdf editor 2026-09-24 18:15:25 +02:00
Richard Schreiber ed68719731 Widget: fix calendar view showing old events due to non-unique key (Z#23247433) (#6585)
* Widget: fix calendar view showing old events due to cache-key (Z#23247433)

* Update EventCalendarCell.vue
2026-09-24 12:00:57 +02:00
9aeb4b9731 Organizer API: Add endpoint for event-meta-properties
* Add API-endpoint for event-meta-properties

* Add new doc-file to index, fix spelling and description

* Fix logentry

* Fix logentry again

* validate and add tests

* add meta_properties from organizer only

* fix choices validation

* filter unknown keys from choices due to django-formsets

* Apply batched suggestions from code review

Co-authored-by: Richard Schreiber <wiffbi@gmail.com>

* add safe-guard normalization to None to to_representation

* Apply batched suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* update tests to check for error-messages as well

* fix flake8

* fix permission tests

* Make ObjectListField more flexibel for re-use

* fix validation result

* Improve validation

* Change to I18nField for validation

* update MetaPropertyDictField

* fix docs for i18n strings

* make label_child configurable if MetaPropertyDictField should contain non-localized stuff

* undo test changes in events test

* fix flake8

* Apply batched suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* improve code formatting

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-09-24 10:51:23 +02:00
Minding 9324887790 Translate OAuth buttons (#6590)
* Translate OAuth buttons

* Revert .po changes
2026-09-24 09:52:47 +02:00
Nate Horst d353384555 Translations: Update Thai
Currently translated at 53.0% (3407 of 6419 strings)

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

powered by weblate
2026-09-24 09:16:49 +02:00
Raphael Michel b8f8e49cce API: Add additional tests for modifying meta data (Z#23247634) 2026-09-23 19:01:52 +02:00
Raphael Michel 806d0a5748 User details: Show list of 2FA devices and allow to reset drift (#6569)
* User details: Show list of 2FA devices

* Add reset button

* Reset throttle

* Refactoring

* Fix delete paths in tests
2026-09-23 17:51:10 +02:00
Raphael Michel 69e541e5af Product list: Fix edge case in dependent availabilities (Z#23243701) (#6532) 2026-09-23 17:22:54 +02:00
Raphael Michel 0723e6015c Event header: Remove date directly above other date (Z#23245589) (#6524)
* Event header: Fix wrapping of date in h2 header (Z#23245589)

This was already correct if the header was a h1

* Revert "Event header: Fix wrapping of date in h2 header (Z#23245589)"

This reverts commit 3d25316fe3.

* Event header: Remove date directly above other date

This removes the date from the <h2> option used when a header image is
uploaded and the header should still be shown. Rendering the date here
makes little sense, since the infobox with the date is always just a few
lines below. It still makes sense for the <h1> option where it becomes
part of the site header.
2026-09-23 15:54:47 +02:00
Raphael Michel b8e1ab8258 Voucher API: Add search parameter (Z#23240589) (#6523) 2026-09-23 15:54:44 +02:00
Raphael Michel 81764278ae Email settings: Collapse attendee texts if unused (Z#23243417) (#6535) 2026-09-23 14:00:56 +02:00
Nate Horst f3068e627a Translations: Update Thai
Currently translated at 52.2% (3356 of 6419 strings)

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

powered by weblate
2026-09-23 11:41:45 +02:00
Nate Horst 993a3f96e9 Translations: Update Thai
Currently translated at 52.1% (3346 of 6419 strings)

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

powered by weblate
2026-09-23 11:41:45 +02:00
Nate Horst 1af0b5e442 Translations: Update Thai
Currently translated at 51.8% (3330 of 6419 strings)

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

powered by weblate
2026-09-23 11:41:45 +02:00
Nate Horst 71b6bf630f Translations: Update Thai
Currently translated at 45.6% (2930 of 6419 strings)

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

powered by weblate
2026-09-23 11:41:45 +02:00
Raphael MichelandMartin Gross 59026da06b Delete PayPal integration v1 (#6530)
* Delete PayPal integration v1

Leaving only migrations behind

* Try to fix CI

* Fix tests some more

* Rename more plugin name instances in tests and docs

---------

Co-authored-by: Martin Gross <martin@pc-coholic.de>
2026-09-23 11:38:16 +02:00
Raphael Michel b3e0892d76 Conditionally import sentry SDK during error page rendering 2026-09-23 11:33:05 +02:00
Raphael Michel 3029a6839b Prevent Sentry SDK from being loaded during tests 2026-09-23 11:29:28 +02:00
Raphael Michel a826b0a1bd Customer area: Hide password change link for SSO users (fixes #6572) (#6580) 2026-09-22 15:22:43 +02:00
Raphael Michel 7d7474c07d Refund form: Do not allow individual negative amounts (#6579) 2026-09-22 12:47:06 +02:00
Raphael Michel 58023381b9 Merge branch 'refund-offset-perm-check' into 'master'
Offset refund: Do not allow to offset to order without access to

See merge request pretix/pretix!52
2026-09-22 12:46:20 +02:00
Raphael Michel e5c44e7aed Offset refund: Do not allow to offset to order without access to 2026-09-22 12:46:20 +02:00
Raphael Michel d77a179606 2FA: Show info on throttled request (#6581)
* 2FA: Show info on throttled request

* Round up the time
2026-09-22 10:38:26 +02:00
Raphael Michel 29614db1e2 InvoiceShredder: Fix shredding of new fields (#6577)
* InvoiceShredder: Fix shredding of new fields

* Update src/pretix/base/migrations/0311_fix_unshredded_invoices.py
2026-09-22 10:37:58 +02:00
Raphael Michel 365051cc21 Event metadata: Fix server-side validation of write protection (#6578) 2026-09-22 10:00:15 +02:00
Raphael Michel 5c6b0eef6f Plugin configuration: Allow organizers to see all events (#6576) 2026-09-22 09:51:18 +02:00
Raphael Michel bc0a6b662b Checkout: Fix VAT ID revalidation after country change (#6575) 2026-09-22 09:51:10 +02:00
dependabot[bot]andRichard Schreiber 0bc1aed1a0 Update pypdf requirement from ==6.11.* to ==6.19.* (#6570)
* Update pypdf requirement from ==6.11.* to ==6.18.*

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.11.0...6.18.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

* Update pyproject.toml

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-09-22 09:38:28 +02:00
Rita Gimenez 0eaa5a081c Translations: Update Catalan
Currently translated at 29.4% (1892 of 6419 strings)

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

powered by weblate
2026-09-21 14:56:27 +02:00
Nate Horst 688c434c4f Translations: Update Thai
Currently translated at 37.1% (2387 of 6419 strings)

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

powered by weblate
2026-09-21 14:56:27 +02:00
dependabot[bot]andRaphael Michel c0474604a1 Update isort requirement from ==8.0.* to ==9.0.* (#6510)
* Update isort requirement from ==8.0.* to ==9.0.*

Updates the requirements on [isort](https://github.com/PyCQA/isort) to permit the latest version.
- [Release notes](https://github.com/PyCQA/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyCQA/isort/compare/8.0.0...9.0.1)

---
updated-dependencies:
- dependency-name: isort
  dependency-version: 9.0.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* Run isort

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-09-21 14:56:14 +02:00
dependabot[bot] 1741d08c2c Update cryptography requirement from >=50.0.0 to >=50.0.1 (#6509)
Updates the requirements on [cryptography](https://github.com/pyca/cryptography) to permit the latest version.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/50.0.0...50.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.1
  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-09-21 14:14:06 +02:00
dependabot[bot] 6bbd808aaa Bump postcss-selector-parser from 7.1.1 to 7.1.5 (#6513)
Bumps [postcss-selector-parser](https://github.com/postcss/postcss-selector-parser) from 7.1.1 to 7.1.5.
- [Release notes](https://github.com/postcss/postcss-selector-parser/releases)
- [Changelog](https://github.com/postcss/postcss-selector-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss-selector-parser/compare/v7.1.1...7.1.5)

---
updated-dependencies:
- dependency-name: postcss-selector-parser
  dependency-version: 7.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-09-21 14:13:46 +02:00
dependabot[bot] aa90c5b3b1 Bump @humanfs/node from 0.16.7 to 0.16.8 (#6517)
Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8.
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-21 14:13:39 +02:00
dependabot[bot] bfbabe0e58 Update fakeredis requirement from ==2.37.* to ==2.38.* (#6563)
Updates the requirements on [fakeredis](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.37.0...v2.38.0)

---
updated-dependencies:
- dependency-name: fakeredis
  dependency-version: 2.38.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-21 14:08:32 +02:00
Raphael Michelandpajowu aa14505d2c Money representation in templates: Allow more precision (#6454)
* Money representation in templates: Allow more precision

When rendering money in templates, we used to have the following logic:

- When the decimal places fit the currency, render with Babel
- When they don't, e.g. we stored 123.67 JPY, even though there are no
  fractional Yens, render without Babel with a custom format, but render
  the fractional Yens because we'd rather *show* wrong data and make the
  bug obvious than hide it.

However, we only did that up to a prevision of two places, we never
showed more. This is still sufficient for core pretix, but we have
plugins that need to operate in fractional cents. Also, we CAN render
everything through babel for consistent formatting.

There is one **risk**: This might cause weird results on SQLite. Since
SQLite has no concept of precise decimal math, results of in-SQL
computations can sometimes experience floating point errors and show
with A LOT of decimal palces. This used to be invisible since the UI
performed the rounding. With this PR – not any more. We'll need to see
how annoying it is, but it should only affect development mode.

This PR also fixes a bug in tax_rate_format that for some reason did not
do what it was supposed to do, even though I tested it back then, weird.
Might even be a Python version thing?

* Update src/pretix/base/templatetags/money.py

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

* Apply suggestion from @pajowu

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

* Fix typing stuff

* Fix precision issue

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-09-18 11:49:12 +02:00
Kara Engelhardt df69656364 PDF: Fix background page being rotated multiple times
transfer_rotation_to_content (or rather add_transformation/replace_content) do not properly duplicate the contentstream they're modifying. this means that if you pass a pdf-level copy of the same bg_page multiple times, the content will be rotated multiple times but the rotation-property ('/Rotate') will only be changed for the first page

pypdfs docs explicitly say that you should not to add_transformation + merge, but instead use merge_transformed_page, which this now does for everything
2026-09-18 11:35:01 +02:00
Kara Engelhardt 82cffd1519 pdf: calculate sizebox based on cropbox and mediabox 2026-09-18 11:33:34 +02:00
Richard Schreiber 7fc527135d PDF: use artbox before trimbox before mediabox from background-PDF as size for canvas 2026-09-18 11:33:34 +02:00
Raphael Michel 8e39b67ee9 Fix missing translatable field label 2026-09-17 11:56:12 +02:00
dependabot[bot] 289cbe5bfc Update django-countries requirement from ==9.0.* to ==9.1.* (#6562)
Updates the requirements on [django-countries](https://github.com/SmileyChris/django-countries) to permit the latest version.
- [Changelog](https://github.com/SmileyChris/django-countries/blob/main/CHANGES.md)
- [Commits](https://github.com/SmileyChris/django-countries/compare/v9.0.0...v9.1.0)

---
updated-dependencies:
- dependency-name: django-countries
  dependency-version: 9.1.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-09-17 10:43:55 +02:00
Raphael Michel 7143b877c8 Translations: Update German
Currently translated at 100.0% (6419 of 6419 strings)

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

powered by weblate
2026-09-16 15:46:28 +02:00
dependabot[bot] e2a78100af Update pyjwt requirement from ==2.13.* to ==2.14.* (#6557)
Updates the requirements on [pyjwt](https://github.com/jpadilla/pyjwt) to permit the latest version.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.13.0...2.14.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-version: 2.14.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-09-16 15:46:16 +02:00
dependabot[bot] ca77467e48 Update sentry-sdk requirement from ==2.68.* to ==2.69.* (#6556)
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.68.0...2.69.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.69.1
  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-09-16 15:46:07 +02:00
Raphael Michel d3cbfcd4da Invoice export: Add transmission-specific data (Z#23239732) (#6522) 2026-09-15 16:44:00 +02:00
Richard Schreiber 3648ef28d7 Fix deanonymizing emails with multiple dots (#6559) 2026-09-15 13:16:36 +02:00
Richard Schreiber 689b99c3aa PDF: fix inline merge_background on fg_pdf instead of its own PdfWriter (#6555)
* PDF: fix inline merge_background on fg_pdf instead of its own PdfWriter

* fix pdf_header

* fix flake8
2026-09-14 15:45:54 +02:00
Raphael Michel 6c40daa863 Hotfix for JS issue when submit() is called from JS 2026-09-14 15:33:25 +02:00
dependabot[bot]andRaphael Michel efdc83f72a Update pypdf requirement from ==6.5.* to ==6.11.* (#6176)
* Update pypdf requirement from ==6.5.* to ==6.11.*

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.5.0...6.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

* Try to adjust code

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@rami.io>
2026-09-14 14:09:28 +02:00
dependabot[bot] 8b10534c74 Bump smol-toml from 1.6.1 to 1.7.1 (#6546)
Bumps [smol-toml](https://github.com/squirrelchat/smol-toml) from 1.6.1 to 1.7.1.
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/compare/v1.6.1...v1.7.1)

---
updated-dependencies:
- dependency-name: smol-toml
  dependency-version: 1.7.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 10:31:12 +02:00
Raphael Michel bcacff2ee7 Voucher list: Add budget and budget used to CSV export (Z#23237857) (#6520)
* Voucher list: Add budget and budget used to CSV export (Z#23237857)

* Fix failing test
2026-09-14 10:31:07 +02:00
Kara Engelhardt 47096f7f1b sendmail: fix query performance when filtering on checkin (Z#23244045) 2026-09-14 10:24:58 +02:00
Raphael Michel c240ebcb8e Force Django update 2026-09-14 09:51:32 +02:00
Raphael Michel ce3e59e020 PDF reports: Fix headers for overlong event names (Z#23239566) (#6521) 2026-09-14 09:38:44 +02:00
Raphael Michel 4a9da40028 [A11y] Form JavaScript: Support for <button formnovalidate> (#6536) 2026-09-14 09:38:34 +02:00
Raphael Michelandpajowu eab1efd5f4 Phone number input: Fix workaround for prefixes shared between countries (fixes #6544) (#6545)
* Phone number input: Fix workaround for prefixes shared between countries (fixes #6544)

* Update src/pretix/base/forms/questions.py

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

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-09-14 09:35:32 +02:00
Richard Schreiber c7290b614e Show payment-QR-codes on mobile (#6503) 2026-09-14 08:50:45 +02:00
Raphael Michel 1aea5633eb Discounts: Fix ManyToMany fields when cloning (#6547) 2026-09-11 18:35:37 +02:00
Richard Schreiber 86cc719651 Widget: serve vite by default, origin-whitelist for vue2 (#6403) 2026-09-11 08:53:42 +02:00
Mira Weller 742a69f09f Add git-blame-ignore-revs file (see #6541) 2026-09-10 12:54:51 +02:00
Yasunobu YesNo Kawaguchi c05dee7af5 Translations: Update Japanese
Currently translated at 100.0% (6419 of 6419 strings)

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

powered by weblate
2026-09-10 12:36:49 +02:00
Albizuri e399a49c8b Translations: Update Basque
Currently translated at 48.0% (3086 of 6419 strings)

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

powered by weblate
2026-09-10 12:36:49 +02:00
Linnea Thelander b9b951e56b Translations: Update Swedish
Currently translated at 98.0% (255 of 260 strings)

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

powered by weblate
2026-09-10 12:36:49 +02:00
Linnea Thelander 88bb1f4008 Translations: Update Swedish
Currently translated at 85.8% (5509 of 6419 strings)

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

powered by weblate
2026-09-10 12:36:49 +02:00
Renne Rocha 24e7db7ddd Translations: Update Portuguese (Brazil)
Currently translated at 92.4% (5937 of 6419 strings)

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

powered by weblate
2026-09-10 12:36:49 +02:00
Hijiri Umemoto 96ed6fe1a6 Translations: Update Japanese
Currently translated at 100.0% (6419 of 6419 strings)

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

powered by weblate
2026-09-10 12:36:49 +02:00
Raphael MichelandKara Engelhardt d85e52c83e ESlint: Ignore vendored and pre-vue code (#6541)
* ESlint: Ignore vendored and pre-vue code

* Format pre-vue code with eslint where possible

---------

Co-authored-by: Kara Engelhardt <engelhardt@pretix.eu>
2026-09-10 12:35:10 +02:00
Raphael Michel d9b5fa5b16 Fix incomplete warning message 2026-09-10 12:16:10 +02:00
Kara Engelhardt fb948cc7f4 sendmail: small performance winds in checkin query 2026-09-10 11:17:32 +02:00
Kara Engelhardt c358f412fd sendmail: code cleanup 2026-09-10 11:17:32 +02:00
Kara Engelhardt b78de6386b sendmail: fix quadratic checkin query 2026-09-10 11:17:32 +02:00
Raphael MichelandRichard Schreiber 50c00b47ae Event index: Add subevent time and name to <title> (Z#23246174) (#6542)
* Event index: Add subevent time and name to <title> (Z#23246174)

* Update src/pretix/presale/templates/pretixpresale/event/index.html

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

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-09-10 10:05:55 +02:00
Ferdinand Malcher 6025c7942f Product pictures: fix CSS to always fit image to viewport (#6527) 2026-09-09 14:10:15 +02:00
dependabot[bot] 23a3412154 Update django-statici18n requirement from ==2.7.* to ==2.8.* (#6529)
Updates the requirements on [django-statici18n](https://github.com/zyegfryed/django-statici18n) to permit the latest version.
- [Changelog](https://github.com/zyegfryed/django-statici18n/blob/main/docs/changelog.rst)
- [Commits](https://github.com/zyegfryed/django-statici18n/compare/v2.7.0...v2.8.0)

---
updated-dependencies:
- dependency-name: django-statici18n
  dependency-version: 2.8.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-09-09 14:08:54 +02:00
Raphael MichelandRichard Schreiber 3635f6fb0c Widget: Add data-fix equivalent for attendee data (Z#23202714) (#6533)
* Widget: Add data-fix equivalent for attendee data (Z#23202714)

* Update src/pretix/presale/checkoutflow.py

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

* Update src/pretix/presale/checkoutflow.py

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

* Update src/pretix/presale/checkoutflow.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-09-09 14:06:41 +02:00
Kara Engelhardt 2aadfa2ce4 RRuleForm: allow selecting the fourth occurence of a weekday of a month (Z#23245893)
The fourth often is the same as the last, but not always. A weekday can appear five times in a month
2026-09-09 12:21:42 +02:00
Raphael Michel dd0d78242e PDF editor: Harden cached file access for background (Z#23242291) (#6446) 2026-09-09 11:13:29 +02:00
Raphael Michelandluelista 42c5de895c CachedFileField: Bind file access to session (Z#23242301) (#6447)
* CachedFileField: Bind file access to session (Z#23242301)

* Apply suggestion from @luelista

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

---------

Co-authored-by: luelista <weller@rami.io>
2026-09-09 09:52:40 +02:00
Raphael Michelandluelista e91718e73b Password reset: Prevent race condition that uses token twice (Z#23242132) (#6448)
* Password reset: Prevent race condition that uses token twice

* Apply suggestion from @luelista

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

---------

Co-authored-by: luelista <weller@rami.io>
2026-09-09 09:52:38 +02:00
Martin GrossandRaphael Michel 2c15d8b074 Event Settings: Make contact_url and imprint_url I18nURLField (Z#23238623) (#6506)
* Event Settings: Make contact_url and imprint_url I18nURLField (Z#23238623)

* isort

* Update doc/api/resources/events.rst

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-09-09 09:14:37 +02:00
Raphael MichelandLukas Bockstaller edb4069e18 Payment step: Allow to postpone payment choice on some sales channels (#6516)
* Payment step: Allow to postpone payment choice on some sales channels

* Add tests

* handle payment provider (de-)selection and partial payments (#6526)

---------

Co-authored-by: Lukas Bockstaller <bockstaller@pretix.eu>
2026-09-08 09:32:15 +02:00
Raphael MichelandRichard Schreiber dc7d5c6029 Event calendar: consider events without products "not available" for filtering (#6515)
* Event calendar: consider events without products "not available" for filtering

Also, drop the help text of the flag that is no longer accurate.

* Update src/pretix/presale/views/organizer.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-09-04 16:29:46 +02:00
Raphael Michel caa6fb187b VAT ID validation: Alternative API for German shops (#6507) 2026-09-02 17:48:38 +02:00
Lukas Bockstaller 7d93cae2a9 set the _required attribute on the ModelMultipleChoiceField (#6514)
setting _required forces the CheckoutFieldRenderer to add the "required" bit to the label.
We only need to remove it from the widget attrs to avoid the html input field validation that would force us to check every single box
2026-09-02 10:35:19 +02:00
Richard Schreiber 58a58eff83 Widget: fix show taxline for 0 or 0.00 tax-rate (#6502) 2026-09-01 13:19:32 +02:00
Martin Gross a84a02c298 Vite: Add dev CORS (#6511) 2026-09-01 11:22:19 +02:00
Martin Gross 4390403b9a Add label-overrides to contact_form_fields_overrides and question_form_fields_overrides (Z#23244154) (#6504) 2026-09-01 10:21:20 +02:00
Lukas Bockstaller 9d53cf840b PayPal: validate that the sale has any captures before marking paid (#6498)
* validate that the sale has any captures before marking paid

* code style
2026-09-01 10:16:51 +02:00
luelista 023f9104ef Fix customer password reset rate limit (#6501) 2026-08-31 16:48:48 +02:00
Lukas Bockstaller e6572344ca CI: add tracing for e2e tests during failure (#6491)
* collect and upload traces on failure

* include deps for pw install
2026-08-31 12:52:06 +02:00
Raphael Michel 2e8e6a6b07 SBOM creation and upload (#6499)
* Build and upload SBOM

* Merge SBOMs

* Use official cli

* Remove matrix

* Merge SBOMs to array

* Use newer sbom-submit

* debug

* Remove debug

* Explicitly setup node

* Add npm location

* Add test

* REmove test call
2026-08-25 15:28:15 +02:00
dependabot[bot] 19cd0a7f43 Update protobuf requirement from ==7.35.* to ==7.36.*
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.36.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 14:37:23 +02:00
CVZ-es aa583a291f Translations: Update Spanish
Currently translated at 100.0% (6419 of 6419 strings)

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

powered by weblate
2026-08-25 14:33:41 +02:00
CVZ-es 1414c22eb6 Translations: Update French
Currently translated at 100.0% (6419 of 6419 strings)

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

powered by weblate
2026-08-25 14:33:41 +02:00
Raphael Michel 0a6b8c0493 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6419 of 6419 strings)

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

powered by weblate
2026-08-25 14:33:41 +02:00
Raphael Michel 4b4a301e6e Translations: Update German
Currently translated at 100.0% (6419 of 6419 strings)

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

powered by weblate
2026-08-25 14:33:41 +02:00
Richard Schreiber d7aae65777 API: fix tax rules create default handling (#6490)
* API: fix tax rules create default handling

* fix flake8
2026-08-25 09:39:21 +02:00
Richard Schreiber dbd971cc22 API: fix writing old permissions on teams (#6489) 2026-08-25 09:38:11 +02:00
Richard Schreiber 6d4aba6e3d Questions: add min-length to string/text type questions (#6488)
* Questions: add min-length to string/text type questions

* fix test
2026-08-25 09:37:37 +02:00
Raphael Michel 4755e7af0e Translations: Update word list 2026-08-24 17:49:33 +02:00
Raphael Michel f476ce7696 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@pretix.eu>
2026-08-24 17:36:00 +02:00
Raphael Michel bf85789efd Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6421 of 6421 strings)

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

powered by weblate
2026-08-24 17:35:25 +02:00
Raphael Michel 3fbd63e80a Translations: Update German
Currently translated at 100.0% (6421 of 6421 strings)

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

powered by weblate
2026-08-24 17:35:25 +02:00
Raphael Michel 2af60b499f Fix typos and extend wordlist 2026-08-24 17:35:17 +02:00
Raphael Michel b74371bef1 Fix typos 2026-08-24 17:26:37 +02:00
Raphael Michel 9f8ba28cda Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@pretix.eu>
2026-08-24 17:12:24 +02:00
Raphael Michel 44a7bfceab Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@pretix.eu>
2026-08-24 16:39:26 +02:00
Albizuri 5af9be6ef0 Translations: Update Basque
Currently translated at 48.2% (3083 of 6387 strings)

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

powered by weblate
2026-08-24 16:38:54 +02:00
b809d93bdc Do not hide subevent list if filtered list is empty (#6460)
* Do not hide subevent list if filtered list is empty

* Event calendar: Allow to show a message if no events are found

* Apply suggestion from @luelista

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

* Fix failures on org level

* add aria-hidden if no subevents

* Update src/pretix/base/settings.py

* Update src/pretix/base/settings.py

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

---------

Co-authored-by: luelista <weller@rami.io>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-08-24 14:42:41 +02:00
Raphael Michel d5a8d71ed3 Sentry: enable_logs is removed 2026-08-24 14:42:15 +02:00
dependabot[bot] d481574d42 Update sentry-sdk requirement from ==2.66.* to ==2.68.*
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.66.0...2.68.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-24 14:42:15 +02:00
Kara Engelhardt 0593172146 Transmit invoices generated due to paymentprovider changes (Z#23242806) 2026-08-24 13:45:54 +02:00
Translate pretix user 586 64a5361dba Translations: Update Italian
Currently translated at 60.8% (3887 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 5622c1ddaa Translations: Update Italian
Currently translated at 60.7% (3877 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 73c29fc3b9 Translations: Update Italian
Currently translated at 60.1% (3842 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 c73f2f77cf Translations: Update Italian
Currently translated at 59.2% (3787 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 55a0db3a32 Translations: Update Italian
Currently translated at 57.7% (3687 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 a47768312b Translations: Update Italian
Currently translated at 57.5% (3676 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Xiaofan Wang cf272bf575 Translations: Update Chinese (Simplified Han script)
Currently translated at 44.7% (2861 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Hijiri Umemoto f3e0e7f4ac Translations: Update Japanese
Currently translated at 100.0% (6387 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 5a06f9416c Translations: Update Italian
Currently translated at 53.0% (3387 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 606e3f1c0f Translations: Update Italian
Currently translated at 99.6% (259 of 260 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 e38ae32028 Translations: Update Italian
Currently translated at 51.4% (3287 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 b74e2fa3e1 Translations: Update Italian
Currently translated at 48.3% (3087 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 c79a7002a6 Translations: Update Italian
Currently translated at 45.5% (2907 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Xiaofan Wang 1ebbae2b5b Translations: Update Chinese (Simplified Han script)
Currently translated at 44.7% (2859 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 7db3b47889 Translations: Update Italian
Currently translated at 43.3% (2767 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
IGnacy-NorthVan b7ac8fc4ef Translations: Update Polish
Currently translated at 89.3% (5706 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Hijiri Umemoto c1a830e54d Translations: Update Japanese
Currently translated at 100.0% (6387 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Hijiri Umemoto 2178dee6db Translations: Update French
Currently translated at 99.9% (6382 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 266104f4af Translations: Update Italian
Currently translated at 42.5% (2720 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Translate pretix user 586 d10afbd080 Translations: Update Italian
Currently translated at 40.3% (2579 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Hijiri Umemoto 1e78bfbc4c Translations: Update Chinese (Traditional Han script)
Currently translated at 88.4% (5647 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Hijiri Umemoto 08356feea1 Translations: Update Japanese
Currently translated at 100.0% (6387 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Julien b124cb6aad Translations: Update Swedish
Currently translated at 98.4% (256 of 260 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Julien cb5182f21c Translations: Update Swedish
Currently translated at 86.2% (5510 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Julien 011487404c Translations: Update French
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Julien 7703c04954 Translations: Update French
Currently translated at 99.8% (6380 of 6387 strings)

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

powered by weblate
2026-08-24 13:33:21 +02:00
Raphael Michelandluelista 58f331ba1f Allow to set payment term per sales channel (#6459)
* Allow to set payment term per sales channel

* Apply suggestion from @luelista

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

---------

Co-authored-by: luelista <weller@rami.io>
2026-08-24 09:09:47 +02:00
Martin Gross 91a3993cef Seating: Set subevents to None if event has no subevents for _seat_allowed annotation (Z#23243880) (#6487) 2026-08-19 14:12:05 +02:00
pajowuandRichard Schreiber 4969c12c0a Fix widget not regenerating (#6474)
* Fix widget not regenerating

Adds support for use_vite variant to updateassets management command and unifies widget generation code

* Simplify DEBUG-mode handling

* fix should_save if file does not exist

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2026-08-19 09:14:24 +02:00
Lukas Bockstaller 0b319d201c handle captures that are declined (PRETIXEU-F8X & PRETIXEU-F93) (#6482)
* handle captures that are declined

* handle failed captures not stored in sale

* further fixes

* fix tests
2026-08-19 08:41:25 +02:00
Richard Schreiber eceeb9caa7 Widget: fix missing item-id in pricebox (Z#23243324) (#6485) 2026-08-18 16:29:33 +02:00
Richard Schreiber 3c17d05d05 Widget: fix hiding taxline when rate is 0 (Z#23243324) (#6486) 2026-08-18 16:19:36 +02:00
Richard Schreiber 1c17c35b16 Add event_begin_time & event_end_time to scheduledmail templates (Z#23241932) (#6484) 2026-08-18 16:19:15 +02:00
Richard Schreiber f9ea9de078 Fix sendmail/scheduled-mails preview event-based placeholders
* Fix sendmail/scheduled-mails placeholders

* Add event_or_subevent to context

* Add placeholders to tests

* fix and improve test

* Revert "fix and improve test"

This reverts commit 874a11e982.

* undo placeholder-tests

* add test
2026-08-18 14:54:19 +02:00
luelista 4dd6e1a19f Fix attendee addresses for country requiring state in address (#6480) 2026-08-14 17:06:41 +02:00
Richard Schreiber 4cb9d76281 Fix typo in event’s default download reminder attendee (#6473) 2026-08-14 16:47:39 +02:00
luelistaandRichard Schreiber f8cc31b120 Order-level questions (#6471)
* New CheckoutSession model, created and deleted throught cart lifetime but only used for order-level question answers so far
* Order-level QuestionAnswers (relations to CheckoutSession / Order)
* New container_type field on Question model to specify whether Question belongs to order or orderposition
* Order-level questions are currently experimental, UI is hidden behind feature flag

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-08-14 14:56:45 +02:00
pajowuandRichard Schreiber b241adb7d0 Fix safari blocking widget opening error url in new tab (Z#23243124) (#6477)
* Fix safari blocking widget opening error url in new tab (Z#23243124)

* Fix autofocus in vue3 widget

autofocus no longer works as the elements are added dynamically

* fix eslint

* rename v-focus to v-autofocus

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2026-08-14 12:34:55 +02:00
Martin Gross 7b1558b22e Revert "Stripe/Wero: Do not send payment_method_data"
This reverts commit 488f731396.
2026-08-14 12:07:34 +02:00
Martin Gross 488f731396 Stripe/Wero: Do not send payment_method_data 2026-08-14 11:24:34 +02:00
Lukas Bockstaller b8b02de283 paypal change default for allow_retries_during_compliance_hold 2026-08-13 18:10:38 +02:00
Lukas Bockstallerandpajowu c4a5a9a84d BasePaymentProvider & PayPal2: allow to cancel pending payments on a per payment basis (Z#23240966) (#6472)
* move payment into pending on PENDING_REVIEW webhook

* mark approved payment as pending

* extend BasePaymentProvider to gate aborting pending payments on a payment per payment basis

* add timeout to paypal after which a pending payment can be canceled

* formatting

* add missing negation

* cleanup abort_pending_allowed methods

* Apply suggestions from code review

Co-authored-by: pajowu <pajowu@pajowu.de>

* check all capture elements

* rename method and change defaults

* remove left over Constant

* flake8 .

---------

Co-authored-by: pajowu <pajowu@pajowu.de>
2026-08-13 17:28:54 +02:00
Kara Engelhardt 4e5fbacf6d Replace deprecated redis setex call 2026-08-13 16:59:42 +02:00
7fe31634e6 Allow RelativeDate in relation to a moment of order (#6160)
* initial implementation

* factor out _resolve_base_date

* add js to prevent illegal inputs

* fix tests

* Update src/pretix/base/reldate.py

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

* Apply suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* move js includes of to fragment_js.html

* add type annotations

* moves logic from RelativeDateWrapper into RelativeDate and adds BaseChoice for configuring which models attributes support which relationship

* fix tests

* test upgrade behaviour

* Apply suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* move reldate.js include in correct file

* add OptionAttrsSelect to allow select options with their own attributes per value

* add a little bit of information to the tests for future reference

* rewrite reldate.js use data-attributes

* general cleanup

* add test for order.subevents cases

* use correct choice format

* remove order.subevent variants

* various cleanup

* Apply suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* remove empty docstrings

* add depreciation warning

* change event listener to pretix:bind-forms

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-08-12 15:37:29 +02:00
Raphael MichelandRichard Schreiber f25c233e91 Fix performance issues in download reminder (#6393)
* Fix performance issues in download reminder

* Update src/pretix/base/services/orders.py

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

* Fixes after review

* Fix check in loop

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-08-10 16:13:54 +02:00
robbi5 5cf28f1b81 Fix typo in devicesecurity blocking checkinrpc.annul (#6467) 2026-08-10 15:58:15 +02:00
Raphael Michel d08216d8c5 API: Allow to simulate check-ins (#6360)
* API: Allow to simulate check-ins

* Add missing file
2026-08-07 18:59:01 +02:00
Lukas Bockstaller 4a28689690 log paypal payment durations (#6461)
* log payment processing durations

* remove log noise

* fix attribute access

* remove debugging import
2026-08-07 16:34:36 +02:00
luelista 958f75b109 Add tests to prevent reintroducing CSP nonces (Z#23240534) (#6409)
As discussed in PR #6387
2026-08-07 14:36:46 +02:00
314 changed files with 37101 additions and 21590 deletions
+2
View File
@@ -0,0 +1,2 @@
# Format pre-vue code with eslint where possible (2026-09-10)
d85e52c83ed3639e040372fd0052e842e4899d90
+43
View File
@@ -0,0 +1,43 @@
name: SBOM
on:
push:
branches: [ master, sbom ]
tags: [ 'v.*' ]
permissions:
contents: read # to fetch code (actions/checkout)
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ubuntu-22.04
name: Submission
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Use Node.js
uses: actions/setup-node@v7
with:
node-version: '24.x'
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install -y gettext unzip
- name: Install Python dependencies
run: pip3 install -U "prisma-sbom-submit[python]"
- name: Create SBOM
run: NPM=$(which npm) prisma-sbom-submit collect . sbom.json
- name: Submit SBOM
run: prisma-sbom-submit upload --server https://prisma.pretix.com sbom.json
env:
PRISMA_UPLOAD_TOKEN: ${{ secrets.PRISMA_UPLOAD_TOKEN }}
+19 -2
View File
@@ -123,7 +123,24 @@ jobs:
working-directory: ./src working-directory: ./src
run: make all compress run: make all compress
- name: Install Playwright browsers - name: Install Playwright browsers
run: playwright install run: playwright install --with-deps
- name: Run E2E tests - name: Run E2E tests
working-directory: ./src working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10 run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10 --tracing=retain-on-failure
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-traces
path: test-results/
- name: Log trace instructions
if: steps.check-traces.outputs.found == 'true'
run: |
{
echo "## 🎭 Playwright traces available"
echo ""
echo "Some tests failed or retried and produced traces."
echo ""
echo "1. Download the **playwright-traces-${{ github.run_id }}** artifact from this run (link in the **Summary** tab, under Artifacts)."
echo "2. Unzip it."
echo "3. Go to https://trace.playwright.dev and drag \`trace.zip\` into the page — or run \`npx playwright show-trace trace.zip\` locally."
} >> "$GITHUB_STEP_SUMMARY"
-2
View File
@@ -16,8 +16,6 @@ recursive-include src/pretix/plugins/banktransfer/templates *
recursive-include src/pretix/plugins/banktransfer/static * recursive-include src/pretix/plugins/banktransfer/static *
recursive-include src/pretix/plugins/manualpayment/templates * recursive-include src/pretix/plugins/manualpayment/templates *
recursive-include src/pretix/plugins/manualpayment/static * recursive-include src/pretix/plugins/manualpayment/static *
recursive-include src/pretix/plugins/paypal/templates *
recursive-include src/pretix/plugins/paypal/static *
recursive-include src/pretix/plugins/paypal2/templates * recursive-include src/pretix/plugins/paypal2/templates *
recursive-include src/pretix/plugins/paypal2/static * recursive-include src/pretix/plugins/paypal2/static *
recursive-include src/pretix/plugins/src/pretixdroid/templates * recursive-include src/pretix/plugins/src/pretixdroid/templates *
+2
View File
@@ -71,6 +71,8 @@ Checking a ticket in
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``. :>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``. :>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``. :>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
object will not reflect the simulated changes.
**Example request**: **Example request**:
+241
View File
@@ -0,0 +1,241 @@
Event Meta Properties
=====================
Resource description
--------------------
An event meta property is used to to define meta information fields for its events.
This information can be re-used, for example, in ticket layouts.
The event meta property resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Unique ID for this property
name string Name of the property
default string Value of the default option
required boolean If ``true``, an event can only be taken live if the
property is set. In event series, it's always optional
to set a value for individual dates
protected boolean If ``true``, the value for an event can only be changed
by organizer-level administrators
filter_public boolean If ``true``, this property will be shown to filter
events in the public event list and calendar
public_label string Public name of the property
filter_allowed boolean If ``true``, this property will be shown to filter
events or reports in the backend, and it can also be
used for hidden filter parameters in the frontend
choices list of objects List of JSON objects representing all permitted values
for this property, or ``null`` for no limitation.
Each choice object has a required internal name named
``key`` and optional public name named ``label``
consisting of a dictionary of i18n string translations
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/
Returns a list of all meta properties for the organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Color",
"default": "blue",
"required": false,
"protected": false,
"filter_public": false,
"public_label": {},
"filter_allowed": true,
"choices": [
{
"key": "blue",
"label": {
"en": "Blue"
},
}
]
}
]
}
:param organizer: The ``slug`` field of the organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Returns information on one property, identified by its id.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/event_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"id": 1,
"name": "Color",
"default": "blue",
"required": false,
"protected": false,
"filter_public": false,
"public_label": {},
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to retrieve
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/event_meta_properties/
Creates a new meta property
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/event_meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "ref-code",
"default": "abcde",
"required": true,
"choices": null
}
**Example response**:
.. sourcecode:: http
{
"id": 2,
"name": "reference",
"default": "abcde",
"required": true,
"protected": false,
"filter_public": false,
"public_label": null,
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:statuscode 201: no error
:statuscode 400: The meta property could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Update a meta property. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide
all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the
fields that you want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/event_meta_properties/2/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"required": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 3,
"name": "reference",
"default": "abcde",
"required": false,
"protected": false,
"filter_public": false,
"public_label": null,
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to modify
:statuscode 200: no error
:statuscode 400: The property could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Delete a meta property.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/event_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
+17 -15
View File
@@ -110,7 +110,7 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer",
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal", "pretix.plugins.paypal2",
"pretix.plugins.ticketoutputpdf" "pretix.plugins.ticketoutputpdf"
], ],
"all_sales_channels": false, "all_sales_channels": false,
@@ -199,7 +199,7 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer",
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal", "pretix.plugins.paypal2",
"pretix.plugins.ticketoutputpdf" "pretix.plugins.ticketoutputpdf"
], ],
"valid_keys": { "valid_keys": {
@@ -262,7 +262,7 @@ Endpoints
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal2"
], ],
"all_sales_channels": true, "all_sales_channels": true,
"limit_sales_channels": [] "limit_sales_channels": []
@@ -299,7 +299,7 @@ Endpoints
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal2"
], ],
"all_sales_channels": true, "all_sales_channels": true,
"limit_sales_channels": [], "limit_sales_channels": [],
@@ -364,7 +364,7 @@ Endpoints
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal2"
], ],
"all_sales_channels": true, "all_sales_channels": true,
"limit_sales_channels": [] "limit_sales_channels": []
@@ -401,7 +401,7 @@ Endpoints
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal" "pretix.plugins.paypal2"
], ],
"all_sales_channels": true, "all_sales_channels": true,
"limit_sales_channels": [], "limit_sales_channels": [],
@@ -438,7 +438,7 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer",
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal", "pretix.plugins.paypal2",
"pretix.plugins.pretixdroid" "pretix.plugins.pretixdroid"
] ]
} }
@@ -475,7 +475,7 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer",
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal", "pretix.plugins.paypal2",
"pretix.plugins.pretixdroid" "pretix.plugins.pretixdroid"
], ],
"all_sales_channels": true, "all_sales_channels": true,
@@ -566,7 +566,7 @@ organizer level.
Content-Type: application/json Content-Type: application/json
{ {
"imprint_url": "https://pretix.eu", "region": "DE",
… …
} }
@@ -579,12 +579,14 @@ organizer level.
Content-Type: application/json Content-Type: application/json
{ {
"imprint_url": "region":
{ {
"value": "https://pretix.eu", "value": "DE",
"label": "Imprint URL", "label": "Region",
"readonly": false, "readonly": false,
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information." "help_text": "Will be used to determine date and time formatting as well as default country for customer
addresses and phone numbers. For formatting, this takes less priority than the language and
is therefore mostly relevant for languages used in different regions globally (like English)."
} }
}, },
… …
@@ -620,7 +622,7 @@ organizer level.
Content-Type: application/json Content-Type: application/json
{ {
"imprint_url": "https://example.org/imprint/" "region": "DE"
} }
**Example response**: **Example response**:
@@ -632,7 +634,7 @@ organizer level.
Content-Type: application/json Content-Type: application/json
{ {
"imprint_url": "https://example.org/imprint/", "region": "DE",
… …
} }
+1
View File
@@ -12,6 +12,7 @@ at :ref:`plugin-docs`.
organizers organizers
events events
subevents subevents
event_meta_properties
taxrules taxrules
categories categories
items items
+5
View File
@@ -65,6 +65,7 @@ valid_date_max date Maximum value f
valid_datetime_min datetime Minimum value for date and time questions (optional) valid_datetime_min datetime Minimum value for date and time questions (optional)
valid_datetime_max datetime Maximum value for date and time questions (optional) valid_datetime_max datetime Maximum value for date and time questions (optional)
valid_file_portrait boolean Turn on file validation for portrait photos valid_file_portrait boolean Turn on file validation for portrait photos
valid_string_length_min integer Minimum length for string questions (optional)
valid_string_length_max integer Maximum length for string questions (optional) valid_string_length_max integer Maximum length for string questions (optional)
dependency_question integer Internal ID of a different question. The current dependency_question integer Internal ID of a different question. The current
question will only be shown if the question given in question will only be shown if the question given in
@@ -130,6 +131,7 @@ Endpoints
"valid_date_max": null, "valid_date_max": null,
"valid_datetime_min": null, "valid_datetime_min": null,
"valid_datetime_max": null, "valid_datetime_max": null,
"valid_string_length_min": null,
"valid_string_length_max": null, "valid_string_length_max": null,
"valid_file_portrait": false, "valid_file_portrait": false,
"dependency_question": null, "dependency_question": null,
@@ -211,6 +213,7 @@ Endpoints
"valid_datetime_min": null, "valid_datetime_min": null,
"valid_datetime_max": null, "valid_datetime_max": null,
"valid_file_portrait": false, "valid_file_portrait": false,
"valid_string_length_min": null,
"valid_string_length_max": null, "valid_string_length_max": null,
"dependency_question": null, "dependency_question": null,
"dependency_value": null, "dependency_value": null,
@@ -315,6 +318,7 @@ Endpoints
"valid_datetime_min": null, "valid_datetime_min": null,
"valid_datetime_max": null, "valid_datetime_max": null,
"valid_file_portrait": false, "valid_file_portrait": false,
"valid_string_length_min": null,
"valid_string_length_max": null, "valid_string_length_max": null,
"options": [ "options": [
{ {
@@ -399,6 +403,7 @@ Endpoints
"valid_datetime_min": null, "valid_datetime_min": null,
"valid_datetime_max": null, "valid_datetime_max": null,
"valid_file_portrait": false, "valid_file_portrait": false,
"valid_string_length_min": null,
"valid_string_length_max": null, "valid_string_length_max": null,
"options": [ "options": [
{ {
+1
View File
@@ -116,6 +116,7 @@ Endpoints
:query integer page: The page number in case of a multi-page result set, default is 1 :query integer page: The page number in case of a multi-page result set, default is 1
:query string code: Only show the voucher with the given voucher code. :query string code: Only show the voucher with the given voucher code.
:query string search: Only show the voucher with the given query found in the code, tag, or comment.
:query integer max_usages: Only show vouchers with the given maximal number of usages. :query integer max_usages: Only show vouchers with the given maximal number of usages.
:query integer redeemed: Only show vouchers with the given number of redemptions. Note that this doesn't tell you if :query integer redeemed: Only show vouchers with the given number of redemptions. Note that this doesn't tell you if
the voucher can still be redeemed, as this also depends on ``max_usages``. See the the voucher can still be redeemed, as this also depends on ``max_usages``. See the
+58 -1
View File
@@ -8,7 +8,64 @@ import vuePug from 'eslint-plugin-vue-pug'
const ignores = globalIgnores([ const ignores = globalIgnores([
'**/node_modules', '**/node_modules',
'**/dist' '**/dist',
// Vendored code
'src/pretix/static/leaflet',
'src/pretix/static/clipboard',
'src/pretix/static/cropper',
'src/pretix/static/lightbox',
'src/pretix/static/are-you-sure',
'src/pretix/static/vuejs',
'src/pretix/static/fontawesome',
'src/pretix/static/typeahead',
'src/pretix/static/moment',
'src/pretix/static/pdfjs',
'src/pretix/static/sortable',
'src/pretix/static/iframeresizer',
'src/pretix/static/bootstrap',
'src/pretix/static/d3',
'src/pretix/static/jsi18n',
'src/pretix/static/fabric',
'src/pretix/static/datetimepicker',
'src/pretix/static/charts',
'src/pretix/static/fileupload',
'src/pretix/static/seating',
'src/pretix/static/rest_framework',
'src/pretix/static/select2',
'src/pretix/static/schema',
'src/pretix/static/slider',
'src/pretix/static/jquery',
'src/pretix/static/colorpicker',
'src/pretix/static/rrule',
'src/pretix/static/pretixcontrol/js/jquery.qrcode.min.js',
'src/pretix/static/pretixpresale/js/widget/docready.js',
// Pre-vue JS code
'src/pretix/static/pretixbase/js/addressform.js',
'src/pretix/static/pretixbase/js/asynctask.js',
'src/pretix/static/pretixbase/js/details.js',
'src/pretix/static/pretixbase/js/gettextstub.js',
'src/pretix/static/pretixbase/js/i18nstring.js',
'src/pretix/static/pretixcontrol/js/menu.js',
'src/pretix/static/pretixcontrol/js/ui/editor.js',
'src/pretix/static/pretixcontrol/js/ui/geo.js',
'src/pretix/static/pretixcontrol/js/ui/main.js',
'src/pretix/static/pretixcontrol/js/ui/plugins.js',
'src/pretix/static/pretixcontrol/js/ui/subevent.js',
'src/pretix/static/pretixcontrol/js/ui/variations.js',
'src/pretix/static/pretixcontrol/js/ui/webauthn.js',
'src/pretix/static/pretixpresale/js/ui/cart.js',
'src/pretix/static/pretixpresale/js/ui/main.js',
'src/pretix/static/pretixpresale/js/ui/questions.js',
'src/pretix/static/pretixpresale/js/widget/floatformat.js',
'src/pretix/static/pretixpresale/js/widget/widget.js',
'src/pretix/plugins/banktransfer/static',
'src/pretix/plugins/paypal2/static',
'src/pretix/plugins/statistics/static',
'src/pretix/plugins/stripe/static',
// Plugin checkouts
'local',
// docs
'doc',
]) ])
export default defineConfig([ export default defineConfig([
+27 -13
View File
@@ -294,29 +294,43 @@
} }
}, },
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": { "engines": {
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/@humanfs/node": { "node_modules/@humanfs/node": {
"version": "0.16.7", "version": "0.16.8",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@humanfs/core": "^0.19.1", "@humanfs/core": "^0.19.2",
"@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0" "@humanwhocodes/retry": "^0.4.0"
}, },
"engines": { "engines": {
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": { "node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -3380,9 +3394,9 @@
} }
}, },
"node_modules/postcss-selector-parser": { "node_modules/postcss-selector-parser": {
"version": "7.1.1", "version": "7.1.5",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz",
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4148,9 +4162,9 @@
} }
}, },
"node_modules/smol-toml": { "node_modules/smol-toml": {
"version": "1.6.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz",
"integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", "integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==",
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
+10 -10
View File
@@ -33,14 +33,14 @@ dependencies = [
"bleach==6.4.*", "bleach==6.4.*",
"celery==5.6.*", "celery==5.6.*",
"chardet==5.2.*", "chardet==5.2.*",
"cryptography>=50.0.0", "cryptography>=50.0.1",
"css-inline==0.21.*", "css-inline==0.21.*",
"defusedcsv>=3.0.0", "defusedcsv>=3.0.0",
"dnspython==2.*", "dnspython==2.*",
"Django[argon2]==5.2.*", "Django[argon2]==5.2.*,>=5.2.17",
"django-bootstrap3==26.2", "django-bootstrap3==26.2",
"django-compressor==4.6.0", "django-compressor==4.6.0",
"django-countries==9.0.*", "django-countries==9.1.*",
"django-filter==26.1", "django-filter==26.1",
"django-formset-js-improved==0.5.0.5", "django-formset-js-improved==0.5.0.5",
"django-formtools==2.7", "django-formtools==2.7",
@@ -56,7 +56,7 @@ dependencies = [
"django-querytagger==0.0.3", "django-querytagger==0.0.3",
"django-redis==7.0.*", "django-redis==7.0.*",
"django-scopes==2.1.*", "django-scopes==2.1.*",
"django-statici18n==2.7.*", "django-statici18n==2.8.*",
"djangorestframework==3.17.*", "djangorestframework==3.17.*",
"dnspython==2.8.*", "dnspython==2.8.*",
"drf_ujson2==1.7.*", "drf_ujson2==1.7.*",
@@ -75,16 +75,16 @@ dependencies = [
"packaging", "packaging",
"paypalrestsdk==1.13.*", "paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*", "paypal-checkout-serversdk==1.0.*",
"PyJWT==2.13.*", "PyJWT==2.14.*",
"phonenumberslite==9.0.*", "phonenumberslite==9.0.*",
"Pillow==12.3.*", "Pillow==12.3.*",
"pretix-plugin-build", "pretix-plugin-build",
"protobuf==7.35.*", "protobuf==7.36.*",
"psycopg2-binary", "psycopg2-binary",
"pycountry", "pycountry",
"pycparser==3.0", "pycparser==3.0",
"pycryptodome==3.23.*", "pycryptodome==3.23.*",
"pypdf==6.5.*", "pypdf==6.19.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab "python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*", "python-dateutil==2.9.*",
"pytz", "pytz",
@@ -94,7 +94,7 @@ dependencies = [
"redis==7.4.*", "redis==7.4.*",
"reportlab==5.0.*", "reportlab==5.0.*",
"requests==2.34.*", "requests==2.34.*",
"sentry-sdk==2.66.*", "sentry-sdk==2.69.*",
"sepaxml==2.7.*", "sepaxml==2.7.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",
@@ -112,10 +112,10 @@ dev = [
"aiohttp==3.14.*", "aiohttp==3.14.*",
"coverage", "coverage",
"coveralls", "coveralls",
"fakeredis==2.37.*", "fakeredis==2.38.*",
"flake8==7.3.*", "flake8==7.3.*",
"freezegun", "freezegun",
"isort==8.0.*", "isort==9.0.*",
"pep8-naming==0.15.*", "pep8-naming==0.15.*",
"potypo", "potypo",
"pytest-asyncio>=1.4.0", "pytest-asyncio>=1.4.0",
-1
View File
@@ -26,7 +26,6 @@ ignore =
src/tests/plugins/* src/tests/plugins/*
src/tests/plugins/badges/* src/tests/plugins/badges/*
src/tests/plugins/banktransfer/* src/tests/plugins/banktransfer/*
src/tests/plugins/paypal/*
src/tests/plugins/paypal2/* src/tests/plugins/paypal2/*
src/tests/plugins/pretixdroid/* src/tests/plugins/pretixdroid/*
src/tests/plugins/stripe/* src/tests/plugins/stripe/*
+3 -3
View File
@@ -115,7 +115,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:event.settings'), ('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'), ('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'), ('POST', 'api-v1:checkinrpc.redeem'),
('POST', 'api-v1:checkinrpc.annull'), ('POST', 'api-v1:checkinrpc.annul'),
('GET', 'api-v1:checkinrpc.search'), ('GET', 'api-v1:checkinrpc.search'),
('GET', 'api-v1:reusablemedium-list'), ('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-lookup'), ('POST', 'api-v1:reusablemedium-lookup'),
@@ -154,7 +154,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:event.settings'), ('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'), ('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'), ('POST', 'api-v1:checkinrpc.redeem'),
('POST', 'api-v1:checkinrpc.annull'), ('POST', 'api-v1:checkinrpc.annul'),
('GET', 'api-v1:checkinrpc.search'), ('GET', 'api-v1:checkinrpc.search'),
) )
@@ -191,7 +191,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:event.settings'), ('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'), ('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'), ('POST', 'api-v1:checkinrpc.redeem'),
('POST', 'api-v1:checkinrpc.annull'), ('POST', 'api-v1:checkinrpc.annul'),
('GET', 'api-v1:checkinrpc.search'), ('GET', 'api-v1:checkinrpc.search'),
) )
+1
View File
@@ -90,6 +90,7 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
answers = serializers.JSONField(required=False, allow_null=True) answers = serializers.JSONField(required=False, allow_null=True)
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES) exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
exchange_medium_identifier = serializers.CharField(required=False) exchange_medium_identifier = serializers.CharField(required=False)
simulate = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
+6 -2
View File
@@ -702,8 +702,12 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
return super().save(**kwargs) return super().save(**kwargs)
def validate_default(self, value): def validate_default(self, value):
if not value and self.instance.default: if not value:
raise ValidationError("You can't remove the default property, instead set it on another tax rule.") if self.instance:
if self.instance.default:
raise ValidationError("You can't remove the default property, instead set it on another tax rule.")
elif not self.context["event"].tax_rules.exists():
raise ValidationError("You can't remove the default property as there is only one tax rule.")
return value return value
+2 -2
View File
@@ -550,7 +550,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values', 'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min', 'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max', 'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
'valid_string_length_max', 'valid_file_portrait') 'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait')
def validate_identifier(self, value): def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance) Question._clean_identifier(self.context['event'], value, self.instance)
@@ -619,7 +619,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
options_data = validated_data.pop('options') if 'options' in validated_data else [] options_data = validated_data.pop('options') if 'options' in validated_data else []
items = validated_data.pop('items', []) items = validated_data.pop('items', [])
question = Question.objects.create(**validated_data) question = Question.objects.create(**validated_data, container_type=Question.ContainerType.ORDERPOSITION)
question.items.set(items) question.items.set(items)
for opt_data in options_data: for opt_data in options_data:
QuestionOption.objects.create(question=question, **opt_data) QuestionOption.objects.create(question=question, **opt_data)
+92 -3
View File
@@ -28,6 +28,7 @@ from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from i18nfield.rest_framework import I18nField
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@@ -40,9 +41,10 @@ from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region from pretix.base.i18n import get_language_without_region
from pretix.base.models import ( from pretix.base.models import (
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction, Customer, Device, EventMetaProperty, GiftCard, GiftCardAcceptance,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, GiftCardTransaction, Membership, MembershipType, OrderPosition, Organizer,
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User, ReusableMedium, SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User,
) )
from pretix.base.models.seating import SeatingPlanLayoutValidator from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.permissions import ( from pretix.base.permissions import (
@@ -426,6 +428,8 @@ class TeamSerializer(serializers.ModelSerializer):
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items(): for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
if full_data.get(k) is True: if full_data.get(k) is True:
data["limit_organizer_permissions"].update({kk: True for kk in v}) data["limit_organizer_permissions"].update({kk: True for kk in v})
for key in list(k for k in data if k.startswith("can_")):
del data[key]
if full_data.get('limit_events') and full_data.get('all_events'): if full_data.get('limit_events') and full_data.get('all_events'):
raise ValidationError('Do not set both limit_events and all_events.') raise ValidationError('Do not set both limit_events and all_events.')
@@ -638,3 +642,88 @@ class OrganizerSettingsSerializer(SettingsSerializer):
) )
# TODO: make sure pub is always correct # TODO: make sure pub is always correct
return 'pub/' + fname return 'pub/' + fname
class MetaPropertyListField(serializers.ListField):
def __init__(self, *args, **kwargs):
kwargs["validators"] = kwargs.pop("validators", [])
def validate_keys_unique(choices):
if not choices:
return
keys = [c.get("key") for c in choices]
if len(set(keys)) < len(keys):
raise ValidationError("The key for each meta property value option must be unique.")
kwargs["validators"].append(
validate_keys_unique
)
super().__init__(*args, **kwargs)
class MetaPropertyDictField(serializers.DictField):
def __init__(self, **kwargs):
self.label_child = kwargs.pop("label_child", I18nField())
super().__init__(**kwargs)
def to_representation(self, value):
# django added unneccessary keys DELETE, ORDER through formsets, filter them here for backwards compat
d = {
"key": value["key"]
}
if "label" in value:
d["label"] = self.label_child.to_representation(value["label"])
return super().to_representation(d)
def to_internal_value(self, data):
if not isinstance(data, dict):
raise ValidationError("Meta property value options must be a dict.")
if not isinstance(data.get("key"), str):
raise ValidationError("Meta property value options must have a key of type string.")
if any(k not in {"key", "label"} for k in data.keys()):
raise ValidationError("Meta property value options may only have a key and optionally a label.")
if "label" in data:
try:
data["label"] = self.label_child.to_internal_value(data["label"])
except ValidationError as e:
raise ValidationError({"label": e.detail})
return super().to_internal_value(data)
class EventMetaPropertiesSerializer(I18nAwareModelSerializer):
choices = MetaPropertyListField(
child=MetaPropertyDictField(
label_child=I18nField()
),
allow_null=True,
)
class Meta:
model = EventMetaProperty
fields = (
'id', 'name', 'default', 'required', 'protected', 'filter_public', 'public_label', 'filter_allowed',
'choices'
)
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)
choices = full_data.get("choices")
default = full_data.get("default")
if choices and default:
choice_keys = [c.get("key") for c in choices]
if default not in choice_keys:
raise ValidationError("You cannot set a default value that is not a valid value.")
if not choices and "choices" in data:
# normalize empty dict to None
data["choices"] = None
return data
+1
View File
@@ -68,6 +68,7 @@ orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportVie
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters') orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet) orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions') orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
orga_router.register(r'event_meta_properties', organizer.EventMetaPropertiesViewSet)
team_router = routers.DefaultRouter() team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet) team_router.register(r'members', organizer.TeamMemberViewSet)
+6
View File
@@ -839,6 +839,11 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
) )
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
if simulate:
raise CheckInError(
gettext('You cannot simulate a medium exchange.'),
'error'
)
with transaction.atomic(): with transaction.atomic():
# Do exchange and check-in atomically, i.e. both succeed or both fail # Do exchange and check-in atomically, i.e. both succeed or both fail
medium = perform_media_exchange( medium = perform_media_exchange(
@@ -1066,6 +1071,7 @@ class CheckinRPCRedeemView(views.APIView):
legacy_url_support=False, legacy_url_support=False,
exchange_medium_type=s.validated_data.get('exchange_medium_type'), exchange_medium_type=s.validated_data.get('exchange_medium_type'),
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'), exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
simulate=s.validated_data.get('simulate'),
) )
+4 -1
View File
@@ -475,7 +475,10 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
write_permission = 'event.items:write' write_permission = 'event.items:write'
def get_queryset(self): def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all() return self.request.event.questions.filter(
# the container_type parameter is undocumented, this API is going to change in a later release
container_type=self.request.GET.get('container_type', Question.ContainerType.ORDERPOSITION),
).prefetch_related('options').all()
@transaction.atomic() @transaction.atomic()
def perform_create(self, serializer): def perform_create(self, serializer):
+51 -4
View File
@@ -44,15 +44,16 @@ from pretix.api.models import OAuthAccessToken
from pretix.api.pagination import TotalOrderingFilter from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.organizer import ( from pretix.api.serializers.organizer import (
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer, CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer, EventMetaPropertiesSerializer, GiftCardSerializer,
GiftCardTransactionSerializer, MembershipSerializer,
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer, MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer, SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer, TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
) )
from pretix.base.models import ( from pretix.base.models import (
Customer, Device, Event, GiftCard, GiftCardTransaction, LogEntry, Customer, Device, Event, EventMetaProperty, GiftCard, GiftCardTransaction,
Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Team, LogEntry, Membership, MembershipType, Organizer, SalesChannel, SeatingPlan,
TeamAPIToken, TeamInvite, User, Team, TeamAPIToken, TeamInvite, User,
) )
from pretix.base.plugins import ( from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
@@ -846,3 +847,49 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
data={'id': instance.pk} data={'id': instance.pk}
) )
instance.delete() instance.delete()
class EventMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = EventMetaPropertiesSerializer
queryset = EventMetaProperty.objects.none()
write_permission = 'organizer.settings.general:write'
def get_queryset(self):
return self.request.organizer.meta_properties.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.property.deleted',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk}
)
instance.delete()
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer_id=self.request.organizer.pk)
serializer.instance.log_action(
'pretix.property.created',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(organizer_id=self.request.organizer.pk)
serializer.instance.log_action(
'pretix.property.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
+4
View File
@@ -40,6 +40,7 @@ with scopes_disabled():
class VoucherFilter(FilterSet): class VoucherFilter(FilterSet):
active = BooleanFilter(method='filter_active') active = BooleanFilter(method='filter_active')
code = CharFilter(lookup_expr='iexact') code = CharFilter(lookup_expr='iexact')
search = CharFilter(method='search_qs')
class Meta: class Meta:
model = Voucher model = Voucher
@@ -54,6 +55,9 @@ with scopes_disabled():
return queryset.filter(Q(redeemed__gte=F('max_usages')) | return queryset.filter(Q(redeemed__gte=F('max_usages')) |
(Q(valid_until__isnull=False) & Q(valid_until__lte=now()))) (Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
def search_qs(self, qs, name, value):
return qs.filter(Q(code__icontains=value) | Q(tag__icontains=value) | Q(comment__icontains=value))
class VoucherViewSet(viewsets.ModelViewSet): class VoucherViewSet(viewsets.ModelViewSet):
serializer_class = VoucherSerializer serializer_class = VoucherSerializer
+7 -3
View File
@@ -27,7 +27,7 @@ from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import List, Optional, Protocol from typing import List, Optional, Protocol
import sentry_sdk from django.conf import settings
from django.db import DatabaseError, transaction from django.db import DatabaseError, transaction
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -236,7 +236,9 @@ class OutboundSyncProvider:
# model changes saved by set_sync_error / clear_in_flight calls below # model changes saved by set_sync_error / clear_in_flight calls below
if sq.failed_attempts >= self.max_attempts: if sq.failed_attempts >= self.max_attempts:
logger.exception('Failed to sync order (max attempts exceeded)') logger.exception('Failed to sync order (max attempts exceeded)')
sentry_sdk.capture_exception(e) if settings.SENTRY_ENABLED:
import sentry_sdk
sentry_sdk.capture_exception(e)
sq.set_sync_error("exceeded", e.messages, e.full_message) sq.set_sync_error("exceeded", e.messages, e.full_message)
else: else:
logger.info( logger.info(
@@ -247,7 +249,9 @@ class OutboundSyncProvider:
sq.clear_in_flight() sq.clear_in_flight()
except Exception as e: except Exception as e:
logger.exception('Failed to sync order (unhandled exception)') logger.exception('Failed to sync order (unhandled exception)')
sentry_sdk.capture_exception(e) if settings.SENTRY_ENABLED:
import sentry_sdk
sentry_sdk.capture_exception(e)
sq.set_sync_error("internal", [], str(e)) sq.set_sync_error("internal", [], str(e))
@cached_property @cached_property
+27 -4
View File
@@ -54,6 +54,7 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable from ...helpers.iter import chunked_iterable
from ..exporter import BaseExporter, MultiSheetListExporter from ..exporter import BaseExporter, MultiSheetListExporter
from ..invoicing.transmission import get_transmission_types
from ..services.export import ExportError from ..services.export import ExportError
from ..services.invoices import invoice_pdf_task from ..services.invoices import invoice_pdf_task
from ..signals import ( from ..signals import (
@@ -197,7 +198,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
def iterate_sheet(self, form_data, sheet): def iterate_sheet(self, form_data, sheet):
_ = gettext _ = gettext
if sheet == 'invoices': if sheet == 'invoices':
yield [ headers = [
_('Invoice number'), _('Invoice number'),
_('Date'), _('Date'),
_('Order code'), _('Order code'),
@@ -230,8 +231,18 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Total value (without taxes)'), _('Total value (without taxes)'),
_('Payment matching IDs'), _('Payment matching IDs'),
_('Payment providers'), _('Payment providers'),
_('Transmission type'),
_('Transmission status'),
_('Transmission date'),
] ]
transmission_types = get_transmission_types()
for tt in transmission_types:
for c in tt.describe_info_columns():
headers.append(str(tt.verbose_name) + ': ' + str(c))
yield headers
p_providers = OrderPayment.objects.filter( p_providers = OrderPayment.objects.filter(
order=OuterRef('order'), order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED, state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -242,7 +253,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
'm' 'm'
).order_by() ).order_by()
base_qs = self.invoices_queryset(form_data)\ base_qs = self.invoices_queryset(form_data)
qs = base_qs.select_related( qs = base_qs.select_related(
'order', 'refers' 'order', 'refers'
@@ -280,7 +291,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
if mid: if mid:
pmis.append(mid) pmis.append(mid)
pmi = '\n'.join(pmis) pmi = '\n'.join(pmis)
yield [ line = [
i.full_invoice_no, i.full_invoice_no,
date_format(i.date, "SHORT_DATE_FORMAT"), date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code, i.order.code,
@@ -315,8 +326,20 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
', '.join([ ', '.join([
str(self.providers.get(p, p)) for p in sorted(set((i.payment_providers or '').split(','))) str(self.providers.get(p, p)) for p in sorted(set((i.payment_providers or '').split(',')))
if p and p != 'free' if p and p != 'free'
]) ]),
i.transmission_type_instance.verbose_name,
i.get_transmission_status_display(),
date_format(i.transmission_date, "SHORT_DATETIME_FORMAT") if i.transmission_date else "",
] ]
for tt in transmission_types:
if tt.identifier == i.transmission_type:
described = dict(tt.describe_info(i.invoice_to_transmission_info, i.invoice_to_country, i.invoice_to_is_business))
for c in tt.describe_info_columns():
line.append(described.get(c, ""))
else:
for c in tt.describe_info_columns():
line.append("")
yield line
elif sheet == 'lines': elif sheet == 'lines':
yield [ yield [
_('Invoice number'), _('Invoice number'),
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -82,8 +82,8 @@ class UserSettingsForm(forms.ModelForm):
class User2FADeviceAddForm(forms.Form): class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64) name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=( devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')), ('otp_totp.totpdevice', _('Smartphone with the Authenticator application')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')), ('pretixbase.webauthndevice', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
)) ))
+24 -18
View File
@@ -47,6 +47,7 @@ from pretix.helpers.i18n import (
get_format_without_seconds, get_javascript_format, get_format_without_seconds, get_javascript_format,
get_javascript_format_without_seconds, get_javascript_format_without_seconds,
) )
from pretix.helpers.safedownload import get_token
def replace_arabic_numbers(inp): def replace_arabic_numbers(inp):
@@ -157,36 +158,26 @@ class TimePickerWidget(forms.TimeInput):
class UploadedFileWidget(forms.ClearableFileInput): class UploadedFileWidget(forms.ClearableFileInput):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.position = kwargs.pop('position')
self.event = kwargs.pop('event')
self.answer = kwargs.pop('answer') self.answer = kwargs.pop('answer')
self.request = kwargs.pop('request')
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class FakeFile: class FakeFile:
def __init__(self, file, position, event, answer): def __init__(self, file, answer, request):
self.file = file self.file = file
self.position = position
self.event = event
self.answer = answer self.answer = answer
self.request = request
def __str__(self): def __str__(self):
return os.path.basename(self.file.name).split('.', 1)[-1] return os.path.basename(self.file.name).split('.', 1)[-1]
@property @property
def url(self): def url(self):
from pretix.base.models import OrderPosition token = get_token(self.request, self.answer)
from pretix.multidomain.urlreverse import eventreverse if self.request.resolver_match.namespace == 'control':
return self.answer.backend_file_url + '?token=' + token
if isinstance(self.position, OrderPosition):
return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={
'order': self.position.order.code,
'secret': self.position.order.secret,
'answer': self.answer.pk,
})
else: else:
return eventreverse(self.event, 'presale:event.cart.download.answer', kwargs={ return self.answer.frontend_file_url + '?token=' + token
'answer': self.answer.pk,
})
def get_context(self, name, value, attrs): def get_context(self, name, value, attrs):
# Browsers can't recognize that the server already has a file uploaded # Browsers can't recognize that the server already has a file uploaded
@@ -199,7 +190,7 @@ class UploadedFileWidget(forms.ClearableFileInput):
def format_value(self, value): def format_value(self, value):
if self.is_initial(value): if self.is_initial(value):
return self.FakeFile(value, self.position, self.event, self.answer) return self.FakeFile(value, self.answer, self.request)
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget): class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
@@ -314,3 +305,18 @@ class BusinessBooleanRadio(forms.RadioSelect):
'False': False, 'False': False,
False: False, False: False,
}.get(value) }.get(value)
class OptionAttrsSelect(forms.Select):
def __init__(self, *args, option_attrs=None, **kwargs):
super().__init__(*args, **kwargs)
self.option_attrs = option_attrs or {}
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
option = super().create_option(
name, value, label, selected, index, subindex=subindex, attrs=attrs
)
extra = self.option_attrs.get(str(value))
if extra:
option["attrs"].update(extra)
return option
@@ -107,6 +107,9 @@ class TransmissionType:
def transmission_info_to_form_data(self, transmission_info: dict) -> dict: def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
return transmission_info return transmission_info
def describe_info_columns(self):
return [f.label for f in self.invoice_address_form_fields.values()]
def describe_info(self, transmission_info: dict, country: Country, is_business: bool): def describe_info(self, transmission_info: dict, country: Country, is_business: bool):
form_data = self.transmission_info_to_form_data(transmission_info) form_data = self.transmission_info_to_form_data(transmission_info)
data = [] data = []
@@ -0,0 +1,63 @@
# Generated by Django 4.2.17 on 2025-01-01 20:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0307_devicelastseen"),
]
operations = [
migrations.CreateModel(
name="CheckoutSession",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("cart_id", models.CharField(max_length=255, unique=True)),
("created", models.DateTimeField(auto_now_add=True)),
("testmode", models.BooleanField(default=False)),
("session_data", models.JSONField(default=dict)),
(
"customer",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="checkout_sessions",
to="pretixbase.customer",
),
),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="checkout_sessions",
to="pretixbase.event",
),
),
(
"sales_channel",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.saleschannel",
),
),
],
),
migrations.AddField(
model_name="invoiceaddress",
name="checkout_session",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="invoice_address",
to="pretixbase.checkoutsession",
),
),
]
@@ -0,0 +1,33 @@
# Generated by Django 5.2.15 on 2026-08-07 20:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0308_checkoutsession_invoiceaddress_checkout_session'),
]
operations = [
migrations.AddField(
model_name='question',
name='container_type',
field=models.CharField(default='P', max_length=5),
),
migrations.AddField(
model_name='questionanswer',
name='checkoutsession',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.checkoutsession'),
),
migrations.AddField(
model_name='questionanswer',
name='order',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.order'),
),
migrations.AlterUniqueTogether(
name='questionanswer',
unique_together={('cartposition', 'question'), ('checkoutsession', 'question'), ('order', 'question'), ('orderposition', 'question')},
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.2.12 on 2026-08-19 11:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0309_alter_questionanswer_unique_together_and_more"),
]
operations = [
migrations.AddField(
model_name="question",
name="valid_string_length_min",
field=models.PositiveIntegerField(null=True),
),
]
@@ -0,0 +1,49 @@
# Generated by Django 5.2.17 on 2026-09-21 11:30
import django.db.models.deletion
from django.db import migrations, models
def fix_unshredded_invoices(apps, _):
Invoice = apps.get_model("pretixbase", "Invoice")
InvoiceLine = apps.get_model("pretixbase", "InvoiceLine")
ignore_fields = (
# bool/int fields are not listed and skipped automatically
'prefix', 'invoice_no', 'full_invoice_no', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_state', 'invoice_from_country', 'invoice_from_tax_id',
'invoice_from_vat_id', 'locale', 'payment_provider_stamp', 'footer_text', 'foreign_currency_display',
'foreign_currency_source', 'transmission_type', 'transmission_provider', 'transmission_status',
)
for i in Invoice.objects.filter(shredded=True):
for f in Invoice._meta.fields:
if f.name in ignore_fields:
continue
val = getattr(i, f.name, None)
if val and isinstance(val, str):
setattr(i, f.name, "█")
elif val and isinstance(val, list): # jsonfield
setattr(i, f.name, [])
elif val and isinstance(val, dict): # jsonfield
setattr(i, f.name, {"_shredded": True})
i.save()
InvoiceLine.objects.filter(
attendee_name__isnull=False,
invoice__shredded=True
).update(attendee_name="█")
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0310_question_valid_string_length_min"),
]
operations = [
migrations.RunPython(
fix_unshredded_invoices,
migrations.RunPython.noop,
),
]
+12
View File
@@ -33,6 +33,7 @@ from django.db.models.aggregates import Sum
from django.db.models.expressions import OuterRef, Subquery from django.db.models.expressions import OuterRef, Subquery
from django.db.models.functions.comparison import Coalesce from django.db.models.functions.comparison import Coalesce
from django.utils.crypto import get_random_string, salted_hmac from django.utils.crypto import get_random_string, salted_hmac
from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
@@ -409,6 +410,17 @@ class AttendeeProfile(models.Model):
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()]) return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])
@cached_property
def answers_key_to_index(self):
return {a.get('field_name'): i for i, a in enumerate(self.answers)}
def store_answer(self, answer_dict):
k = answer_dict['field_name']
if k in self.answers_key_to_index:
self.answers[self.answers_key_to_index[k]] = answer_dict
else:
self.answers.append(answer_dict)
def generate_client_id(): def generate_client_id():
return get_random_string(40) return get_random_string(40)
+1
View File
@@ -166,6 +166,7 @@ class Device(LoggedModel):
) )
security_profile = models.CharField( security_profile = models.CharField(
max_length=190, max_length=190,
verbose_name=_('Security profile'),
default='full', default='full',
null=True, null=True,
blank=False blank=False
+1 -1
View File
@@ -736,7 +736,7 @@ class Event(EventMixin, LoggedModel):
self.settings.mail_send_order_paid_attendee = True self.settings.mail_send_order_paid_attendee = True
self.settings.mail_send_order_approved_attendee = True self.settings.mail_send_order_approved_attendee = True
self.settings.mail_send_order_approved_free_attendee = True self.settings.mail_send_order_approved_free_attendee = True
self.settings.mail_text_download_reminder_attendee = True self.settings.mail_send_download_reminder_attendee = True
@property @property
def social_image(self): def social_image(self):
+22 -1
View File
@@ -50,7 +50,7 @@ from dateutil.tz import datetime_exists
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import ( from django.core.validators import (
MaxLengthValidator, MinValueValidator, RegexValidator, MaxLengthValidator, MinLengthValidator, MinValueValidator, RegexValidator,
) )
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
@@ -1606,6 +1606,9 @@ class Question(LoggedModel):
:param dependency_values: The values that `dependency_question` needs to be set to for this question to be applicable. :param dependency_values: The values that `dependency_question` needs to be set to for this question to be applicable.
:type dependency_values: list[str] :type dependency_values: list[str]
""" """
class ContainerType(models.TextChoices):
ORDER = "O", _("Order")
ORDERPOSITION = "P", _("Order position")
TYPE_NUMBER = "N" TYPE_NUMBER = "N"
TYPE_STRING = "S" TYPE_STRING = "S"
TYPE_TEXT = "T" TYPE_TEXT = "T"
@@ -1641,6 +1644,12 @@ class Question(LoggedModel):
related_name="questions", related_name="questions",
on_delete=models.CASCADE on_delete=models.CASCADE
) )
container_type = models.CharField(
max_length=5,
choices=ContainerType.choices,
verbose_name=_("Asked on"),
default=ContainerType.ORDERPOSITION,
)
question = I18nTextField( question = I18nTextField(
verbose_name=_("Question") verbose_name=_("Question")
) )
@@ -1722,6 +1731,11 @@ class Question(LoggedModel):
valid_datetime_max = models.DateTimeField(null=True, blank=True, valid_datetime_max = models.DateTimeField(null=True, blank=True,
verbose_name=_('Maximum value'), verbose_name=_('Maximum value'),
help_text=_('Currently not supported in our apps and during check-in')) help_text=_('Currently not supported in our apps and during check-in'))
valid_string_length_min = models.PositiveIntegerField(null=True, blank=True,
verbose_name=_('Minimum length'),
help_text=_(
'Currently not supported in our apps and during check-in'
))
valid_string_length_max = models.PositiveIntegerField(null=True, blank=True, valid_string_length_max = models.PositiveIntegerField(null=True, blank=True,
verbose_name=_('Maximum length'), verbose_name=_('Maximum length'),
help_text=_( help_text=_(
@@ -1876,6 +1890,11 @@ class Question(LoggedModel):
else: else:
raise ValidationError(_('Unknown country code.')) raise ValidationError(_('Unknown country code.'))
elif self.type in (Question.TYPE_STRING, Question.TYPE_TEXT): elif self.type in (Question.TYPE_STRING, Question.TYPE_TEXT):
if self.valid_string_length_min is not None and len(answer) < self.valid_string_length_min:
raise ValidationError(MinLengthValidator.message % {
'limit_value': self.valid_string_length_min,
'show_value': len(answer)
})
if self.valid_string_length_max is not None and len(answer) > self.valid_string_length_max: if self.valid_string_length_max is not None and len(answer) > self.valid_string_length_max:
raise ValidationError(MaxLengthValidator.message % { raise ValidationError(MaxLengthValidator.message % {
'limit_value': self.valid_string_length_max, 'limit_value': self.valid_string_length_max,
@@ -1897,6 +1916,8 @@ class Question(LoggedModel):
raise ValidationError(_("The maximum date must not be before the minimum value.")) raise ValidationError(_("The maximum date must not be before the minimum value."))
if self.valid_number_max and self.valid_number_min and self.valid_number_min > self.valid_number_max: if self.valid_number_max and self.valid_number_min and self.valid_number_min > self.valid_number_max:
raise ValidationError(_("The maximum value must not be lower than the minimum value.")) raise ValidationError(_("The maximum value must not be lower than the minimum value."))
if self.valid_string_length_max and self.valid_string_length_min and self.valid_string_length_min > self.valid_string_length_max:
raise ValidationError(_("The maximum length must not be shorter than the minimum length."))
super().clean() super().clean()
def clean_type_change(self, old_type, new_type): def clean_type_change(self, old_type, new_type):
+90 -48
View File
@@ -626,42 +626,14 @@ class Order(LockModel, LoggedModel):
self.save(update_fields=['last_modified']) self.save(update_fields=['last_modified'])
def set_expires(self, now_dt=None, subevents=None): def set_expires(self, now_dt=None, subevents=None):
now_dt = now_dt or now() from pretix.base.services.payment import compute_payment_deadline
tz = ZoneInfo(self.event.settings.timezone)
mode = self.event.settings.get('payment_term_mode')
if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if self.event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
elif mode == 'minutes':
exp_by_date = now_dt.astimezone(tz) + timedelta(minutes=self.event.settings.get('payment_term_minutes', as_type=int))
else:
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
self.expires = exp_by_date self.expires = compute_payment_deadline(
event=self.event,
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper) sales_channel=self.sales_channel,
if term_last: now_dt=now_dt,
if self.event.has_subevents and subevents: subevents=subevents,
terms = [ )
term_last.datetime(se).date()
for se in subevents
]
if not terms:
return
term_last = min(terms)
else:
term_last = term_last.datetime(self.event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < self.expires:
self.expires = term_last
@cached_property @cached_property
def tax_total(self): def tax_total(self):
@@ -1304,10 +1276,9 @@ class Order(LockModel, LoggedModel):
def answerfile_name(instance, filename: str) -> str: def answerfile_name(instance, filename: str) -> str:
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits) secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
event = (instance.cartposition if instance.cartposition else instance.orderposition.order).event
return 'cachedfiles/answers/{org}/{ev}/{secret}.{filename}'.format( return 'cachedfiles/answers/{org}/{ev}/{secret}.{filename}'.format(
org=event.organizer.slug, org=instance.event.organizer.slug,
ev=event.slug, ev=instance.event.slug,
secret=secret, secret=secret,
filename=escape_uri_path(filename), filename=escape_uri_path(filename),
) )
@@ -1336,6 +1307,14 @@ class QuestionAnswer(models.Model):
'CartPosition', null=True, blank=True, 'CartPosition', null=True, blank=True,
related_name='answers', on_delete=models.CASCADE related_name='answers', on_delete=models.CASCADE
) )
order = models.ForeignKey(
'Order', null=True, blank=True,
related_name='answers', on_delete=models.CASCADE
)
checkoutsession = models.ForeignKey(
'CheckoutSession', null=True, blank=True,
related_name='answers', on_delete=models.CASCADE
)
question = models.ForeignKey( question = models.ForeignKey(
Question, related_name='answers', on_delete=models.CASCADE Question, related_name='answers', on_delete=models.CASCADE
) )
@@ -1351,16 +1330,21 @@ class QuestionAnswer(models.Model):
objects = ScopedManager(organizer='question__event__organizer') objects = ScopedManager(organizer='question__event__organizer')
class Meta: class Meta:
unique_together = [['orderposition', 'question'], ['cartposition', 'question']] unique_together = [
['orderposition', 'question'],
['cartposition', 'question'],
['order', 'question'],
['checkoutsession', 'question'],
]
@property @property
def backend_file_url(self): def backend_file_url(self):
if self.file: if self.file:
if self.orderposition: if self.associated_order:
return reverse('control:event.order.download.answer', kwargs={ return reverse('control:event.order.download.answer', kwargs={
'code': self.orderposition.order.code, 'code': self.associated_order.code,
'event': self.orderposition.order.event.slug, 'event': self.associated_order.event.slug,
'organizer': self.orderposition.order.event.organizer.slug, 'organizer': self.associated_order.event.organizer.slug,
'answer': self.pk, 'answer': self.pk,
}) })
return "" return ""
@@ -1370,14 +1354,14 @@ class QuestionAnswer(models.Model):
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
if self.file: if self.file:
if self.orderposition: if self.associated_order:
url = eventreverse(self.orderposition.order.event, 'presale:event.order.download.answer', kwargs={ url = eventreverse(self.associated_order.event, 'presale:event.order.download.answer', kwargs={
'order': self.orderposition.order.code, 'order': self.associated_order.code,
'secret': self.orderposition.order.secret, 'secret': self.associated_order.secret,
'answer': self.pk, 'answer': self.pk,
}) })
else: else:
url = eventreverse(self.cartposition.event, 'presale:event.cart.download.answer', kwargs={ url = eventreverse(self.event, 'presale:event.cart.download.answer', kwargs={
'answer': self.pk, 'answer': self.pk,
}) })
@@ -1392,6 +1376,24 @@ class QuestionAnswer(models.Model):
def file_name(self): def file_name(self):
return self.file.name.split('.', 1)[-1] return self.file.name.split('.', 1)[-1]
@property
def associated_order(self):
if self.orderposition:
return self.orderposition.order
elif self.order:
return self.order
@property
def event(self):
if self.orderposition:
return self.orderposition.order.event
elif self.cartposition:
return self.cartposition.event
elif self.order:
return self.order.event
elif self.checkoutsession:
return self.checkoutsession.event
def __str__(self): def __str__(self):
return self.to_string(use_cached=True) return self.to_string(use_cached=True)
@@ -3177,6 +3179,39 @@ class Transaction(models.Model):
return self.tax_value_includes_rounding_correction * self.count return self.tax_value_includes_rounding_correction * self.count
class CheckoutSession(models.Model):
"""
A checkout session optionally bundles cart positions with additional information. This is historically
not required in pretix and currently only used in the Storefront API.
"""
event = models.ForeignKey(
Event,
verbose_name=_("Event"),
related_name="checkout_sessions",
on_delete=models.CASCADE,
)
cart_id = models.CharField(
max_length=255, unique=True,
verbose_name=_("Cart ID (e.g. session key)"),
)
created = models.DateTimeField(
verbose_name=_("Date"),
auto_now_add=True,
)
customer = models.ForeignKey(
Customer,
related_name='checkout_sessions',
null=True, blank=True,
on_delete=models.SET_NULL,
)
sales_channel = models.ForeignKey(
"SalesChannel",
on_delete=models.CASCADE,
)
testmode = models.BooleanField(default=False)
session_data = models.JSONField(default=dict)
class CartPosition(AbstractPosition): class CartPosition(AbstractPosition):
""" """
A cart position is similar to an order line, except that it is not A cart position is similar to an order line, except that it is not
@@ -3381,6 +3416,13 @@ class CartPosition(AbstractPosition):
class InvoiceAddress(models.Model): class InvoiceAddress(models.Model):
last_modified = models.DateTimeField(auto_now=True) last_modified = models.DateTimeField(auto_now=True)
checkout_session = models.OneToOneField(
CheckoutSession,
null=True,
blank=True,
related_name='invoice_address',
on_delete=models.CASCADE
)
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE) order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
customer = models.ForeignKey( customer = models.ForeignKey(
Customer, Customer,
+21 -4
View File
@@ -330,9 +330,24 @@ class BasePaymentProvider:
payment method. This returns ``False`` by default which is no guarantee that payment method. This returns ``False`` by default which is no guarantee that
aborting a pending payment can never happen, it just hides the frontend button aborting a pending payment can never happen, it just hides the frontend button
to avoid users accidentally committing double payments. to avoid users accidentally committing double payments.
If the decision doesn't depend on the specific payment, then only implementing
``abort_pending_allowed`` is enough, ``payment_abort_pending_allowed(payment: OrderPayment)``
is expected to take this into account.
As a consumer only evaluate ``payment_abort_pending_allowed(payment: OrderPayment)``
to check if aborting this pending payment is possible.
""" """
return False return False
def _payment_abort_pending_allowed(self, payment: OrderPayment) -> bool:
"""
Experimental: This might change during upcomming releases.
Whether or not a user can abort a payment in pending state to switch to another
payment method. This returns ``self.abort_pending_allowed`` by default which is
no guarantee that aborting a pending payment can never happen, it just hides the
frontend button to avoid users accidentally committing double payments.
"""
return self.abort_pending_allowed
@property @property
def requires_invoice_immediately(self): def requires_invoice_immediately(self):
""" """
@@ -1019,10 +1034,12 @@ class BasePaymentProvider:
On success, you should set ``payment.state = OrderPayment.PAYMENT_STATE_CANCELED`` (or call the super method). On success, you should set ``payment.state = OrderPayment.PAYMENT_STATE_CANCELED`` (or call the super method).
On failure, you should raise a PaymentException. On failure, you should raise a PaymentException.
""" """
if payment.state == OrderPayment.PAYMENT_STATE_PENDING and not self.abort_pending_allowed:
raise PaymentException(_( if payment.state == OrderPayment.PAYMENT_STATE_PENDING:
"This payment is already being processed and can not be canceled any more." if not self._payment_abort_pending_allowed(payment):
)) raise PaymentException(_(
"This payment is already being processed and cannot be canceled any more."
))
payment.state = OrderPayment.PAYMENT_STATE_CANCELED payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save(update_fields=['state']) payment.save(update_fields=['state'])
+40 -34
View File
@@ -801,6 +801,18 @@ def generate_compressed_addon_list(op, order, event, only_checked_in=False):
return addonlist return addonlist
def get_sizebox(page: pypdf.PageObject):
mediabox = page.mediabox
cropbox = page.cropbox
return pypdf.generic.RectangleObject((
max(mediabox[0], cropbox[0]),
max(mediabox[1], cropbox[1]),
min(mediabox[2], cropbox[2]),
min(mediabox[3], cropbox[3]),
))
class Renderer: class Renderer:
def __init__(self, event, layout, background_file): def __init__(self, event, layout, background_file):
@@ -1153,11 +1165,10 @@ class Renderer:
elif o['type'] == "poweredby": elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o) self._draw_poweredby(canvas, op, o)
if self.bg_pdf: if self.bg_pdf:
page_size = ( first_page = self.bg_pdf.pages[0]
self.bg_pdf.pages[0].mediabox[2] - self.bg_pdf.pages[0].mediabox[0], sizebox = get_sizebox(first_page)
self.bg_pdf.pages[0].mediabox[3] - self.bg_pdf.pages[0].mediabox[1] page_size = (sizebox.width, sizebox.height)
) if first_page.rotation in (90, 270):
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
# swap dimensions due to pdf being rotated # swap dimensions due to pdf being rotated
page_size = page_size[::-1] page_size = page_size[::-1]
canvas.setPageSize(page_size) canvas.setPageSize(page_size)
@@ -1232,9 +1243,7 @@ class Renderer:
for i, page in enumerate(fg_pdf.pages): for i, page in enumerate(fg_pdf.pages):
bg_page = self.bg_pdf.pages[i] bg_page = self.bg_pdf.pages[i]
_correct_page_media_box(bg_page) _merge_with_correct_page_media_box(output, page, bg_page)
page.merge_page(bg_page, over=False)
output.add_page(page)
# pdf_header is a string like "%pdf-X.X" # pdf_header is a string like "%pdf-X.X"
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]): if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
@@ -1299,39 +1308,36 @@ def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
bg_pdf.write(bg_filename) bg_pdf.write(bg_filename)
subprocess.run(pdftk_cmd, check=True, stdout=out_file) subprocess.run(pdftk_cmd, check=True, stdout=out_file)
else: else:
output = PdfWriter()
for i, page in enumerate(fg_pdf.pages): for i, page in enumerate(fg_pdf.pages):
bg_page = bg_pdf.pages[i] bg_page = bg_pdf.pages[i]
_correct_page_media_box(bg_page) _merge_with_correct_page_media_box(output, page, bg_page)
page.merge_page(bg_page, over=False)
# pdf_header is a string like "%pdf-X.X" # pdf_header is a string like "%pdf-X.X"
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]): output.pdf_header = (
fg_pdf.pdf_header = bg_pdf.pdf_header bg_pdf.pdf_header
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:])
fg_pdf.write(out_file) else fg_pdf.pdf_header
)
output.write(out_file)
def _correct_page_media_box(page: pypdf.PageObject): def _merge_with_correct_page_media_box(output: pypdf.PdfWriter, fg_page: pypdf.PageObject, bg_page: pypdf.PageObject):
if page.rotation != 0: """
page.transfer_rotation_to_content() Adds fg_page to output, merging bg_page behind it.
media_box = page.mediabox
If bg_page has a non-zero mergebox/cropbox or is rotated via /Rotate, a transformation is applied to fix this."""
trsf = pypdf.Transformation() trsf = pypdf.Transformation()
if media_box.bottom != 0: if bg_page.rotation != 0:
trsf = trsf.translate(0, -media_box.bottom) trsf = trsf.rotate(-bg_page.rotation)
if media_box.left != 0:
trsf = trsf.translate(-media_box.left, 0) mb = get_sizebox(bg_page)
page.add_transformation(trsf, False) pt1 = trsf.apply_on(mb.lower_left)
for b in ["/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"]: pt2 = trsf.apply_on(mb.upper_right)
if b in page: trsf = trsf.translate(-min(pt1[0], pt2[0]), -min(pt1[1], pt2[1]))
rr = pypdf.generic.RectangleObject(page[b])
pt1 = trsf.apply_on(rr.lower_left) fg_page = output.add_page(fg_page)
pt2 = trsf.apply_on(rr.upper_right) fg_page.merge_transformed_page(bg_page, trsf, over=False, expand=False)
page[pypdf.generic.NameObject(b)] = pypdf.generic.RectangleObject((
min(pt1[0], pt2[0]),
min(pt1[1], pt2[1]),
max(pt1[0], pt2[0]),
max(pt1[1], pt2[1]),
))
@deconstructible @deconstructible
+373 -155
View File
@@ -19,30 +19,245 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
import copy
import datetime import datetime
import os
import warnings
from collections import namedtuple from collections import namedtuple
from typing import Union from dataclasses import dataclass
from typing import (
TYPE_CHECKING, Iterable, List, Literal, Optional, Tuple, Union, cast,
)
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from dateutil import parser from dateutil import parser
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.forms.widgets import Widget
from django.utils.formats import get_format from django.utils.formats import get_format
from django.utils.functional import lazy from django.utils.functional import Promise, lazy
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
BASE_CHOICES = ( from pretix.base.forms.widgets import OptionAttrsSelect
('date_from', _('Event start')),
('date_to', _('Event end')),
('date_admission', _('Event admission')),
('presale_start', _('Presale start')),
('presale_end', _('Presale end')),
)
RelativeDate = namedtuple('RelativeDate', ['days', 'minutes', 'time', 'is_after', 'base_date_name'], defaults=(0, None, None, False, 'date_from')) if TYPE_CHECKING:
from .models import Event, Order, SubEvent
@dataclass(frozen=True)
class BaseChoice:
base: Literal["event", "order"]
attribute: str
text: Promise
supports_before: bool
supports_after: bool
@property
def key(self) -> str:
key = f"{self.base}__{self.attribute}"
return key
@staticmethod
def find(objects: Iterable["BaseChoice"], key: str) -> "BaseChoice":
if "__" in key:
choice = next((obj for obj in objects if obj.key == key), None)
else:
# fallback for RelativeDateFields stored, before support for bases other than event was added
choice = next((obj for obj in objects if obj.attribute == key and obj.base == "event"), None)
if choice is None:
raise TypeError(f"key {key} must be a valid key in BASE_CHOICES")
return choice
BASE_CHOICES: List[BaseChoice] = [
BaseChoice('event', 'date_from', _('Event start'), True, True),
BaseChoice('event', 'date_to', _('Event end'), True, True),
BaseChoice('event', 'date_admission', _('Event admission'), True, True),
BaseChoice('event', 'presale_start', _('Presale start'), True, True),
BaseChoice('event', 'presale_end', _('Presale end'), True, True),
BaseChoice('order', 'datetime', _('Order creation'), False, True),
BaseChoice('order', 'expires', _('Order expiry'), True, True),
]
EVENT_BASE_CHOICES = [
x for x in BASE_CHOICES if x.base == 'event'
]
ORDER_BASE_CHOICES = [
x for x in BASE_CHOICES if x.base == 'order'
]
@dataclass(frozen=True)
class RelativeDate:
"""
This contains information on a date that is defined in relation to a fixed base point.
This means that the underlying data is a fixed date as the base point and a number of days or a time interval
to calculate the date.
The list of valid base date choices is defined in BASE_CHOICES.
If the base_date_name is not set, the date_from attribute of Event is used.
"""
days: int = 0
minutes: Optional[int] = None
time: Optional[datetime.time] = None
is_after: bool = False
base_date_name: str = 'event__date_from__'
def __post_init__(self) -> None:
if self.is_after and not self._choice.supports_after:
raise ValueError(
"The selected base date and attribute combination does not support relative dates placed after the base date"
)
if not self.is_after and not self._choice.supports_before:
raise ValueError(
"The selected base date and attribute combination does not support relative dates placed before the base date")
@property
def _choice(self):
return BaseChoice.find(BASE_CHOICES, self.base_date_name)
@property
def key(self):
return self._choice.key
def __eq__(self, o: object) -> bool:
if not isinstance(o, RelativeDate):
return False
return self.to_string() == o.to_string()
def _resolve_date(self, base: "Event | Order | SubEvent") -> Tuple[datetime.datetime, ZoneInfo]:
"""
Resolves the datetime and timezone information of this RelativeDate object in relation to the provided reference.
:param base: the reference which should be used to resolve the relative date
:return:
"""
from .models import Event, Order, SubEvent
choice = self._choice
if choice.base == "order" and isinstance(base, Order):
event = base.event
base_date = getattr(base, choice.attribute)
elif choice.base == "event" and isinstance(base, SubEvent):
event = base.event
base_date = (getattr(base, choice.attribute) or
getattr(base.event, choice.attribute) or
base.date_from)
elif choice.base == "event" and isinstance(base, Event):
event = base
base_date = getattr(base, choice.attribute) or event.date_from
else:
raise TypeError("The base defined by data does not match the passed in base")
tz = ZoneInfo(event.settings.timezone)
return base_date, tz
def date(self, base: "Event | Order | SubEvent") -> datetime.date:
"""
Resolves the effective date of this RelativeDate object in relation to the provided reference.
:param base: the reference which should be used to resolve the date
:return: datetime.date
"""
if self.minutes is not None:
raise ValueError('A minute-based relative datetime can not be used as a date')
base_date, tz = self._resolve_date(base)
if self.is_after:
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.days)
else:
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.days)
return new_date.date()
def datetime(self, base: "Event | Order | SubEvent") -> datetime.datetime:
"""
Resolves the effective datetime of this RelativeDate object in relation to the provided reference.
:param base: the reference which should be used to resolve the datetime
:return: datetime.datetime
"""
base_date, tz = self._resolve_date(base)
if self.minutes is not None:
if self.is_after:
return base_date.astimezone(tz) + datetime.timedelta(minutes=self.minutes)
else:
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.minutes)
else:
if self.is_after:
new_date = (base_date.astimezone(tz) + datetime.timedelta(days=self.days)).astimezone(tz)
else:
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.days)).astimezone(tz)
if self.time:
new_date = new_date.replace(
hour=self.time.hour,
minute=self.time.minute,
second=self.time.second
)
new_date = new_date.astimezone(tz)
return new_date
def to_string(self) -> str:
if self.minutes is not None:
return 'RELDATE/minutes/{}/{}/{}'.format( #
self.minutes,
self._choice.key,
'after' if self.is_after else '',
)
return 'RELDATE/{}/{}/{}/{}'.format( #
self.days,
self.time.strftime('%H:%M:%S') if self.time else '-',
self._choice.key,
'after' if self.is_after else '',
)
@classmethod
def from_string(cls, input: str):
if not input.startswith('RELDATE/'):
raise TypeError("Invalid input for RelativeDate.from_string()")
parts = input.split('/')
if parts[1] == 'minutes':
data = RelativeDate(
days=0,
minutes=int(parts[2]),
base_date_name=parts[3],
time=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
else:
if parts[2] == '-':
time = None
else:
timeparts = parts[2].split(':')
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
try:
data = RelativeDate(
days=int(parts[1] or 0),
base_date_name=parts[3],
time=time,
minutes=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
except ValueError:
data = RelativeDate(
days=0,
base_date_name=parts[3],
time=time,
minutes=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
return data
class RelativeDateWrapper: class RelativeDateWrapper:
@@ -50,130 +265,53 @@ class RelativeDateWrapper:
This contains information on a date that might be relative to an event. This means This contains information on a date that might be relative to an event. This means
that the underlying data is either a fixed date or a number of days and a wall clock that the underlying data is either a fixed date or a number of days and a wall clock
time to calculate the date based on a base point. time to calculate the date based on a base point.
The base point can be the date_from, date_to, date_admission, presale_start or presale_end
attribute of an event or subevent. If the respective attribute is not set, ``date_from``
will be used.
""" """
def __init__(self, data: Union[datetime.datetime, RelativeDate]): def __init__(self, data: Union[datetime.datetime, RelativeDate]):
self.data = data self.data = data
def date(self, event) -> datetime.date: def date(self, base: "Event | Order | SubEvent") -> datetime.date:
from .models import SubEvent """
If the RelativeDateWrapper wraps a RelativeDate object:
Resolves the effective date of this object in relation to the provided reference.
If the RelativeDateWrapper wraps an absolute date or datetime:
Returns the wrapped absolute date.
:param base: the reference which should be used to resolve the date in case it is a relative date.
:return: datetime.date
"""
if isinstance(self.data, datetime.datetime): if isinstance(self.data, datetime.datetime):
return self.data.date() return self.data.date()
elif isinstance(self.data, datetime.date): elif isinstance(self.data, datetime.date):
return self.data return self.data
else: else:
if self.data.minutes is not None: return self.data.date(base)
raise ValueError('A minute-based relative datetime can not be used as a date')
tz = ZoneInfo(event.settings.timezone) def datetime(self, base: "Event | Order | SubEvent") -> datetime.datetime:
if isinstance(event, SubEvent): """
base_date = ( If the RelativeDateWrapper wraps a RelativeDate object:
getattr(event, self.data.base_date_name) Resolves the effective datetime of this object in relation to the provided reference.
or getattr(event.event, self.data.base_date_name) If the RelativeDateWrapper wraps an absolute date or datetime:
or event.date_from Returns the wrapped absolute datetime.
)
else:
base_date = getattr(event, self.data.base_date_name) or event.date_from
if self.data.is_after:
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)
else:
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)
return new_date.date()
def datetime(self, event) -> datetime.datetime:
from .models import SubEvent
:param base: the reference which should be used to resolve the datetime in case it is a relative datetime.
:return: datetime.datetime
"""
if isinstance(self.data, (datetime.datetime, datetime.date)): if isinstance(self.data, (datetime.datetime, datetime.date)):
return self.data return self.data
else: else:
tz = ZoneInfo(event.settings.timezone) return self.data.datetime(base)
if isinstance(event, SubEvent):
base_date = (
getattr(event, self.data.base_date_name)
or getattr(event.event, self.data.base_date_name)
or event.date_from
)
else:
base_date = getattr(event, self.data.base_date_name) or event.date_from
if self.data.minutes is not None:
if self.data.is_after:
return base_date.astimezone(tz) + datetime.timedelta(minutes=self.data.minutes)
else:
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.data.minutes)
else:
if self.data.is_after:
new_date = (base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)).astimezone(tz)
else:
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)).astimezone(tz)
if self.data.time:
new_date = new_date.replace(
hour=self.data.time.hour,
minute=self.data.time.minute,
second=self.data.time.second
)
new_date = new_date.astimezone(tz)
return new_date
def to_string(self) -> str: def to_string(self) -> str:
if isinstance(self.data, (datetime.datetime, datetime.date)): if isinstance(self.data, (datetime.datetime, datetime.date)):
return self.data.isoformat() return self.data.isoformat()
else: else:
if self.data.minutes is not None: return self.data.to_string()
return 'RELDATE/minutes/{}/{}/{}'.format( #
self.data.minutes,
self.data.base_date_name,
'after' if self.data.is_after else '',
)
return 'RELDATE/{}/{}/{}/{}'.format( #
self.data.days,
self.data.time.strftime('%H:%M:%S') if self.data.time else '-',
self.data.base_date_name,
'after' if self.data.is_after else '',
)
@classmethod @classmethod
def from_string(cls, input: str): def from_string(cls, input: str):
if input.startswith('RELDATE/'): if input.startswith('RELDATE/'):
parts = input.split('/') data = RelativeDate.from_string(input)
if parts[1] == 'minutes':
data = RelativeDate(
days=0,
minutes=int(parts[2]),
base_date_name=parts[3],
time=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
else:
if parts[2] == '-':
time = None
else:
timeparts = parts[2].split(':')
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
try:
data = RelativeDate(
days=int(parts[1] or 0),
base_date_name=parts[3],
time=time,
minutes=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
except ValueError:
data = RelativeDate(
days=0,
base_date_name=parts[3],
time=time,
minutes=None,
is_after=len(parts) > 4 and parts[4] == "after",
)
if data.base_date_name not in [k[0] for k in BASE_CHOICES]:
raise ValueError('{} is not a valid base date'.format(data.base_date_name))
else: else:
data = parser.parse(input) data = parser.parse(input)
return RelativeDateWrapper(data) return RelativeDateWrapper(data)
@@ -187,7 +325,6 @@ BEFORE_AFTER_CHOICE = (
('after', _('after')), ('after', _('after')),
) )
reldatetimeparts = namedtuple('reldatetimeparts', ( reldatetimeparts = namedtuple('reldatetimeparts', (
"status", # 0 "status", # 0
"absolute", # 1 "absolute", # 1
@@ -202,6 +339,14 @@ reldatetimeparts = namedtuple('reldatetimeparts', (
reldatetimeparts.indizes = reldatetimeparts(*range(9)) reldatetimeparts.indizes = reldatetimeparts(*range(9))
def _get_choices(base_choices: List[BaseChoice]) -> List[Tuple[str, Promise]]:
return [(c.key, c.text) for c in base_choices]
def _get_choice_validation_obj(choices: List[BaseChoice]):
return {c.key: {"data-supports-before": c.supports_before, "data-supports-after": c.supports_after} for c in choices}
class RelativeDateTimeWidget(forms.MultiWidget): class RelativeDateTimeWidget(forms.MultiWidget):
template_name = 'pretixbase/forms/widgets/reldatetime.html' template_name = 'pretixbase/forms/widgets/reldatetime.html'
parts = reldatetimeparts parts = reldatetimeparts
@@ -209,6 +354,7 @@ class RelativeDateTimeWidget(forms.MultiWidget):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.status_choices = kwargs.pop('status_choices') self.status_choices = kwargs.pop('status_choices')
base_choices = kwargs.pop('base_choices') base_choices = kwargs.pop('base_choices')
choices = _get_choices(base_choices)
def placeholder_datetime_format(): def placeholder_datetime_format():
df = get_format('DATETIME_INPUT_FORMATS')[0] df = get_format('DATETIME_INPUT_FORMATS')[0]
@@ -220,21 +366,29 @@ class RelativeDateTimeWidget(forms.MultiWidget):
tf = get_format('TIME_INPUT_FORMATS')[0] tf = get_format('TIME_INPUT_FORMATS')[0]
return datetime.time(8, 30, 0).strftime(tf) return datetime.time(8, 30, 0).strftime(tf)
widgets = reldatetimeparts( widgets = cast(dict[str, Widget | type[Widget]], cast(object, reldatetimeparts(
status=forms.RadioSelect(choices=self.status_choices), status=forms.RadioSelect(choices=self.status_choices),
absolute=forms.DateTimeInput( absolute=forms.DateTimeInput(
attrs={'placeholder': lazy(placeholder_datetime_format, str), 'class': 'datetimepicker'} attrs={'placeholder': lazy(placeholder_datetime_format, str), 'class': 'datetimepicker'}
), ),
rel_days_number=forms.NumberInput(), rel_days_number=forms.NumberInput(),
rel_mins_relationto=forms.Select(choices=base_choices), rel_mins_relationto=OptionAttrsSelect(
attrs={'data-relative-choice': True},
choices=choices,
option_attrs=_get_choice_validation_obj(base_choices)
),
rel_days_timeofday=forms.TimeInput( rel_days_timeofday=forms.TimeInput(
attrs={'placeholder': lazy(placeholder_time_format, str), 'class': 'timepickerfield'} attrs={'placeholder': lazy(placeholder_time_format, str), 'class': 'timepickerfield'}
), ),
rel_mins_number=forms.NumberInput(), rel_mins_number=forms.NumberInput(),
rel_days_relationto=forms.Select(choices=base_choices), rel_days_relationto=OptionAttrsSelect(
rel_mins_relation=forms.Select(choices=BEFORE_AFTER_CHOICE), attrs={'data-relative-choice': True},
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE), choices=choices,
) option_attrs=_get_choice_validation_obj(base_choices)
),
rel_mins_relation=forms.Select(attrs={'data-relation-choice': True}, choices=BEFORE_AFTER_CHOICE),
rel_days_relation=forms.Select(attrs={'data-relation-choice': True}, choices=BEFORE_AFTER_CHOICE),
)))
super().__init__(widgets=widgets, *args, **kwargs) super().__init__(widgets=widgets, *args, **kwargs)
def decompress(self, value): def decompress(self, value):
@@ -247,10 +401,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
status="unset", status="unset",
absolute=None, absolute=None,
rel_days_number=1, rel_days_number=1,
rel_mins_relationto="date_from", rel_mins_relationto="event__date_from",
rel_days_timeofday=None, rel_days_timeofday=None,
rel_mins_number=0, rel_mins_number=0,
rel_days_relationto="date_from", rel_days_relationto="event__date_from",
rel_mins_relation="before", rel_mins_relation="before",
rel_days_relation="before" rel_days_relation="before"
) )
@@ -259,10 +413,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
status="absolute", status="absolute",
absolute=value.data, absolute=value.data,
rel_days_number=1, rel_days_number=1,
rel_mins_relationto="date_from", rel_mins_relationto="event__date_from",
rel_days_timeofday=None, rel_days_timeofday=None,
rel_mins_number=0, rel_mins_number=0,
rel_days_relationto="date_from", rel_days_relationto="event__date_from",
rel_mins_relation="before", rel_mins_relation="before",
rel_days_relation="before" rel_days_relation="before"
) )
@@ -271,10 +425,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
status="relative_minutes", status="relative_minutes",
absolute=None, absolute=None,
rel_days_number=None, rel_days_number=None,
rel_mins_relationto=value.data.base_date_name, rel_mins_relationto=value.data.key,
rel_days_timeofday=None, rel_days_timeofday=None,
rel_mins_number=value.data.minutes, rel_mins_number=value.data.minutes,
rel_days_relationto=value.data.base_date_name, rel_days_relationto=value.data.key,
rel_mins_relation="after" if value.data.is_after else "before", rel_mins_relation="after" if value.data.is_after else "before",
rel_days_relation="after" if value.data.is_after else "before" rel_days_relation="after" if value.data.is_after else "before"
) )
@@ -282,10 +436,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
status="relative", status="relative",
absolute=None, absolute=None,
rel_days_number=value.data.days, rel_days_number=value.data.days,
rel_mins_relationto=value.data.base_date_name, rel_mins_relationto=value.data.key,
rel_days_timeofday=value.data.time, rel_days_timeofday=value.data.time,
rel_mins_number=0, rel_mins_number=0,
rel_days_relationto=value.data.base_date_name, rel_days_relationto=value.data.key,
rel_mins_relation="after" if value.data.is_after else "before", rel_mins_relation="after" if value.data.is_after else "before",
rel_days_relation="after" if value.data.is_after else "before" rel_days_relation="after" if value.data.is_after else "before"
) )
@@ -309,17 +463,41 @@ class RelativeDateTimeField(forms.MultiValueField):
('relative', _('Relative date:')), ('relative', _('Relative date:')),
('relative_minutes', _('Relative time:')), ('relative_minutes', _('Relative time:')),
] ]
self.relative_to_order = kwargs.pop('relative_to_order', False)
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
if self.relative_to_order:
possible_choices.extend(ORDER_BASE_CHOICES)
if kwargs.get('limit_choices'): if kwargs.get('limit_choices'):
limit = kwargs.pop('limit_choices') limit = kwargs.pop('limit_choices')
choices = [(k, v) for k, v in BASE_CHOICES if k in limit] if any(["__" not in l for l in limit]):
else: _warn_skips = (str(os.path.dirname(__file__)),)
choices = BASE_CHOICES
warnings.warn(
message="Please prefix limit_choices with the base the attributes refer to, for example event__date_from",
category=DeprecationWarning,
skip_file_prefixes=_warn_skips
)
possible_choices = [
c for c in possible_choices if
# new base case as we want limit_choices to be expressed as base__attribute
(c.key in limit) or
# fallback for old event based entries
# if the base is an event, then using only attribute is fine
(c.base == "event" and c.attribute in limit)
]
if not kwargs.get('required', True): if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set'))) status_choices.insert(0, ('unset', _('Not set')))
choices = _get_choices(possible_choices)
fields = reldatetimeparts( fields = reldatetimeparts(
status=forms.ChoiceField( status=forms.ChoiceField(
choices=status_choices, choices=status_choices,
required=True required=True,
), ),
absolute=forms.DateTimeField( absolute=forms.DateTimeField(
required=False required=False
@@ -329,7 +507,7 @@ class RelativeDateTimeField(forms.MultiValueField):
), ),
rel_mins_relationto=forms.ChoiceField( rel_mins_relationto=forms.ChoiceField(
choices=choices, choices=choices,
required=False required=False,
), ),
rel_days_timeofday=forms.TimeField( rel_days_timeofday=forms.TimeField(
required=False, required=False,
@@ -339,7 +517,7 @@ class RelativeDateTimeField(forms.MultiValueField):
), ),
rel_days_relationto=forms.ChoiceField( rel_days_relationto=forms.ChoiceField(
choices=choices, choices=choices,
required=False required=False,
), ),
rel_mins_relation=forms.ChoiceField( rel_mins_relation=forms.ChoiceField(
choices=BEFORE_AFTER_CHOICE, choices=BEFORE_AFTER_CHOICE,
@@ -350,8 +528,9 @@ class RelativeDateTimeField(forms.MultiValueField):
required=False required=False
), ),
) )
if 'widget' not in kwargs: if 'widget' not in kwargs:
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=choices) kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=possible_choices)
kwargs.pop('max_length', 0) kwargs.pop('max_length', 0)
kwargs.pop('empty_value', 0) kwargs.pop('empty_value', 0)
super().__init__( super().__init__(
@@ -359,21 +538,24 @@ class RelativeDateTimeField(forms.MultiValueField):
) )
def set_event(self, event): def set_event(self, event):
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = [ possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None) if self.relative_to_order:
] possible_choices.extend(ORDER_BASE_CHOICES)
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = [
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None) possible_choices = possible_choices
] choices = _get_choices(possible_choices)
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = choices
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = choices
def compress(self, data_list): def compress(self, data_list):
if not data_list: if not data_list:
return None return None
data = reldatetimeparts(*data_list) data = reldatetimeparts(*data_list)
if data.status == 'absolute': if data.status == 'unset':
return RelativeDateWrapper(data.absolute)
elif data.status == 'unset':
return None return None
elif data.status == 'absolute':
return RelativeDateWrapper(data.absolute)
elif data.status == 'relative_minutes': elif data.status == 'relative_minutes':
return RelativeDateWrapper(RelativeDate( return RelativeDateWrapper(RelativeDate(
days=0, days=0,
@@ -404,6 +586,18 @@ class RelativeDateTimeField(forms.MultiValueField):
raise ValidationError(self.error_messages['incomplete']) raise ValidationError(self.error_messages['incomplete'])
elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto): elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto):
raise ValidationError(self.error_messages['incomplete']) raise ValidationError(self.error_messages['incomplete'])
elif data.status == 'relative':
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
if data.rel_days_relation == "before" and not choice.supports_before:
raise ValidationError(_('A relative date cannot be expressed as "before" for "{}"'.format(choice.text)))
elif data.status == 'relative' and data.rel_days_relation == "after" and not choice.supports_after:
raise ValidationError(_('A relative date cannot be expressed as "after" for "{}"'.format(choice.text)))
elif data.status == 'relative_minutes':
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
if data.rel_days_relation == "before" and not choice.supports_before:
raise ValidationError(_('A relative time cannot be expressed as "before" for "{}"'.format(choice.text)))
elif data.rel_days_relation == "after" and not choice.supports_after:
raise ValidationError(_('A relative time cannot be expressed as "after" for "{}"'.format(choice.text)))
return super().clean(value) return super().clean(value)
@@ -424,15 +618,20 @@ class RelativeDateWidget(RelativeDateTimeWidget):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.status_choices = kwargs.pop('status_choices') self.status_choices = kwargs.pop('status_choices')
widgets = reldateparts( self.base_choices = kwargs.pop('base_choices')
widgets = cast(dict[str, Widget | type[Widget]], cast(object, reldateparts(
status=forms.RadioSelect(choices=self.status_choices), status=forms.RadioSelect(choices=self.status_choices),
absolute=forms.DateInput( absolute=forms.DateInput(
attrs={'class': 'datepickerfield'} attrs={'class': 'datepickerfield'}
), ),
rel_days_number=forms.NumberInput(), rel_days_number=forms.NumberInput(),
rel_days_relationto=forms.Select(choices=kwargs.pop('base_choices')), rel_days_relationto=OptionAttrsSelect(
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE), choices=self.base_choices,
) option_attrs=_get_choice_validation_obj(self.base_choices),
attrs={'data-relative-choice': True},
),
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE, attrs={'data-relation-choice': True},),
)))
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs) forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
def decompress(self, value): def decompress(self, value):
@@ -460,7 +659,7 @@ class RelativeDateWidget(RelativeDateTimeWidget):
status="relative", status="relative",
absolute=None, absolute=None,
rel_days_number=value.data.days, rel_days_number=value.data.days,
rel_days_relationto=value.data.base_date_name, rel_days_relationto=value.data.key,
rel_days_relation="after" if value.data.is_after else "before" rel_days_relation="after" if value.data.is_after else "before"
) )
@@ -474,6 +673,15 @@ class RelativeDateField(RelativeDateTimeField):
] ]
if not kwargs.get('required', True): if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set'))) status_choices.insert(0, ('unset', _('Not set')))
self.relative_to_order = kwargs.pop('relative_to_order', False)
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
if self.relative_to_order:
possible_choices.extend(ORDER_BASE_CHOICES)
choices = _get_choices(possible_choices)
fields = reldateparts( fields = reldateparts(
status=forms.ChoiceField( status=forms.ChoiceField(
choices=status_choices, choices=status_choices,
@@ -486,33 +694,37 @@ class RelativeDateField(RelativeDateTimeField):
required=False required=False
), ),
rel_days_relationto=forms.ChoiceField( rel_days_relationto=forms.ChoiceField(
choices=BASE_CHOICES, choices=choices,
required=False required=False,
), ),
rel_days_relation=forms.ChoiceField( rel_days_relation=forms.ChoiceField(
choices=BEFORE_AFTER_CHOICE, choices=BEFORE_AFTER_CHOICE,
required=False required=False
), ),
) )
if 'widget' not in kwargs: if 'widget' not in kwargs:
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=BASE_CHOICES) kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=possible_choices)
forms.MultiValueField.__init__( forms.MultiValueField.__init__(
self, fields=fields, require_all_fields=False, *args, **kwargs self, fields=fields, require_all_fields=False, *args, **kwargs
) )
def set_event(self, event): def set_event(self, event):
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = [ choices = [
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None) (c.key, c.text) for c in EVENT_BASE_CHOICES if getattr(event, c.attribute, None)
] ]
if self.relative_to_order:
choices += [(c.key, c.text) for c in ORDER_BASE_CHOICES]
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = choices
def compress(self, data_list): def compress(self, data_list):
if not data_list: if not data_list:
return None return None
data = reldateparts(*data_list) data = reldateparts(*data_list)
if data.status == 'absolute': if data.status == 'unset':
return RelativeDateWrapper(data.absolute)
elif data.status == 'unset':
return None return None
elif data.status == 'absolute':
return RelativeDateWrapper(data.absolute)
else: else:
return RelativeDateWrapper(RelativeDate( return RelativeDateWrapper(RelativeDate(
days=data.rel_days_number, days=data.rel_days_number,
@@ -525,8 +737,14 @@ class RelativeDateField(RelativeDateTimeField):
data = reldateparts(*value) data = reldateparts(*value)
if data.status == 'absolute' and not data.absolute: if data.status == 'absolute' and not data.absolute:
raise ValidationError(self.error_messages['incomplete']) raise ValidationError(self.error_messages['incomplete'])
elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto): if data.status == 'relative':
raise ValidationError(self.error_messages['incomplete']) choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
if data.rel_days_number is None or not data.rel_days_relationto:
raise ValidationError(self.error_messages['incomplete'])
elif data.rel_days_relation == "before" and not choice.supports_before:
raise ValidationError(_('A relative date cannot be expressed as "before" for "{}"'.format(choice.text)))
elif data.rel_days_relation == "after" and not choice.supports_after:
raise ValidationError(_('A relative date cannot be expressed as "after" for "{}"'.format(choice.text)))
return forms.MultiValueField.clean(self, value) return forms.MultiValueField.clean(self, value)
+13 -1
View File
@@ -61,7 +61,7 @@ from pretix.base.models import (
Seat, SeatCategoryMapping, Voucher, Seat, SeatCategoryMapping, Voucher,
) )
from pretix.base.models.event import SubEvent from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee from pretix.base.models.orders import CheckoutSession, OrderFee
from pretix.base.models.tax import TaxRule from pretix.base.models.tax import TaxRule
from pretix.base.reldate import RelativeDateWrapper from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.checkin import _save_answers from pretix.base.services.checkin import _save_answers
@@ -472,6 +472,16 @@ class CartManager:
if term_last < time_machine_now(self.real_now_dt): if term_last < time_machine_now(self.real_now_dt):
raise CartError(error_messages['payment_ended']) raise CartError(error_messages['payment_ended'])
def _ensure_checkout_session(self):
CheckoutSession.objects.get_or_create(
event=self.event,
cart_id=self.cart_id,
defaults={
"sales_channel": self._sales_channel,
"testmode": self.event.testmode,
},
)
def _extend_expiry_of_valid_existing_positions(self): def _extend_expiry_of_valid_existing_positions(self):
# real_now_dt is initialized at CartManager instantiation, so it's slightly in the past. Add a small # real_now_dt is initialized at CartManager instantiation, so it's slightly in the past. Add a small
# delta to reduce risk of extending already expired CartPositions. # delta to reduce risk of extending already expired CartPositions.
@@ -1559,6 +1569,7 @@ class CartManager:
def commit(self): def commit(self):
self._check_presale_dates() self._check_presale_dates()
self._ensure_checkout_session()
self._check_max_cart_size() self._check_max_cart_size()
err = self._delete_out_of_timeframe() err = self._delete_out_of_timeframe()
@@ -1594,6 +1605,7 @@ def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None,
'max_value': str(max_value) if max_value is not None else None, 'max_value': str(max_value) if max_value is not None else None,
'info_data': info_data or {}, 'info_data': info_data or {},
}) })
cart_session['payments_postpone'] = False
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None): def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
+5 -3
View File
@@ -40,7 +40,7 @@ import dateutil
import dateutil.parser import dateutil.parser
from dateutil.tz import datetime_exists from dateutil.tz import datetime_exists
from django.core.files import File from django.core.files import File
from django.db import IntegrityError, transaction from django.db import IntegrityError
from django.db.models import ( from django.db.models import (
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min, BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
OuterRef, Q, Subquery, TextField, Value, When, OuterRef, Q, Subquery, TextField, Value, When,
@@ -59,6 +59,7 @@ from pretix.base.models import (
) )
from pretix.base.signals import checkin_created, periodic_task from pretix.base.signals import checkin_created, periodic_task
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.database import conditional_atomic
from pretix.helpers.jsonlogic import Logic from pretix.helpers.jsonlogic import Logic
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
from pretix.helpers.jsonlogic_query import ( from pretix.helpers.jsonlogic_query import (
@@ -891,6 +892,7 @@ def _save_answers(op, answers, given_answers):
qa.answer = answer qa.answer = answer
qa.save(update_fields=['answer']) qa.save(update_fields=['answer'])
qa.options.clear() qa.options.clear()
return qa
written = False written = False
for q, a in given_answers.items(): for q, a in given_answers.items():
@@ -1043,10 +1045,10 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if not simulate: if not simulate:
_save_answers(op, answers, given_answers) _save_answers(op, answers, given_answers)
with transaction.atomic(): with conditional_atomic(not simulate):
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic # Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
opqs = OrderPosition.all.select_related("order", "item") opqs = OrderPosition.all.select_related("order", "item")
if type != Checkin.TYPE_EXIT: if type != Checkin.TYPE_EXIT and not simulate:
opqs = opqs.select_for_update(of=OF_SELF) opqs = opqs.select_for_update(of=OF_SELF)
op = opqs.get(pk=op.pk) op = opqs.get(pk=op.pk)
+5
View File
@@ -33,6 +33,7 @@ from pretix.base.models.customers import CustomerSSOGrant
from ..models import CachedFile, CartPosition, InvoiceAddress from ..models import CachedFile, CartPosition, InvoiceAddress
from ..models.auth import UserKnownLoginSource from ..models.auth import UserKnownLoginSource
from ..models.orders import CheckoutSession
from ..signals import periodic_task from ..signals import periodic_task
@@ -43,6 +44,10 @@ def clean_cart_positions(sender, **kwargs):
cp.delete() cp.delete()
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=True): for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=True):
cp.delete() cp.delete()
for cs in CheckoutSession.objects.filter(created__lt=now() - timedelta(days=14)).exclude(
Exists(CartPosition.objects.filter(cart_id=OuterRef("cart_id")))
):
cs.delete()
for ia in InvoiceAddress.objects.filter(order__isnull=True, customer__isnull=True, last_modified__lt=now() - timedelta(days=14)): for ia in InvoiceAddress.objects.filter(order__isnull=True, customer__isnull=True, last_modified__lt=now() - timedelta(days=14)):
ia.delete() ia.delete()
+138 -87
View File
@@ -48,12 +48,12 @@ from celery.exceptions import MaxRetriesExceededError
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import models, transaction
from django.db.models import ( from django.db.models import (
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum, Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Subquery,
Value, Sum, Value,
) )
from django.db.models.functions import Coalesce, Greatest from django.db.models.functions import Cast, Greatest
from django.db.transaction import get_connection from django.db.transaction import get_connection
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -71,10 +71,10 @@ from pretix.base.models import (
Membership, Order, OrderPayment, OrderPosition, Quota, Seat, Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
SeatCategoryMapping, User, Voucher, SeatCategoryMapping, User, Voucher,
) )
from pretix.base.models.event import SubEvent from pretix.base.models.event import Event_SettingsStore, SubEvent
from pretix.base.models.orders import ( from pretix.base.models.orders import (
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund, BlockedTicketSecret, CheckoutSession, InvoiceAddress, OrderFee,
generate_secret, OrderRefund, generate_secret,
) )
from pretix.base.models.organizer import SalesChannel, TeamAPIToken from pretix.base.models.organizer import SalesChannel, TeamAPIToken
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
@@ -961,7 +961,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress, def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
meta_info: dict, event: Event, require_approval=False): meta_info: dict, event: Event, sales_channel: SalesChannel, require_approval=False):
fees = [] fees = []
# Pre-rounding, pre-fee total is used for fee calculation # Pre-rounding, pre-fee total is used for fee calculation
total = sum([c.gross_price_before_rounding for c in positions]) total = sum([c.gross_price_before_rounding for c in positions])
@@ -1021,7 +1021,14 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
payments_assigned += to_pay payments_assigned += to_pay
p['payment_amount'] = to_pay p['payment_amount'] = to_pay
if total != payments_assigned and not require_approval: allow_postponed_payment = (
require_approval or
(
sales_channel.identifier in event.settings.payment_choice_postpone_allowed_channels and not payment_requests
)
)
if total != payments_assigned and not allow_postponed_payment:
raise OrderError(_("The selected payment methods do not cover the total balance.")) raise OrderError(_("The selected payment methods do not cover the total balance."))
return fees return fees
@@ -1030,7 +1037,8 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime, def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None, payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
address: InvoiceAddress=None, meta_info: dict=None, shown_total=None, address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
customer=None, valid_if_pending=False, api_meta: dict=None, tax_rounding_mode=None): customer=None, valid_if_pending=False, api_meta: dict=None, tax_rounding_mode=None,
cart_id: str=None):
payments = [] payments = []
try: try:
@@ -1042,7 +1050,15 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
# Final calculation of fees, also performs final rounding # Final calculation of fees, also performs final rounding
try: try:
fees = _apply_rounding_and_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval) fees = _apply_rounding_and_fees(
positions,
payment_requests,
address,
meta_info,
event,
sales_channel=sales_channel,
require_approval=require_approval
)
except TaxRule.SaleNotAllowed: except TaxRule.SaleNotAllowed:
raise OrderError(error_messages['country_blocked']) raise OrderError(error_messages['country_blocked'])
@@ -1113,6 +1129,14 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
if meta_info: if meta_info:
for msg in meta_info.get('confirm_messages', []): for msg in meta_info.get('confirm_messages', []):
order.log_action('pretix.event.order.consent', data={'msg': msg}) order.log_action('pretix.event.order.consent', data={'msg': msg})
if cart_id:
try:
session = CheckoutSession.objects.get(event=event, cart_id=cart_id)
except CheckoutSession.DoesNotExist:
pass
else:
session.answers.update(order=order, checkoutsession=None)
session.delete()
order_placed.send(event, order=order, bulk=False) order_placed.send(event, order=order, bulk=False)
return order, payments return order, payments
@@ -1160,7 +1184,7 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str], def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web', email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web',
shown_total=None, customer=None, api_meta: dict=None, tax_rounding_mode=None): shown_total=None, customer=None, api_meta: dict=None, tax_rounding_mode=None, cart_id: str=None):
for p in payment_requests: for p in payment_requests:
p['pprov'] = event.get_payment_providers(cached=True)[p['provider']] p['pprov'] = event.get_payment_providers(cached=True)[p['provider']]
if not p['pprov']: if not p['pprov']:
@@ -1267,6 +1291,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
valid_if_pending=valid_if_pending, valid_if_pending=valid_if_pending,
api_meta=api_meta, api_meta=api_meta,
tax_rounding_mode=tax_rounding_mode, tax_rounding_mode=tax_rounding_mode,
cart_id=cart_id,
) )
try: try:
@@ -1494,83 +1519,104 @@ def send_expiry_warnings(sender, **kwargs):
@scopes_disabled() @scopes_disabled()
def send_download_reminders(sender, **kwargs): def send_download_reminders(sender, **kwargs):
today = now().replace(hour=0, minute=0, second=0, microsecond=0) today = now().replace(hour=0, minute=0, second=0, microsecond=0)
qs = Order.objects.annotate(
first_date=Coalesce( events = Event.objects.filter(
Min('all_positions__subevent__date_from'), Q(has_subevents=False, date_from__gte=now()) |
F('event__date_from') (Q(has_subevents=True) & Q(Exists(
SubEvent.objects.filter(event_id=OuterRef('id'), date_from__gte=now())
)))
).annotate(
reminder_days=Subquery(
Event_SettingsStore.objects.filter(
object=OuterRef('id'),
key='mail_days_download_reminder'
).exclude(
value="None"
).annotate(
val=Cast(F("value"), output_field=models.IntegerField()),
).values("val")
) )
).filter( ).filter(
download_reminder_sent=False, reminder_days__isnull=False,
datetime__lte=now() - timedelta(hours=2), ).order_by()
first_date__gte=today,
).only(
'pk', 'event_id', 'sales_channel', 'datetime',
).order_by('event_id')
event_id = None
days = None
event = None
for o in qs: for event in events.iterator(chunk_size=10_000):
if o.event_id != event_id: qs = event.orders.filter(
days = o.event.settings.get('mail_days_download_reminder', as_type=int) download_reminder_sent=False,
event = o.event datetime__lte=now() - timedelta(hours=2),
event_id = o.event_id )
if days is None: if event.has_subevents:
continue qs = qs.annotate(
first_date=Min('all_positions__subevent__date_from')
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder: ).filter(
continue Q(first_date__gte=today)
)
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0) else:
if now() < reminder_date or o.datetime > reminder_date: event_reminder_date = (event.date_from - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
continue if now() < event_reminder_date:
with transaction.atomic():
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
if o.download_reminder_sent:
# Race condition
continue
positions = list(o.positions_with_tickets)
if not positions:
continue continue
if not o.ticket_download_available: qs = qs.only(
'pk', 'event_id', 'sales_channel', 'datetime',
).order_by()
for o in qs:
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
continue continue
if o.status != Order.STATUS_PAID: if event.has_subevents:
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending): reminder_date = ((o.first_date or event.date_from) - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
else:
reminder_date = event_reminder_date
if now() < reminder_date or o.datetime > reminder_date:
continue
with transaction.atomic():
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
if o.download_reminder_sent:
# Race condition
continue
positions = list(o.positions_with_tickets)
if not positions:
continue continue
with language(o.locale, o.event.settings.region): if not o.ticket_download_available:
o.download_reminder_sent = True continue
o.save(update_fields=['download_reminder_sent'])
email_template = event.settings.mail_text_download_reminder
email_subject = event.settings.mail_subject_download_reminder
email_context = get_email_context(event=event, order=o)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True
)
if event.settings.mail_send_download_reminder_attendee: if o.status != Order.STATUS_PAID:
for p in positions: if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
if p.subevent_id: continue
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
hour=0, minute=0, second=0, microsecond=0 with language(o.locale, o.event.settings.region):
) o.download_reminder_sent = True
if now() < reminder_date: o.save(update_fields=['download_reminder_sent'])
continue email_template = event.settings.mail_text_download_reminder
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email: email_subject = event.settings.mail_subject_download_reminder
email_template = event.settings.mail_text_download_reminder_attendee email_context = get_email_context(event=event, order=o)
email_subject = event.settings.mail_subject_download_reminder_attendee o.send_mail(
email_context = get_email_context(event=event, order=o, position=p) email_subject, email_template, email_context,
o.send_mail( 'pretix.event.order.email.download_reminder_sent',
email_subject, email_template, email_context, attach_tickets=True
'pretix.event.order.email.download_reminder_sent', )
attach_tickets=True, position=p
) if event.settings.mail_send_download_reminder_attendee:
for p in positions:
if p.subevent_id:
reminder_date = (p.subevent.date_from - timedelta(days=event.reminder_days)).replace(
hour=0, minute=0, second=0, microsecond=0
)
if now() < reminder_date:
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
email_template = event.settings.mail_text_download_reminder_attendee
email_subject = event.settings.mail_subject_download_reminder_attendee
email_context = get_email_context(event=event, order=o, position=p)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True, position=p
)
def notify_user_changed_order(order, user=None, auth=None, invoices=[]): def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
@@ -3169,12 +3215,12 @@ class OrderChangeManager:
def perform_order(self, event: Event, payments: List[dict], positions: List[str], def perform_order(self, event: Event, payments: List[dict], positions: List[str],
email: str=None, locale: str=None, address: int=None, meta_info: dict=None, email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None, sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None,
api_meta: dict=None): api_meta: dict=None, cart_id: str=None):
with language(locale), time_machine_now_assigned(override_now_dt): with language(locale), time_machine_now_assigned(override_now_dt):
try: try:
try: try:
return _perform_order(event, payments, positions, email, locale, address, meta_info, return _perform_order(event, payments, positions, email, locale, address, meta_info,
sales_channel, shown_total, customer, api_meta) sales_channel, shown_total, customer, api_meta, cart_id=cart_id)
except LockTimeoutException: except LockTimeoutException:
self.retry() self.retry()
except (MaxRetriesExceededError, LockTimeoutException): except (MaxRetriesExceededError, LockTimeoutException):
@@ -3438,7 +3484,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
} }
) )
new_invoice_created = False new_invoice = None
if recreate_invoices: if recreate_invoices:
# Lock to prevent duplicate invoice creation # Lock to prevent duplicate invoice creation
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk) order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
@@ -3449,13 +3495,16 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
if has_active_invoice and order.total != oldtotal: if has_active_invoice and order.total != oldtotal:
try: try:
generate_cancellation(i) generate_cancellation(i)
generate_invoice(order) new_invoice = generate_invoice(order)
except Exception as e: except Exception as e:
logger.exception("Could not generate invoice.") logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={ order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e) "exception": str(e)
}) })
new_invoice_created = True else:
order.log_action('pretix.event.order.invoice.generated', data={
'invoice': new_invoice.pk
})
elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order): elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order):
if order.event.settings.get('invoice_generate') == 'True' or ( if order.event.settings.get('invoice_generate') == 'True' or (
@@ -3465,10 +3514,9 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
try: try:
if has_active_invoice: if has_active_invoice:
generate_cancellation(i) generate_cancellation(i)
i = generate_invoice(order) new_invoice = generate_invoice(order)
new_invoice_created = True
order.log_action('pretix.event.order.invoice.generated', data={ order.log_action('pretix.event.order.invoice.generated', data={
'invoice': i.pk 'invoice': new_invoice.pk
}) })
except Exception as e: except Exception as e:
logger.exception("Could not generate invoice.") logger.exception("Could not generate invoice.")
@@ -3476,8 +3524,11 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
"exception": str(e) "exception": str(e)
}) })
if new_invoice and invoice_transmission_separately(new_invoice):
transmit_invoice.apply_async(args=(order.event_id, new_invoice.pk, False))
order.create_transactions() order.create_transactions()
return old_fee, new_fee, fee, new_payment, new_invoice_created return old_fee, new_fee, fee, new_payment, bool(new_invoice)
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards") @receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
+76
View File
@@ -0,0 +1,76 @@
#
# 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 datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
from django.utils.timezone import make_aware, now
from pretix.base.models import Event, SalesChannel
from pretix.base.reldate import RelativeDateWrapper
def compute_payment_deadline(event: Event, sales_channel: SalesChannel, now_dt=None, subevents=None) -> datetime:
now_dt = now_dt or now()
tz = ZoneInfo(event.settings.timezone)
sales_channel_suffix = "_" + sales_channel.identifier.replace(".", "_")
if not (mode := event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
mode = event.settings.get('payment_term_mode')
sales_channel_suffix = ""
if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(
days=event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
elif mode == 'minutes':
exp_by_date = now_dt.astimezone(tz) + timedelta(
minutes=event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
else:
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
expires = exp_by_date
term_last = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if event.has_subevents and subevents:
terms = [
term_last.datetime(se).date()
for se in subevents
]
if not terms:
return expires
term_last = min(terms)
else:
term_last = term_last.datetime(event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < expires:
return term_last
return expires
+16
View File
@@ -561,6 +561,22 @@ def base_placeholders(sender, **kwargs):
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''), 'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
lambda event: str(event.location or ''), lambda event: str(event.location or ''),
), ),
SimpleFunctionalTextPlaceholder(
'event_begin_time', ['event_or_subevent'],
lambda event_or_subevent:
date_format(event_or_subevent.date_from.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
if event_or_subevent.date_from
else '',
lambda event: date_format(event.date_from.astimezone(event.timezone), 'TIME_FORMAT') if event.date_from else '',
),
SimpleFunctionalTextPlaceholder(
'event_end_time', ['event_or_subevent'],
lambda event_or_subevent:
date_format(event_or_subevent.date_to.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
if event_or_subevent.date_to
else '',
lambda event: date_format(event.date_to.astimezone(event.timezone), 'TIME_FORMAT') if event.date_to else '',
),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'event_admission_time', ['event_or_subevent'], 'event_admission_time', ['event_or_subevent'],
lambda event_or_subevent: lambda event_or_subevent:
+1 -1
View File
@@ -193,7 +193,7 @@ class QuotaAvailability:
lock_name = '_'.join([str(p) for p in sorted([q.pk for q in quotas])]) lock_name = '_'.join([str(p) for p in sorted([q.pk for q in quotas])])
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}'): if rc.exists(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}'):
return return
rc.setex(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}', '1', 10) rc.set(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}', '1', ex=10)
update = defaultdict(list) update = defaultdict(list)
for q in quotas: for q in quotas:
+68 -2
View File
@@ -343,6 +343,66 @@ def _validate_vat_id_EU(vat_id, country_code):
return vat_id return vat_id
def _validate_vat_id_EU_fallback_germany(vat_id, country_code, requester_id):
# We can skip most static validation checks because _validate_vat_id_EU always runs before
vat_id = normalize_vat_id(vat_id, country_code)
# The VIES service of the European commission is overused and down due to rate limits A LOT. There is another
# API by German BZSt, but it only works if the requester is German and the requested is not.
# https://www.bzst.de/DE/Unternehmen/Identifikationsnummern/Umsatzsteuer-Identifikationsnummer/AuslaendischeUSt-IdNr/auslaendische_ust_idnr_node.html
try:
r = requests.post(
"https://api.evatr.vies.bzst.de/app/v1/abfrage",
json={
"anfragendeUstid": requester_id,
"angefragteUstid": vat_id,
},
timeout=10,
)
d = r.json()
if r.status_code == 200:
if d['status'] in ('evatr-0000', 'evatr-2008'):
# evatr-0000: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt gültig.
# evatr-2008: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt gültig.
# Für die qualifizierte Bestätigungsanfrage liegt einer Besonderheit vor.
# Für Rückfragen wenden Sie sich an das BZSt.
return vat_id
# evatr-2002: Die angefragte USt-IdNr. ist zum Anfragezeitpunkt nicht gültig.
# Sie ist erst gültig ab dem Datum im Feld gueltigAb.
# evatr-2006: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt nicht gültig.
# Sie war gültig im Zeitraum, der durch die Werte in den Feldern gueltigAb und gueltigBis beschrieben ist.
raise VATIDFinalError(error_messages['invalid'])
elif r.status_code == 400:
if d['status'] in ('evatr-0002', 'evatr-0004', 'evatr-0008'):
# evatr-0002: Mindestens eins der Pflichtfelder ist nicht besetzt.
# evatr-0004: Die anfragende DE Ust-IdNr. ist syntaktisch falsch. Sie passt nicht in das deutsche Erzeugungsschema.
# evatr-0008: Die maximale Anzahl von qualifizierten Bestätigungsabfragen für diese Session wurde erreicht.
# Bitte starten Sie erneut mit einer einfachen Bestätigungsabfrage.
raise VATIDTemporaryError(error_messages['unavailable'])
# evatr-0005: Die angegebene angefragte Ust-IdNr. ist syntaktisch falsch.
# evatr-0012: Die angefrage USt-IdNr. ist syntaktisch falsch. Sie passt nicht in das Erzeugungsschema.
# evatr-2003: Das angegebene Länderkennzeichen der angefragten USt-IdNr. ist nicht gültig.
raise VATIDFinalError(error_messages['invalid'])
elif r.status_code == 403:
# evatr-0006: Die anfragende DE USt-IdNr. ist nicht berechtigt eine DE Ust-IdNr. anzufragen.
# evatr-0007: Fehlerhafter Aufruf.
raise VATIDTemporaryError(error_messages['unavailable'])
elif r.status_code == 404:
if d['status'] in ('evatr-2005'):
# evatr-2005: Die angegebene eigene DE Ust-IdNr. ist zum Anfragezeitpunkt nicht gültig.
raise VATIDTemporaryError(error_messages['unavailable'])
# evatr-2001: Die angefragte USt-IdNr. ist zum Anfragezeitpunkt nicht vergeben.
raise VATIDFinalError(error_messages['invalid'])
else: # 500, 503
raise VATIDTemporaryError(error_messages['unavailable'])
except requests.RequestException:
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(error_messages['unavailable'])
except ValueError: # JSON parsing failed
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(error_messages['unavailable'])
def _validate_vat_id_CH(vat_id, country_code): def _validate_vat_id_CH(vat_id, country_code):
if vat_id[:3] != 'CHE': if vat_id[:3] != 'CHE':
raise VATIDFinalError(error_messages['country_mismatch']) raise VATIDFinalError(error_messages['country_mismatch'])
@@ -394,12 +454,18 @@ def _validate_vat_id_CH(vat_id, country_code):
return vat_id return vat_id
def validate_vat_id(vat_id, country_code): def validate_vat_id(vat_id, country_code, requester_id=None):
if not vat_id: if not vat_id:
return vat_id return vat_id
country_code = str(country_code) country_code = str(country_code)
if is_eu_country(country_code): if is_eu_country(country_code):
return _validate_vat_id_EU(vat_id, country_code) try:
return _validate_vat_id_EU(vat_id, country_code)
except VATIDTemporaryError:
if requester_id and requester_id.startswith("DE") and not vat_id.startswith("DE"):
return _validate_vat_id_EU_fallback_germany(vat_id, country_code, requester_id)
else:
raise
elif country_code == 'CH': elif country_code == 'CH':
return _validate_vat_id_CH(vat_id, country_code) return _validate_vat_id_CH(vat_id, country_code)
elif country_code == 'NO': elif country_code == 'NO':
File diff suppressed because one or more lines are too long
+22 -10
View File
@@ -50,8 +50,8 @@ from pretix.api.serializers.order import (
from pretix.api.serializers.waitinglist import WaitingListSerializer from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.i18n import LazyLocaleException from pretix.base.i18n import LazyLocaleException
from pretix.base.models import ( from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, OrderPayment, CachedCombinedTicket, CachedTicket, Event, Invoice, InvoiceAddress,
OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer, OrderPayment, OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer,
) )
from pretix.base.services.invoices import invoice_pdf_task from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.signals import register_data_shredders from pretix.base.signals import register_data_shredders
@@ -598,18 +598,30 @@ class InvoiceShredder(BaseDataShredder):
def shred_data(self, progress_callback=None): def shred_data(self, progress_callback=None):
qs_i = self.event.invoices.filter(shredded=False) qs_i = self.event.invoices.filter(shredded=False)
total = qs_i.count() total = qs_i.count()
ignore_fields = (
'prefix', 'invoice_no', 'full_invoice_no', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_state', 'invoice_from_country', 'invoice_from_tax_id',
'invoice_from_vat_id', 'locale', 'payment_provider_stamp', 'footer_text', 'foreign_currency_display',
'foreign_currency_source', 'transmission_type', 'transmission_provider', 'transmission_status',
)
for i in _progress_helper(qs_i, progress_callback, 0, total): for i in _progress_helper(qs_i, progress_callback, 0, total):
if i.file: if i.file:
i.file.delete() i.file.delete()
i.shredded = True i.shredded = True
i.introductory_text = "█"
i.additional_text = "█" for f in Invoice._meta.fields:
i.invoice_to = "█" if f.name in ignore_fields:
i.payment_provider_text = "█" continue
i.transmission_info = {"_shredded": True} val = getattr(i, f.name, None)
i.save() if val and isinstance(val, str):
i.lines.update(description="█") setattr(i, f.name, "█")
elif val and isinstance(val, list): # jsonfield
setattr(i, f.name, [])
elif val and isinstance(val, dict): # jsonfield
setattr(i, f.name, {"_shredded": True})
i.save()
i.lines.update(description="█", attendee_name="█")
class CachedTicketShredder(BaseDataShredder): class CachedTicketShredder(BaseDataShredder):
+52 -23
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
from decimal import ROUND_HALF_UP, Decimal from decimal import ROUND_HALF_UP, Decimal
from typing import Optional
from babel import Locale, UnknownLocaleError from babel import Locale, UnknownLocaleError
from babel.numbers import format_currency from babel.numbers import format_currency
@@ -35,32 +36,32 @@ register = template.Library()
@register.filter("money") @register.filter("money")
def money_filter(value: Decimal, arg='', hide_currency=False): def money_filter(value: Optional[Decimal | float | int | str], arg='', hide_currency=False):
if isinstance(value, (float, int)): if isinstance(value, (float, int, str)):
if value == '':
return value
value = Decimal(value) value = Decimal(value)
if value is None: if value is None:
value = Decimal('0.00') value = Decimal('0.00')
if not isinstance(value, Decimal): if not isinstance(value, Decimal):
if value == '':
return value
raise TypeError("Invalid data type passed to money filter: %r" % type(value)) raise TypeError("Invalid data type passed to money filter: %r" % type(value))
if not arg: if not arg:
raise ValueError("No currency passed.") raise ValueError("No currency passed.")
arg = arg.upper() arg = arg.upper()
places = settings.CURRENCY_PLACES.get(arg, 2) if value.normalize().as_tuple().exponent < -9:
rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP) # Heuristic: It's unlikely we'll ever see values of less than 0.000000001 in any currency. Therefore, if we
if places < 2 and rounded != value: # do see them, we very likely deal with a floating point error. This happens mostly in dev mode when computations
# We display decimal places even if we shouldn't for this currency if rounding # are made in SQLite, which uses REAL precision, but it can also happen when we naively pass a float from Python
# would make the numbers incorrect. If this branch executes, it's likely a bug in # land to this filter (even though it should not happen).
# pretix, but we won't show wrong numbers! value = value.quantize(Decimal('1e-9'), ROUND_HALF_UP).normalize()
if hide_currency:
return floatformat(value, "2g") currency_places = settings.CURRENCY_PLACES.get(arg, 2)
else: required_places = -value.normalize().as_tuple().exponent
return '{} {}'.format(arg, floatformat(value, "2g")) render_places = max(currency_places, required_places)
if hide_currency: if hide_currency:
return floatformat(value, f"{places}g") return floatformat(value, f"{render_places}g")
try: try:
locale = Locale(get_babel_locale()) locale = Locale(get_babel_locale())
@@ -68,14 +69,29 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
locale = "en" locale = "en"
try: try:
return format_currency(value, arg, locale=locale) return format_currency(
value,
arg,
locale=locale,
# We only allow Babel to restrict the digits to the digits defined by the currency if this does not remove any
# precision in case we have sub-currency precision (which we shouldn't have in most places, but it's still
# better than showing wrong data). Note: Weird precision effects can occur after in-database arithmetic
# on SQLite, since SQLite does not have fixed-decimal computation.
currency_digits=currency_places >= required_places,
decimal_quantization=currency_places >= required_places,
)
except: except:
return '{} {}'.format(arg, floatformat(value, f"{places}g")) return '{} {}'.format(arg, floatformat(value, f"{render_places}g"))
@register.filter("money_without_currency")
def money_filter_without_currency(value: Optional[Decimal | float | int | str], arg=''):
return money_filter(value, arg, hide_currency=True)
@register.filter("money_numberfield") @register.filter("money_numberfield")
def money_numberfield_filter(value: Decimal, arg=''): def money_numberfield_filter(value: Optional[Decimal | float | int | str], arg=''):
if isinstance(value, (float, int)): if isinstance(value, (float, int, str)):
value = Decimal(value) value = Decimal(value)
if not isinstance(value, Decimal): if not isinstance(value, Decimal):
raise TypeError("Invalid data type passed to money filter: %r" % type(value)) raise TypeError("Invalid data type passed to money filter: %r" % type(value))
@@ -87,15 +103,28 @@ def money_numberfield_filter(value: Decimal, arg=''):
@register.filter(is_safe=True) @register.filter(is_safe=True)
def tax_rate_format(number): def tax_rate_format(number: Optional[Decimal | float | int | str]):
""" """
Display a Decimal to its significant decimal places, used for tax rates. Display a Decimal to its significant decimal places, used for tax rates.
""" """
assert isinstance(number, Decimal) if isinstance(number, (float, int, str)):
if number == '':
return number
number = Decimal(number)
if number is None:
number = Decimal('0.00')
if not isinstance(number, Decimal):
raise TypeError("Invalid data type passed to tax rate format filter: %r" % type(number))
if number.normalize().as_tuple().exponent < -9:
# Heuristic: It's unlikely we'll ever see values of less than 0.000000001 in any currency. Therefore, if we
# do see them, we very likely deal with a floating point error. This happens mostly in dev mode when computations
# are made in SQLite, which uses REAL precision, but it can also happen when we naively pass a float from Python
# land to this filter (even though it should not happen).
number = number.quantize(Decimal('1e-9'), ROUND_HALF_UP).normalize()
return mark_safe( return mark_safe(
formats.number_format( formats.number_format(
number.normalize(), number,
-number.as_tuple().exponent, -number.normalize().as_tuple().exponent,
use_l10n=True, use_l10n=True,
force_grouping=False, force_grouping=False,
) )
+7 -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 # 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/>. # <https://www.gnu.org/licenses/>.
# #
from django.conf import settings
from django.http import ( from django.http import (
HttpResponseForbidden, HttpResponseNotFound, HttpResponseServerError, HttpResponseForbidden, HttpResponseNotFound, HttpResponseServerError,
) )
@@ -27,7 +28,6 @@ from django.template import TemplateDoesNotExist, loader
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from sentry_sdk import last_event_id
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.middleware import get_language_from_request from pretix.base.middleware import get_language_from_request
@@ -106,9 +106,14 @@ def server_error(request):
template = loader.get_template('500.html') template = loader.get_template('500.html')
except TemplateDoesNotExist: except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html') return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
if settings.SENTRY_ENABLED:
from sentry_sdk import last_event_id
sentry_id = last_event_id()
else:
sentry_id = None
r = HttpResponseServerError(template.render({ r = HttpResponseServerError(template.render({
'request': request, 'request': request,
'sentry_event_id': last_event_id(), 'sentry_event_id': sentry_id,
})) }))
r.xframe_options_exempt = True r.xframe_options_exempt = True
return r return r
+114 -72
View File
@@ -32,18 +32,21 @@ from django.utils.functional import cached_property
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from pretix.base.forms.questions import ( from pretix.base.forms.questions import (
BaseInvoiceAddressForm, BaseInvoiceNameForm, BaseQuestionsForm, BaseInvoiceAddressForm, BaseInvoiceNameForm, OrderLevelQuestionsForm,
TicketLevelQuestionsForm,
) )
from pretix.base.models import ( from pretix.base.models import (
CartPosition, InvoiceAddress, OrderPosition, Question, QuestionAnswer, CartPosition, InvoiceAddress, OrderPosition, Question, QuestionAnswer,
QuestionOption, QuestionOption,
) )
from pretix.base.models.customers import AttendeeProfile from pretix.base.models.customers import AttendeeProfile
from pretix.base.models.orders import CheckoutSession, Order
from pretix.presale.signals import contact_form_fields_overrides from pretix.presale.signals import contact_form_fields_overrides
class BaseQuestionsViewMixin: class BaseQuestionsViewMixin:
form_class = BaseQuestionsForm order_form_class = OrderLevelQuestionsForm
orderposition_form_class = TicketLevelQuestionsForm
all_optional = False all_optional = False
@cached_property @cached_property
@@ -56,6 +59,28 @@ class BaseQuestionsViewMixin:
def question_form_kwargs(self, cr): def question_form_kwargs(self, cr):
return {} return {}
@property
def order_question_container(self):
raise NotImplementedError()
@cached_property
def order_questions_form(self):
container = self.order_question_container
if container is None:
return None
kwargs = {} # self.question_form_kwargs(cr)
form = self.order_form_class(
event=self.request.event,
prefix='order',
request=self.request,
container=container,
all_optional=self.all_optional,
data=(self.request.POST if self.request.method == 'POST' else None),
files=(self.request.FILES if self.request.method == 'POST' else None),
**kwargs
)
return form
@cached_property @cached_property
def forms(self): def forms(self):
""" """
@@ -69,15 +94,17 @@ class BaseQuestionsViewMixin:
orderpos = cr if isinstance(cr, OrderPosition) else None orderpos = cr if isinstance(cr, OrderPosition) else None
kwargs = self.question_form_kwargs(cr) kwargs = self.question_form_kwargs(cr)
form = self.form_class(event=self.request.event, form = self.orderposition_form_class(
prefix=cr.id, event=self.request.event,
request=self.request, prefix=cr.id,
cartpos=cartpos, request=self.request,
orderpos=orderpos, cartpos=cartpos,
all_optional=self.all_optional, orderpos=orderpos,
data=(self.request.POST if self.request.method == 'POST' else None), all_optional=self.all_optional,
files=(self.request.FILES if self.request.method == 'POST' else None), data=(self.request.POST if self.request.method == 'POST' else None),
**kwargs) files=(self.request.FILES if self.request.method == 'POST' else None),
**kwargs
)
form.pos = cartpos or orderpos form.pos = cartpos or orderpos
form.show_copy_answers_to_addon_button = form.pos.addon_to and ( form.show_copy_answers_to_addon_button = form.pos.addon_to and (
set(form.pos.addon_to.item.questions.all()) & set(form.pos.item.questions.all()) or set(form.pos.addon_to.item.questions.all()) & set(form.pos.item.questions.all()) or
@@ -108,6 +135,8 @@ class BaseQuestionsViewMixin:
question_field.initial = getattr(question_field, 'initial', None) or src['initial'] question_field.initial = getattr(question_field, 'initial', None) or src['initial']
if 'validators' in src: if 'validators' in src:
question_field.validators += src['validators'] question_field.validators += src['validators']
if 'label' in src:
question_field.label = src['label']
if len(form.fields) > 0: if len(form.fields) > 0:
formlist.append(form) formlist.append(form)
@@ -130,8 +159,38 @@ class BaseQuestionsViewMixin:
def save(self): def save(self):
failed = False failed = False
if self.order_questions_form:
if not self.order_questions_form.is_valid():
failed = True
else:
checkoutsession = self.order_question_container if isinstance(self.order_question_container, CheckoutSession) else None
order = self.order_question_container if isinstance(self.order_question_container, Order) else None
for k, v in self.order_questions_form.cleaned_data.items():
if k.startswith('question_'):
field = self.order_questions_form.fields[k]
if hasattr(field, 'answer'):
# We already have a cached answer object, so we don't
# have to create a new one
if v == '' or v is None or (isinstance(field, forms.FileField) and v is False) \
or (isinstance(v, QuerySet) and not v.exists()):
if field.answer.file:
field.answer.file.delete()
field.answer.delete()
else:
self._save_to_answer(field, field.answer, v)
field.answer.save()
elif v != '' and v is not None:
self._upsert_answer(
field, v,
checkoutsession=checkoutsession,
order=order,
question=field.question,
)
for form in self.forms: for form in self.forms:
meta_info = form.pos.meta_info_data meta_info = form.pos.meta_info_data
cartposition = form.pos if isinstance(form.pos, CartPosition) else None
orderposition = form.pos if isinstance(form.pos, OrderPosition) else None
# Every form represents a CartPosition or OrderPosition with questions attached # Every form represents a CartPosition or OrderPosition with questions attached
if not form.is_valid(): if not form.is_valid():
failed = True failed = True
@@ -140,10 +199,8 @@ class BaseQuestionsViewMixin:
prof = AttendeeProfile.objects.filter( prof = AttendeeProfile.objects.filter(
customer=self.cart_customer, pk=form.cleaned_data.get('saved_id') customer=self.cart_customer, pk=form.cleaned_data.get('saved_id')
).first() or AttendeeProfile(customer=getattr(self, 'cart_customer', None)) ).first() or AttendeeProfile(customer=getattr(self, 'cart_customer', None))
answers_key_to_index = {a.get('field_name'): i for i, a in enumerate(prof.answers)}
else: else:
prof = AttendeeProfile(customer=getattr(self, 'cart_customer', None)) prof = AttendeeProfile(customer=getattr(self, 'cart_customer', None))
answers_key_to_index = {}
# This form was correctly filled, so we store the data as # This form was correctly filled, so we store the data as
# answers to the questions / in the CartPosition object # answers to the questions / in the CartPosition object
@@ -181,64 +238,19 @@ class BaseQuestionsViewMixin:
else: else:
self._save_to_answer(field, field.answer, v) self._save_to_answer(field, field.answer, v)
field.answer.save() field.answer.save()
if isinstance(field, forms.ModelMultipleChoiceField) or isinstance(field, forms.ModelChoiceField):
answer_value = {o.identifier: str(o) for o in field.answer.options.all()} answer_dict = self._build_answer_dict(field, field.answer, k)
elif isinstance(field, forms.BooleanField): prof.store_answer(answer_dict)
answer_value = bool(field.answer.answer)
else:
answer_value = str(field.answer.answer)
answer_dict = {
'field_name': k,
'field_label': str(field.label),
'value': answer_value,
'question_type': field.question.type,
'question_identifier': field.question.identifier,
}
if k in answers_key_to_index:
prof.answers[answers_key_to_index[k]] = answer_dict
else:
prof.answers.append(answer_dict)
elif v != '' and v is not None: elif v != '' and v is not None:
answer = QuestionAnswer( answer = self._upsert_answer(
cartposition=(form.pos if isinstance(form.pos, CartPosition) else None), field, v,
orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None), cartposition=cartposition,
orderposition=orderposition,
question=field.question, question=field.question,
) )
try:
self._save_to_answer(field, answer, v)
answer.save()
except IntegrityError:
# Since we prefill ``field.answer`` at form creation time, there's a possible race condition
# here if the users submits their save request a second time while the first one is still running,
# thus leading to duplicate QuestionAnswer objects. Since Django doesn't support UPSERT, the "proper"
# fix would be a transaction with select_for_update(), or at least fetching using get_or_create here
# again. However, both of these approaches have a significant performance overhead for *all* requests,
# while the issue happens very very rarely. So we opt for just catching the error and retrying properly.
answer = QuestionAnswer.objects.get(
cartposition=(form.pos if isinstance(form.pos, CartPosition) else None),
orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None),
question=field.question,
)
self._save_to_answer(field, answer, v)
answer.save()
if isinstance(field, forms.ModelMultipleChoiceField) or isinstance(field, forms.ModelChoiceField): answer_dict = self._build_answer_dict(field, answer, k)
answer_value = {o.identifier: str(o) for o in answer.options.all()} prof.store_answer(answer_dict)
elif isinstance(field, forms.BooleanField):
answer_value = bool(answer.answer)
else:
answer_value = str(answer.answer)
answer_dict = {
'field_name': k,
'field_label': str(field.label),
'value': answer_value,
'question_type': field.question.type,
'question_identifier': field.question.identifier,
}
if k in answers_key_to_index:
prof.answers[answers_key_to_index[k]] = answer_dict
else:
prof.answers.append(answer_dict)
else: else:
field = form.fields[k] field = form.fields[k]
@@ -257,10 +269,7 @@ class BaseQuestionsViewMixin:
'question_type': None, 'question_type': None,
'question_identifier': None, 'question_identifier': None,
} }
if k in answers_key_to_index: prof.store_answer(answer_dict)
prof.answers[answers_key_to_index[k]] = answer_dict
else:
prof.answers.append(answer_dict)
form.pos.meta_info = json.dumps(meta_info) form.pos.meta_info = json.dumps(meta_info)
form.pos.save() form.pos.save()
@@ -271,6 +280,23 @@ class BaseQuestionsViewMixin:
return not failed return not failed
def _upsert_answer(self, field, v, **answer_kwargs):
answer = QuestionAnswer(**answer_kwargs)
try:
self._save_to_answer(field, answer, v)
answer.save()
except IntegrityError:
# Since we prefill ``field.answer`` at form creation time, there's a possible race condition
# here if the users submits their save request a second time while the first one is still running,
# thus leading to duplicate QuestionAnswer objects. Since Django doesn't support UPSERT, the "proper"
# fix would be a transaction with select_for_update(), or at least fetching using get_or_create here
# again. However, both of these approaches have a significant performance overhead for *all* requests,
# while the issue happens very very rarely. So we opt for just catching the error and retrying properly.
answer = QuestionAnswer.objects.get(**answer_kwargs)
self._save_to_answer(field, answer, v)
answer.save()
return answer
def _save_to_answer(self, field, answer, value): def _save_to_answer(self, field, answer, value):
if isinstance(field, forms.ModelMultipleChoiceField): if isinstance(field, forms.ModelMultipleChoiceField):
answstr = ", ".join([str(o) for o in value]) answstr = ", ".join([str(o) for o in value])
@@ -294,6 +320,21 @@ class BaseQuestionsViewMixin:
else: else:
answer.answer = value answer.answer = value
def _build_answer_dict(self, field, answer, k):
if isinstance(field, forms.ModelMultipleChoiceField) or isinstance(field, forms.ModelChoiceField):
answer_value = {o.identifier: str(o) for o in answer.options.all()}
elif isinstance(field, forms.BooleanField):
answer_value = bool(answer.answer)
else:
answer_value = str(answer.answer)
return {
'field_name': k,
'field_label': str(field.label),
'value': answer_value,
'question_type': field.question.type,
'question_identifier': field.question.identifier,
}
class OrderQuestionsViewMixin(BaseQuestionsViewMixin): class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
invoice_form_class = BaseInvoiceAddressForm invoice_form_class = BaseInvoiceAddressForm
@@ -309,7 +350,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
def positions(self): def positions(self):
qqs = self.request.event.questions.all() qqs = self.request.event.questions.all()
if self.only_user_visible: if self.only_user_visible:
qqs = qqs.filter(ask_during_checkin=False, hidden=False) qqs = qqs.filter(ask_during_checkin=False, hidden=False, container_type=Question.ContainerType.ORDERPOSITION)
return list(self.order.positions.select_related( return list(self.order.positions.select_related(
'item', 'variation' 'item', 'variation'
).prefetch_related( ).prefetch_related(
@@ -397,6 +438,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['order'] = self.order ctx['order'] = self.order
ctx['order_questions_form'] = self.order_questions_form
ctx['formgroups'] = self.formdict.items() ctx['formgroups'] = self.formdict.items()
ctx['invoice_form'] = self.invoice_form ctx['invoice_form'] = self.invoice_form
ctx['invoice_address_asked'] = self.address_asked ctx['invoice_address_asked'] = self.address_asked
+12 -1
View File
@@ -172,7 +172,9 @@ class CachedFileInput(forms.ClearableFileInput):
from ...base.models import CachedFile from ...base.models import CachedFile
v = super().value_from_datadict(data, files, name) v = super().value_from_datadict(data, files, name)
if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None
return CachedFile.objects.filter(id=data[name + '-cachedfile']).first() v = CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
if not v.allowed_for_session(self.request):
v = None
return v return v
def get_context(self, name, value, attrs): def get_context(self, name, value, attrs):
@@ -244,6 +246,11 @@ class ExtFileField(ExtValidationMixin, SizeFileField):
class CachedFileField(ExtFileField): class CachedFileField(ExtFileField):
widget = CachedFileInput widget = CachedFileInput
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self.widget.request = self.request
def to_python(self, data): def to_python(self, data):
from ...base.models import CachedFile from ...base.models import CachedFile
@@ -271,6 +278,8 @@ class CachedFileField(ExtFileField):
filename=data.name, filename=data.name,
type=data.content_type, type=data.content_type,
) )
if self.request:
cf.bind_to_session(self.request) # no salt because we want direct web access
cf.file.save(data.name, data.file) cf.file.save(data.name, data.file)
cf.save() cf.save()
data._uploaded_to = cf data._uploaded_to = cf
@@ -294,6 +303,8 @@ class CachedFileField(ExtFileField):
filename=data.name, filename=data.name,
type=data.content_type, type=data.content_type,
) )
if self.request:
cf.bind_to_session(self.request) # no salt because we want direct web access
cf.file.save(data.name, data.file) cf.file.save(data.name, data.file)
cf.save() cf.save()
data._uploaded_to = cf data._uploaded_to = cf
+64 -6
View File
@@ -400,10 +400,10 @@ class EventMetaValueForm(forms.ModelForm):
if self.disabled: if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly' self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_slug(self): def clean_value(self):
if self.disabled: if self.disabled:
return self.instance.value if self.instance else None return self.instance.value if self.instance else None
return self.cleaned_data['slug'] return self.cleaned_data['value']
class Meta: class Meta:
model = EventMetaValue model = EventMetaValue
@@ -600,6 +600,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'imprint_url', 'imprint_url',
'checkout_email_helptext', 'checkout_email_helptext',
'presale_has_ended_text', 'presale_has_ended_text',
'event_list_empty_text',
'voucher_explanation_text', 'voucher_explanation_text',
'checkout_success_text', 'checkout_success_text',
'show_dates_on_frontpage', 'show_dates_on_frontpage',
@@ -734,6 +735,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_available_only'] del self.fields['event_list_available_only']
del self.fields['event_list_filters'] del self.fields['event_list_filters']
del self.fields['event_calendar_future_only'] del self.fields['event_calendar_future_only']
del self.fields['event_list_empty_text']
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([ self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items() (a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items()
], key=lambda a: a[0]) ], key=lambda a: a[0])
@@ -853,9 +855,60 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
'payment_term_accept_late', 'payment_term_accept_late',
'payment_pending_hidden', 'payment_pending_hidden',
'payment_explanation', 'payment_explanation',
'payment_choice_postpone_allowed_channels',
'tax_rule_payment', 'tax_rule_payment',
] ]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
channels = list(self.obj.organizer.sales_channels.all())
self.fields['payment_choice_postpone_allowed_channels'].choices = [
(c.identifier, c.label) for c in channels
if c.type_instance.payment_restrictions_supported
]
self.term_channel_fields = {}
for c in channels:
if c.type_instance.payment_restrictions_supported and c.identifier != "web":
# At the moment, it seems sufficient to allow this for the same channel types as other payment settings
# We can always introduce more flags later if needed
suffix = '_' + c.identifier.replace(".", "_")
self.term_channel_fields[c] = [
'payment_term_mode' + suffix,
'payment_term_days' + suffix,
'payment_term_minutes' + suffix,
]
self.fields['payment_term_mode' + suffix] = DEFAULTS['payment_term_mode']['form_class'](
label=_("Payment term"),
widget=forms.RadioSelect,
required=False,
choices=(
('', _("same as above")),
('days', _("different payment term in days")),
('minutes', _("different payment term in minutes"))
),
)
self.fields['payment_term_days' + suffix] = DEFAULTS['payment_term_days']['form_class'](
required=False,
**DEFAULTS['payment_term_days']['form_kwargs'](suffix, 1),
)
self.fields['payment_term_minutes' + suffix] = DEFAULTS['payment_term_minutes']['form_class'](
required=False,
**DEFAULTS['payment_term_minutes']['form_kwargs'](suffix, 2),
)
def clean(self):
data = super().clean()
for c in self.term_channel_fields.keys():
suffix = '_' + c.identifier.replace(".", "_")
mode = self.cleaned_data.get(f'payment_term_mode{suffix}')
if mode == 'days' and self.cleaned_data.get(f'payment_term_days{suffix}') is None:
raise ValidationError({f'payment_term_days{suffix}': _("This field is required.")})
if mode == 'minutes' and self.cleaned_data.get(f'payment_term_minutes{suffix}') is None:
raise ValidationError({f'payment_term_minutes{suffix}': _("This field is required.")})
return data
def clean_payment_term_days(self): def clean_payment_term_days(self):
value = self.cleaned_data.get('payment_term_days') value = self.cleaned_data.get('payment_term_days')
if self.cleaned_data.get('payment_term_mode') == 'days' and value is None: if self.cleaned_data.get('payment_term_mode') == 'days' and value is None:
@@ -1582,10 +1635,15 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering) self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering)
for k, v in list(self.fields.items()): for k, v in list(self.fields.items()):
if k.endswith('_attendee') and not event.settings.attendee_emails_asked: if k.endswith('_attendee'):
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter if not event.settings.attendee_emails_asked:
# the user interface with it # If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
del self.fields[k] # the user interface with it
del self.fields[k]
elif 'subject' in k and k.replace("subject", "send") in self.fields:
v.widget.attrs["data-display-dependency"] = f'#id_{k.replace("subject", "send")}'
elif 'text' in k and k.replace("text", "send") in self.fields:
v.widget.attrs["data-display-dependency"] = f'#id_{k.replace("text", "send")}'
class TicketSettingsForm(SettingsForm): class TicketSettingsForm(SettingsForm):
+3 -3
View File
@@ -105,12 +105,12 @@ class GlobalSettingsForm(SettingsForm):
domain=settings.SITE_URL domain=settings.SITE_URL
) )
)), )),
('widget_vite_origins', forms.CharField( ('widget_vue2_origins', forms.CharField(
widget=forms.Textarea(attrs={'rows': '3'}), widget=forms.Textarea(attrs={'rows': '3'}),
required=False, required=False,
# Not translated on purpose, this is a temporary feature and contains too many special case words # Not translated on purpose, this is a temporary feature and contains too many special case words
label="Vite widget origins", label="Vue2 widget origins",
help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the new vite-based widget.", help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the old vue2-based widget.",
)) ))
]) ])
responses = register_global_settings.send(self) responses = register_global_settings.send(self)
+12 -3
View File
@@ -153,11 +153,19 @@ class QuestionForm(I18nModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['items'].queryset = self.instance.event.items.all() if self.instance.container_type == Question.ContainerType.ORDERPOSITION:
self.fields['items'].required = True self.fields['items'].queryset = self.instance.event.items.all()
self.fields['items'].required = True
else:
del self.fields['items']
del self.fields['ask_during_checkin']
del self.fields['show_during_checkin']
del self.fields['print_on_invoice']
self.fields['dependency_question'].widget.attrs['data-container-type'] = self.instance.container_type
self.fields['dependency_question'].queryset = self.instance.event.questions.filter( self.fields['dependency_question'].queryset = self.instance.event.questions.filter(
type__in=(Question.TYPE_BOOLEAN, Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE), type__in=(Question.TYPE_BOOLEAN, Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE),
ask_during_checkin=False ask_during_checkin=False,
container_type=self.instance.container_type,
) )
if self.instance.pk: if self.instance.pk:
self.fields['dependency_question'].queryset = self.fields['dependency_question'].queryset.exclude( self.fields['dependency_question'].queryset = self.fields['dependency_question'].queryset.exclude(
@@ -243,6 +251,7 @@ class QuestionForm(I18nModelForm):
'valid_date_min', 'valid_date_min',
'valid_date_max', 'valid_date_max',
'valid_file_portrait', 'valid_file_portrait',
'valid_string_length_min',
'valid_string_length_max', 'valid_string_length_max',
] ]
widgets = { widgets = {
+2
View File
@@ -87,6 +87,7 @@ class RRuleForm(forms.Form):
('1', pgettext_lazy('rrule', 'first')), ('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')), ('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')), ('3', pgettext_lazy('rrule', 'third')),
('4', pgettext_lazy('rrule', 'fourth')),
('-1', pgettext_lazy('rrule', 'last')), ('-1', pgettext_lazy('rrule', 'last')),
], ],
required=False required=False
@@ -134,6 +135,7 @@ class RRuleForm(forms.Form):
('1', pgettext_lazy('rrule', 'first')), ('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')), ('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')), ('3', pgettext_lazy('rrule', 'third')),
('4', pgettext_lazy('rrule', 'fourth')),
('-1', pgettext_lazy('rrule', 'last')), ('-1', pgettext_lazy('rrule', 'last')),
], ],
required=False required=False
+8 -8
View File
@@ -94,14 +94,14 @@ class SubEventBulkForm(SubEventForm):
label=_('Start of presale'), label=_('Start of presale'),
help_text=_('Optional. No products will be sold before this date.'), help_text=_('Optional. No products will be sold before this date.'),
required=False, required=False,
limit_choices=('date_from', 'date_to'), limit_choices=('event__date_from', 'event__date_to'),
) )
rel_presale_end = RelativeDateTimeField( rel_presale_end = RelativeDateTimeField(
label=_('End of presale'), label=_('End of presale'),
help_text=_('Optional. No products will be sold after this date. If you do not set this value, the presale ' help_text=_('Optional. No products will be sold after this date. If you do not set this value, the presale '
'will end after the end date of your event.'), 'will end after the end date of your event.'),
required=False, required=False,
limit_choices=('date_from', 'date_to'), limit_choices=('event__date_from', 'event__date_to'),
) )
skip_if_overlap = forms.BooleanField( skip_if_overlap = forms.BooleanField(
label=pgettext_lazy('subevent', 'Skip dates that overlap with any existing date'), label=pgettext_lazy('subevent', 'Skip dates that overlap with any existing date'),
@@ -333,12 +333,12 @@ class BulkSubEventItemForm(SubEventItemForm):
rel_available_from = RelativeDateTimeField( rel_available_from = RelativeDateTimeField(
label=_('Available from'), label=_('Available from'),
required=False, required=False,
limit_choices=('date_from', 'date_to'), limit_choices=('event__date_from', 'event__date_to'),
) )
rel_available_until = RelativeDateTimeField( rel_available_until = RelativeDateTimeField(
label=_('Available until'), label=_('Available until'),
required=False, required=False,
limit_choices=('date_from', 'date_to'), limit_choices=('event__date_from', 'event__date_to'),
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -355,12 +355,12 @@ class BulkSubEventItemVariationForm(SubEventItemVariationForm):
rel_available_from = RelativeDateTimeField( rel_available_from = RelativeDateTimeField(
label=_('Available from'), label=_('Available from'),
required=False, required=False,
limit_choices=('date_from', 'date_to'), limit_choices=('event__date_from', 'event__date_to'),
) )
rel_available_until = RelativeDateTimeField( rel_available_until = RelativeDateTimeField(
label=_('Available_until'), label=_('Available_until'),
required=False, required=False,
limit_choices=('date_from', 'date_to'), limit_choices=('event__date_from', 'event__date_to'),
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -435,10 +435,10 @@ class SubEventMetaValueForm(forms.ModelForm):
if self.disabled: if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly' self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_slug(self): def clean_value(self):
if self.disabled: if self.disabled:
return self.instance.value if self.instance else None return self.instance.value if self.instance else None
return self.cleaned_data['slug'] return self.cleaned_data['value']
class Meta: class Meta:
model = SubEventMetaValue model = SubEventMetaValue
+5
View File
@@ -717,6 +717,10 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'), 'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'), 'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'),
'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'), 'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'),
'pretix.property.created': _('An organizer meta property has been created.'),
'pretix.property.deleted': _('An organizer meta property has been deleted.'),
'pretix.property.changed': _('An organizer meta property has been changed.'),
'pretix.property.reordered': _('An organizer meta property has been reordered.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'), 'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'), 'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'), 'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
@@ -774,6 +778,7 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'), 'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'), 'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'), 'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
'pretix.user.settings.2fa.resetdrift': _('Drift and throttle values for two-factor devices have been reset.'),
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to ' 'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
'your account.'), 'your account.'),
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed ' 'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
+2 -2
View File
@@ -85,8 +85,8 @@ class PermissionMiddleware:
"user.settings.2fa.enable", "user.settings.2fa.enable",
"user.settings.2fa.disable", "user.settings.2fa.disable",
"user.settings.2fa.regenemergency", "user.settings.2fa.regenemergency",
"user.settings.2fa.confirm.totp", "user.settings.2fa.confirm.otp_totp.totpdevice",
"user.settings.2fa.confirm.webauthn", "user.settings.2fa.confirm.pretixbase.webauthndevice",
"user.settings.2fa.delete", "user.settings.2fa.delete",
"user.settings.2fa.leaveteams", "user.settings.2fa.leaveteams",
"auth.logout", "auth.logout",
@@ -45,8 +45,8 @@
</p> </p>
<div class="form-group buttons"> <div class="form-group buttons">
<input type="submit" class="btn btn-large btn-default" value="Cancel"/> <input type="submit" class="btn btn-large btn-default" value="{% trans "Cancel" %}"/>
<input type="submit" class="btn btn-large btn-primary" name="allow" value="Authorize"/> <input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans "Authorize" %}"/>
</div> </div>
</form> </form>
{% else %} {% else %}
@@ -58,6 +58,7 @@
<script type="text/javascript" src="{% static "leaflet/leaflet.js" %}"></script> <script type="text/javascript" src="{% static "leaflet/leaflet.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/geo.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/geo.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/reldate.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "sortable/Sortable.js" %}"></script> <script type="text/javascript" src="{% static "sortable/Sortable.js" %}"></script>
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script> <script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
@@ -2,9 +2,10 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load getitem %}
{% block inside %} {% block inside %}
<h1>{% trans "Payment settings" %}</h1> <h1>{% trans "Payment settings" %}</h1>
<form action="" method="post" class="form-horizontal form-plugins"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<div class="tabbed-form"> <div class="tabbed-form">
<fieldset> <fieldset>
@@ -71,20 +72,44 @@
{% bootstrap_form_errors form layout="control" %} {% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.payment_term_mode layout="control" %} {% bootstrap_field form.payment_term_mode layout="control" %}
{% bootstrap_field form.payment_term_days layout="control" %} {% bootstrap_field form.payment_term_days layout="control" %}
{% bootstrap_field form.payment_term_weekdays layout="control" %}
{% bootstrap_field form.payment_term_minutes layout="control" %} {% bootstrap_field form.payment_term_minutes layout="control" %}
{% bootstrap_field form.payment_term_weekdays layout="control" %}
{% bootstrap_field form.payment_term_last layout="control" %} {% bootstrap_field form.payment_term_last layout="control" %}
{% bootstrap_field form.payment_term_expire_automatically layout="control" %} {% bootstrap_field form.payment_term_expire_automatically layout="control" %}
{% trans "days" context "unit" as days %} {% trans "days" context "unit" as days %}
{% bootstrap_field form.payment_term_expire_delay_days layout="control" addon_after=days %} {% bootstrap_field form.payment_term_expire_delay_days layout="control" addon_after=days %}
{% bootstrap_field form.payment_term_accept_late layout="control" %} {% bootstrap_field form.payment_term_accept_late layout="control" %}
{% bootstrap_field form.payment_pending_hidden layout="control" %} {% bootstrap_field form.payment_pending_hidden layout="control" %}
{% for c, fields in form.term_channel_fields.items %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.type_instance.verbose_name }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.type_instance.verbose_name }}"></span>
{% endif %}
{{ c.label }}
</h3>
</div>
<div class="panel-body">
{% for f in fields %}
{% bootstrap_field form|getitem:f layout="control" %}
{% endfor %}
</div>
</div>
{% endfor %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Advanced" %}</legend> <legend>{% trans "Advanced" %}</legend>
{% bootstrap_form_errors form layout="control" %} {% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.tax_rule_payment layout="control" %} {% bootstrap_field form.tax_rule_payment layout="control" %}
{% bootstrap_field form.payment_explanation layout="control" %} {% bootstrap_field form.payment_explanation layout="control" %}
{% bootstrap_field form.payment_choice_postpone_allowed_channels layout="control" %}
</fieldset> </fieldset>
</div> </div>
{% if "event.settings.payment:write" in request.eventpermset %} {% if "event.settings.payment:write" in request.eventpermset %}
@@ -275,6 +275,9 @@
{% bootstrap_field sform.event_calendar_future_only layout="control" %} {% bootstrap_field sform.event_calendar_future_only layout="control" %}
{% endif %} {% endif %}
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %} {% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
{% if sform.event_list_empty_text %}
{% bootstrap_field sform.event_list_empty_text layout="control" %}
{% endif %}
<h4>{% trans "Order details" %}</h4> <h4>{% trans "Order details" %}</h4>
@@ -13,9 +13,11 @@
{% blocktrans with url=edit_url|add:"#tab-0-1-open" %}If you want to keep the answers, <a href="{{url}}">edit the question</a> and set it to hidden.{% endblocktrans %} {% blocktrans with url=edit_url|add:"#tab-0-1-open" %}If you want to keep the answers, <a href="{{url}}">edit the question</a> and set it to hidden.{% endblocktrans %}
</p> </p>
</div> </div>
{% for item in dependent %} <ul>
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item }}</a></li> {% for item in dependent %}
{% endfor %} <li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item }}</a></li>
{% endfor %}
</ul>
{% endif %} {% endif %}
<div class="form-group submit-group"> <div class="form-group submit-group">
<a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel"> <a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
@@ -24,7 +24,9 @@
<legend>{% trans "General" %}</legend> <legend>{% trans "General" %}</legend>
{% bootstrap_field form.question layout="control" %} {% bootstrap_field form.question layout="control" %}
{% bootstrap_field form.type layout="control" %} {% bootstrap_field form.type layout="control" %}
{% bootstrap_field form.items layout="control" %} {% if form.items %}
{% bootstrap_field form.items layout="control" %}
{% endif %}
{% bootstrap_field form.required layout="control" %} {% bootstrap_field form.required layout="control" %}
<div class="alert alert-info alert-required-boolean"> <div class="alert alert-info alert-required-boolean">
{% blocktrans trimmed %} {% blocktrans trimmed %}
@@ -45,6 +47,7 @@
{% bootstrap_field form.valid_datetime_max layout="control" %} {% bootstrap_field form.valid_datetime_max layout="control" %}
</div> </div>
<div id="valid-string"> <div id="valid-string">
{% bootstrap_field form.valid_string_length_min layout="control" %}
{% bootstrap_field form.valid_string_length_max layout="control" %} {% bootstrap_field form.valid_string_length_max layout="control" %}
</div> </div>
<div id="valid-file"> <div id="valid-file">
@@ -128,10 +131,16 @@
<legend>{% trans "Advanced" %}</legend> <legend>{% trans "Advanced" %}</legend>
{% bootstrap_field form.help_text layout="control" %} {% bootstrap_field form.help_text layout="control" %}
{% bootstrap_field form.identifier layout="control" %} {% bootstrap_field form.identifier layout="control" %}
{% bootstrap_field form.ask_during_checkin layout="control" %} {% if form.ask_during_checkin %}
{% bootstrap_field form.show_during_checkin layout="control" %} {% bootstrap_field form.ask_during_checkin layout="control" %}
{% endif %}
{% if form.show_during_checkin %}
{% bootstrap_field form.show_during_checkin layout="control" %}
{% endif %}
{% bootstrap_field form.hidden layout="control" %} {% bootstrap_field form.hidden layout="control" %}
{% bootstrap_field form.print_on_invoice layout="control" %} {% if form.print_on_invoice %}
{% bootstrap_field form.print_on_invoice layout="control" %}
{% endif %}
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label" for="id_dependency_question"> <label class="col-md-3 control-label" for="id_dependency_question">
@@ -10,11 +10,26 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% csrf_token %} {% csrf_token %}
{% if 'event.items:write' in request.eventpermset %}
<p> {% if request.event.settings.feature_flag_order_level_questions %}
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new question" %} {% if 'event.items:write' in request.eventpermset %}
</a> <p>
</p> <a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new per-ticket question" %}
</a>
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=O" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new order-level question" %}
</a>
</p>
{% endif %}
<h2>{% trans "Per-ticket questions" %}</h2>
<p>{% trans "These questions are asked for every ticket, so possibly multiple times in the same order." %}</p>
{% else %}
{% if 'event.items:write' in request.eventpermset %}
<p>
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new question" %}
</a>
</p>
{% endif %}
{% endif %} {% endif %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-quotas"> <table class="table table-hover table-quotas">
@@ -32,8 +47,8 @@
<th class="action-col-2"></th> <th class="action-col-2"></th>
</tr> </tr>
</thead> </thead>
<tbody data-dnd-url="{% url "control:event.items.questions.reorder" organizer=request.event.organizer.slug event=request.event.slug %}"> <tbody data-dnd-url="{% url "control:event.items.questions.reorder" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P">
{% for q in questions %} {% for q in questions %}{% if q.container_type == "P" %}
<tr data-dnd-id="{{ q.id }}"> <tr data-dnd-id="{{ q.id }}">
<td> <td>
<strong> <strong>
@@ -63,7 +78,6 @@
{% if q.pk and q.ask_during_checkin %} {% if q.pk and q.ask_during_checkin %}
<span class="fa fa-check-square text-muted" data-toggle="tooltip" title="{% trans "Ask during check-in" %}"></span> <span class="fa fa-check-square text-muted" data-toggle="tooltip" title="{% trans "Ask during check-in" %}"></span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if q.pk and q.hidden %} {% if q.pk and q.hidden %}
@@ -102,8 +116,90 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endif %}{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% if request.event.settings.feature_flag_order_level_questions %}
<h2>
{% trans "Per-order questions" %}
<small><span class="label label-info" title="
{% trans "This functionality is in active development and expected to change significantly over the coming months." %}
{% trans "Per-order questions are currently not supported and will not be displayed in pretixPOS." %}
" data-toggle="tooltip">
<span class="fa fa-flask" aria-hidden="true"></span>
{% trans "Experimental feature" %}
</span></small>
</h2>
<p>{% trans "These questions are asked once per order." %}</p>
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Question" %}</th>
<th>{% trans "Type" %}</th>
<th class="iconcol"></th>
<th class="iconcol"></th>
<th class="iconcol"></th>
{% if 'event.items:write' in request.eventpermset %}
<th class="action-col-2"></th>
{% endif %}
<th class="action-col-2"></th>
</tr>
</thead>
<tbody data-dnd-url="{% url "control:event.items.questions.reorder" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=O">
{% for q in questions %}{% if q.container_type == "O" %}
<tr data-dnd-id="{{ q.id }}">
<td>
<strong>
{{ q.question }}
</strong><br>
<small class="text-muted">{{ q.identifier }}</small>
</td>
<td>
{% if q.pk %}
{{ q.get_type_display }}
{% else %}
{% trans "System question" %}
{% endif %}
</td>
<td>
{% if q.required %}
<span class="fa fa-exclamation-circle text-muted" data-toggle="tooltip" title="{% trans "Required question" %}"></span>
{% endif %}
</td>
<td>
{% if q.pk and q.ask_during_checkin %}
<span class="fa fa-check-square text-muted" data-toggle="tooltip" title="{% trans "Ask during check-in" %}"></span>
{% endif %}
</td>
<td>
{% if q.pk and q.hidden %}
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Hidden question" %}"></span>
{% endif %}
</td>
{% if 'event.items:write' in request.eventpermset %}
<td class="dnd-container">
</td>
{% endif %}
<td class="text-right flip">
{% if q.pk %}
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
{% else %}
{% if 'event.settings.general:write' in request.eventpermset %}
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
{% endif %}
{% endif %}
</td>
</tr>
{% endif %}{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %} {% endblock %}
@@ -35,6 +35,18 @@
</div> </div>
</details> </details>
{% endif %} {% endif %}
{% if order_questions_form.fields %}
<details class="panel panel-default" open>
<summary class="panel-heading">
<h4 class="panel-title">
<strong>{% trans "Additional order information" %}</strong>
</h4>
</summary>
<div class="panel-body">
{% bootstrap_form order_questions_form layout="horizontal" %}
</div>
</details>
{% endif %}
{% for pos, forms in formgroups %} {% for pos, forms in formgroups %}
<details class="panel panel-default" open> <details class="panel panel-default" open>
<summary class="panel-heading"> <summary class="panel-heading">
@@ -0,0 +1,28 @@
{% load safelink %}
{% load i18n %}
{% load rich_text %}
{% if answer %}
{% if answer.file %}
<span class="fa fa-file"></span>
<a href="{{ answer.backend_file_url }}?token={% answer_token request answer %}">
{{ answer.file_name }}
</a>
<span class="label label-danger" data-toggle="tooltip"
title="{% trans "This file has been uploaded by a user and could contain viruses or other malicious content." %}">
{% trans "UNSAFE" %}
</span>
{% if answer.is_image %}
<br>
<a href="{{ answer.backend_file_url }}?token={% answer_token request answer %}" data-lightbox="order"
class="answer-thumb">
<img src="{{ answer.backend_file_url }}?token={% answer_token request answer %}">
</a>
{% endif %}
{% elif question.type == "M" %}
{{ answer.to_string_i18n|rich_text_snippet }}
{% else %}
{{ answer.to_string_i18n|linebreaksbr }}
{% endif %}
{% else %}
<em>{% trans "not answered" %}</em>
{% endif %}
@@ -427,6 +427,14 @@
</form> </form>
</dd> </dd>
{% endif %} {% endif %}
{% if order.answers.all %}
<hr>
{% endif %}
{% for ans in order.answers.all %}
<dt>{{ ans.question.internal_name|default:ans.question.question }}</dt>
<dd>{% include "pretixcontrol/order/fragment_question_answer.html" with request=request question=ans.question answer=ans %}</dd>
{% endfor %}
</dl> </dl>
</div> </div>
</div> </div>
@@ -664,31 +672,7 @@
{% endif %} {% endif %}
</dt> </dt>
<dd> <dd>
{% if q.answer %} {% include "pretixcontrol/order/fragment_question_answer.html" with request=request question=q answer=q.answer %}
{% if q.answer.file %}
<span class="fa fa-file"></span>
<a href="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}">
{{ q.answer.file_name }}
</a>
<span class="label label-danger" data-toggle="tooltip"
title="{% trans "This file has been uploaded by a user and could contain viruses or other malicious content." %}">
{% trans "UNSAFE" %}
</span>
{% if q.answer.is_image %}
<br>
<a href="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}" data-lightbox="order"
class="answer-thumb">
<img src="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}">
</a>
{% endif %}
{% elif q.type == "M" %}
{{ q.answer.to_string_i18n|rich_text_snippet }}
{% else %}
{{ q.answer.to_string_i18n|linebreaksbr }}
{% endif %}
{% else %}
<em>{% trans "not answered" %}</em>
{% endif %}
</dd> </dd>
{% endfor %} {% endfor %}
{% for q in line.additional_fields %} {% for q in line.additional_fields %}
@@ -15,7 +15,7 @@
<div> <div>
<div class="big-radio radio"> <div class="big-radio radio">
<label> <label>
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}> <input type="radio" required value="otp_totp.totpdevice" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "otp_totp.totpdevice" %}checked{% endif %}>
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br> <strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
<div class="help-block"> <div class="help-block">
{% blocktrans trimmed %} {% blocktrans trimmed %}
@@ -26,7 +26,7 @@
</div> </div>
<div class="big-radio radio"> <div class="big-radio radio">
<label> <label>
<input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}> <input type="radio" required value="pretixbase.webauthndevice" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "pretixbase.webauthndevice" %}checked{% endif %}>
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br> <strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
<div class="help-block"> <div class="help-block">
{% blocktrans trimmed %} {% blocktrans trimmed %}
@@ -116,14 +116,14 @@
{% for d in devices %} {% for d in devices %}
<li class="list-group-item"> <li class="list-group-item">
<a class="btn btn-danger btn-xs pull-right flip" <a class="btn btn-danger btn-xs pull-right flip"
href="{% url "control:user.settings.2fa.delete" devicetype=d.devicetype device=d.pk %}"> href="{% url "control:user.settings.2fa.delete" devicetype=d.model_label device=d.pk %}">
Delete Delete
</a> </a>
{% if d.devicetype == "totp" %} {% if d.model_label == "otp_totp.totpdevice" %}
<span class="fa fa-mobile"></span> <span class="fa fa-mobile"></span>
{% elif d.devicetype == "webauthn" %} {% elif d.model_label == "pretixbase.webauthndevice" %}
<span class="fa fa-usb"></span> <span class="fa fa-usb"></span>
{% elif d.devicetype == "u2f" %} {% elif d.model_label == "pretixbase.u2fdevice" %}
<span class="fa fa-usb"></span> <span class="fa fa-usb"></span>
{% endif %} {% endif %}
{{ d.name }} {{ d.name }}
@@ -1,6 +1,7 @@
{% extends "pretixcontrol/base.html" %} {% extends "pretixcontrol/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load icon %}
{% block title %}{% trans "User" %}{% endblock %} {% block title %}{% trans "User" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "User" %} {{ user.email }}</h1> <h1>{% trans "User" %} {{ user.email }}</h1>
@@ -59,8 +60,83 @@
{% bootstrap_field form.is_verified layout='control' %} {% bootstrap_field form.is_verified layout='control' %}
{% endif %} {% endif %}
{% bootstrap_field form.last_login layout='control' %} {% bootstrap_field form.last_login layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}
{% bootstrap_field form.needs_password_change layout='control' %} {% bootstrap_field form.needs_password_change layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<button class="btn btn-default btn-xs pull-right" type="submit" form="resetdriftthrottle">
{% trans "Reset drift and throttle" %}
</button>
<h3 class="panel-title">
{% trans "Available two-factor authentication methods" %}
</h3>
</div>
<table class="panel-body table table-hover">
{% for d in devices %}
<tr>
<td>
{% if d.model_label == 'otp_totp.totpdevice' %}
TOTP
{% elif d.model_label == 'pretixbase.u2fdevice' %}
U2F
{% elif d.model_label == 'pretixbase.webauthndevice' %}
WebAuthn
{% elif d.model_label == 'otp_static.staticdevice' %}
{% trans "Emergency tokens" %}
{% endif %}
{% if d.confirmed %}
{% icon "check" %}
{% else %}
{% icon "warning" %}
{% endif %}
</td>
<td>
{{ d.name }}
</td>
<td>
{% if d.throttling_failure_timestamp %}
{% blocktrans trimmed with date=d.throttling_failure_timestamp|date:"SHORT_DATETIME_FORMAT" count cnt=d.throttling_failure_count %}
1 failed attempt since {{ date }}
{% plural %}
{{ cnt }} failed attempts since {{ date }}
{% endblocktrans %}
<br>
{% endif %}
{% if d.throttling_enabled and not d.verify_is_allowed.0 %}
<strong>
{% blocktrans trimmed with date=d.verify_is_allowed.1.locked_until|date:"SHORT_DATETIME_FORMAT" %}
Currently locked until {{ date }}
{% endblocktrans %}
</strong>
<br>
{% endif %}
{% if d.model_label == 'otp_totp.totpdevice' %}
<small>
<code>step = {{ d.step }},
t0 = {{ d.t0 }},
digits = {{ d.digits }},
tolerance = {{ d.tolerance }},
drift = {{ d.drift }},
last_t = {{ d.last_t }}</code>
</small>
{% elif d.model_label == 'pretixbase.u2fdevice' %}
<small>
<code>sign_count = {{ d.sign_count }}</code>
</small>
{% elif d.model_label == 'otp_static.staticdevice' %}
<small>
<code>token_count = {{ d.token_set.count }}</code>
</small>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Team memberships" %}</legend> <legend>{% trans "Team memberships" %}</legend>
@@ -102,4 +178,8 @@
</div> </div>
</div> </div>
</div> </div>
<form action="{% url "control:users.resetdriftthrottle" id=user.pk %}" id="resetdriftthrottle" method="post">
{% csrf_token %}
</form>
{% endblock %} {% endblock %}
+3 -2
View File
@@ -78,6 +78,7 @@ urlpatterns = [
re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'), re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'),
re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'), re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'),
re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'), re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'),
re_path(r'^users/(?P<id>\d+)/resetdriftthrottle$', users.Reset2FADriftThrottleView.as_view(), name='users.resetdriftthrottle'),
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'), re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'), re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'), re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
@@ -106,9 +107,9 @@ urlpatterns = [
re_path(r'^settings/2fa/regenemergency', user.User2FARegenerateEmergencyView.as_view(), re_path(r'^settings/2fa/regenemergency', user.User2FARegenerateEmergencyView.as_view(),
name='user.settings.2fa.regenemergency'), name='user.settings.2fa.regenemergency'),
re_path(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(), re_path(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
name='user.settings.2fa.confirm.totp'), name='user.settings.2fa.confirm.otp_totp.totpdevice'),
re_path(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(), re_path(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(),
name='user.settings.2fa.confirm.webauthn'), name='user.settings.2fa.confirm.pretixbase.webauthndevice'),
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(), re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
name='user.settings.2fa.delete'), 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/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'),
+53 -15
View File
@@ -35,6 +35,7 @@
import base64 import base64
import json import json
import logging import logging
import math
import time import time
from urllib.parse import quote, urljoin, urlparse from urllib.parse import quote, urljoin, urlparse
@@ -50,11 +51,12 @@ from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, ngettext
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django_otp import match_token from django_otp import devices_for_user
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from webauthn.helpers import generate_challenge from webauthn.helpers import generate_challenge
@@ -64,6 +66,7 @@ from pretix.base.forms.auth import (
) )
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.helpers import OF_SELF
from pretix.helpers.http import get_client_ip, redirect_to_url from pretix.helpers.http import get_client_ip, redirect_to_url
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
from pretix.helpers.security import handle_login_source, session_login from pretix.helpers.security import handle_login_source, session_login
@@ -395,15 +398,17 @@ class Recover(TemplateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if self.form.is_valid(): if self.form.is_valid():
try: with transaction.atomic():
user = User.objects.get(id=self.request.GET.get('id'), auth_backend='native') # Check token in transaction to prevent race condition
except User.DoesNotExist: try:
return self.invalid('unknownuser') user = User.objects.select_for_update(of=OF_SELF).get(id=self.request.GET.get('id'), auth_backend='native')
if not default_token_generator.check_token(user, self.request.GET.get('token')): except User.DoesNotExist:
return self.invalid('invalid') return self.invalid('unknownuser')
user.set_password(self.form.cleaned_data['password']) if not default_token_generator.check_token(user, self.request.GET.get('token')):
user.needs_password_change = False return self.invalid('invalid')
user.save() user.set_password(self.form.cleaned_data['password'])
user.needs_password_change = False
user.save()
messages.success(request, _('You can now login using your new password.')) messages.success(request, _('You can now login using your new password.'))
user.log_action('pretix.control.auth.user.forgot_password.recovered') user.log_action('pretix.control.auth.user.forgot_password.recovered')
@@ -460,6 +465,7 @@ class Login2FAView(TemplateView):
token = request.POST.get('token', '').strip().replace(' ', '') token = request.POST.get('token', '').strip().replace(' ', '')
valid = False valid = False
retry_after = None
if 'webauthn_challenge' in self.request.session and token.startswith('{'): if 'webauthn_challenge' in self.request.session and token.startswith('{'):
challenge = self.request.session['webauthn_challenge'] challenge = self.request.session['webauthn_challenge']
@@ -515,12 +521,28 @@ class Login2FAView(TemplateView):
valid = True valid = True
break break
else: else:
valid = match_token(self.user, token) with transaction.atomic():
if isinstance(valid, StaticDevice): for device in devices_for_user(self.user, for_verify=True):
if isinstance(device, StaticDevice) and len(token) < 12:
# If we enter a wrong TOTP token (which is 6 characters), do not even try if it is a valid
# emergency token, which will only "lock up" the StaticDevice due to the throttling plugin
# and just locks people out without security gain.
continue
if device.verify_token(token):
valid = True
break
elif hasattr(device, 'verify_is_allowed'):
verify_allowed, reason_dict = device.verify_is_allowed()
if not verify_allowed:
if not retry_after or reason_dict['locked_until'] > retry_after:
retry_after = reason_dict['locked_until']
else:
device = None
if isinstance(device, StaticDevice):
self.user.send_security_notice([ self.user.send_security_notice([
_("A recovery code for two-factor authentification was used to log in.") _("A recovery code for two-factor authentification was used to log in.")
]) ])
if valid: if valid:
logger.info(f"Backend login successful for user {self.user.pk} with 2FA.") logger.info(f"Backend login successful for user {self.user.pk} with 2FA.")
pretix_successful_logins.inc(1) pretix_successful_logins.inc(1)
@@ -533,7 +555,23 @@ class Login2FAView(TemplateView):
return redirect('control:index') return redirect('control:index')
else: else:
pretix_failed_logins.inc(1, reason="2fa") pretix_failed_logins.inc(1, reason="2fa")
messages.error(request, _('Invalid code, please try again.')) msg = _('Invalid code, please try again.')
if retry_after:
seconds = (retry_after - now()).total_seconds()
minutes = seconds / 60
if minutes >= 1:
msg = ngettext(
'Invalid code. Please try again after waiting {value} minute.',
'Invalid code. Please try again after waiting {value} minutes.',
minutes,
).format(value=math.ceil(minutes))
elif seconds >= 1:
msg = ngettext(
'Invalid code. Please try again after waiting {value} second.',
'Invalid code. Please try again after waiting {value} seconds.',
seconds,
).format(value=math.ceil(seconds))
messages.error(request, msg)
return redirect('control:auth.login.2fa') return redirect('control:auth.login.2fa')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
+3
View File
@@ -163,6 +163,9 @@ class DiscountCreate(EventPermissionRequiredMixin, CreateView):
i = modelcopy(self.copy_from) i = modelcopy(self.copy_from)
i.pk = None i.pk = None
kwargs['instance'] = i kwargs['instance'] = i
kwargs["initial"]["limit_sales_channels"] = self.copy_from.limit_sales_channels.all()
kwargs["initial"]["condition_limit_products"] = self.copy_from.condition_limit_products.all()
kwargs["initial"]["benefit_limit_products"] = self.copy_from.benefit_limit_products.all()
else: else:
kwargs['instance'] = Discount(event=self.request.event) kwargs['instance'] = Discount(event=self.request.event)
+23 -93
View File
@@ -65,6 +65,7 @@ from pretix.api.serializers.item import (
ItemVariationSerializer, ItemVariationSerializer,
) )
from pretix.base.forms import I18nFormSet from pretix.base.forms import I18nFormSet
from pretix.base.forms.questions import get_fake_attendee_questions
from pretix.base.models import ( from pretix.base.models import (
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, LogEntry, CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, LogEntry,
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota, OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
@@ -426,7 +427,7 @@ def reorder_categories(request, organizer, event):
FakeQuestion = namedtuple( FakeQuestion = namedtuple(
'FakeQuestion', 'id question position required' 'FakeQuestion', 'id question position required container_type'
) )
@@ -440,85 +441,8 @@ class QuestionList(ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
questions = []
if self.request.event.settings.attendee_names_asked: questions = get_fake_attendee_questions(self.request.event.settings)
questions.append(
FakeQuestion(
id='attendee_name_parts',
question=_('Attendee name'),
position=self.request.event.settings.system_question_order.get(
'attendee_name_parts', 0
),
required=self.request.event.settings.attendee_names_required,
)
)
if self.request.event.settings.attendee_emails_asked:
questions.append(
FakeQuestion(
id='attendee_email',
question=_('Attendee email'),
position=self.request.event.settings.system_question_order.get(
'attendee_email', 0
),
required=self.request.event.settings.attendee_emails_required,
)
)
if self.request.event.settings.attendee_company_asked:
questions.append(
FakeQuestion(
id='company',
question=_('Company'),
position=self.request.event.settings.system_question_order.get(
'company', 0
),
required=self.request.event.settings.attendee_company_required,
)
)
if self.request.event.settings.attendee_addresses_asked:
questions.append(
FakeQuestion(
id='street',
question=_('Street'),
position=self.request.event.settings.system_question_order.get(
'street', 0
),
required=self.request.event.settings.attendee_addresses_required,
)
)
questions.append(
FakeQuestion(
id='zipcode',
question=_('ZIP code'),
position=self.request.event.settings.system_question_order.get(
'zipcode', 0
),
required=self.request.event.settings.attendee_addresses_required,
)
)
questions.append(
FakeQuestion(
id='city',
question=_('City'),
position=self.request.event.settings.system_question_order.get(
'city', 0
),
required=self.request.event.settings.attendee_addresses_required,
)
)
questions.append(
FakeQuestion(
id='country',
question=_('Country'),
position=self.request.event.settings.system_question_order.get(
'country', 0
),
required=self.request.event.settings.attendee_addresses_required,
)
)
questions += list(ctx['questions']) questions += list(ctx['questions'])
questions.sort(key=lambda q: q.position) questions.sort(key=lambda q: q.position)
@@ -535,14 +459,16 @@ def reorder_questions(request, organizer, event):
except (JSONDecodeError, KeyError, ValueError): except (JSONDecodeError, KeyError, ValueError):
return HttpResponseBadRequest("expected JSON: {ids:[]}") return HttpResponseBadRequest("expected JSON: {ids:[]}")
qs = request.event.questions.filter(container_type=request.GET['container_type'])
# filter system_questions - normal questions are int/digit, system_questions strings # filter system_questions - normal questions are int/digit, system_questions strings
custom_question_ids = [i for i in ids if i.isdigit()] custom_question_ids = [i for i in ids if i.isdigit()]
input_questions = list(request.event.questions.filter(id__in=custom_question_ids)) input_questions = list(qs.filter(id__in=custom_question_ids))
if len(input_questions) != len(custom_question_ids): if len(input_questions) != len(custom_question_ids):
raise Http404(_("Some of the provided object ids are invalid.")) raise Http404(_("Some of the provided object ids are invalid."))
if len(input_questions) != request.event.questions.count(): if len(input_questions) != qs.count():
raise Http404(_("Not all objects have been selected.")) raise Http404(_("Not all objects have been selected."))
for q in input_questions: for q in input_questions:
@@ -556,18 +482,19 @@ def reorder_questions(request, organizer, event):
} }
) )
system_question_order = {} if request.GET['container_type'] == Question.ContainerType.ORDERPOSITION:
for s in ('attendee_name_parts', 'attendee_email', 'company', 'street', 'zipcode', 'city', 'country'): system_question_order = {}
if s in ids: for s in ('attendee_name_parts', 'attendee_email', 'company', 'street', 'zipcode', 'city', 'country'):
system_question_order[s] = ids.index(s) if s in ids:
else: system_question_order[s] = ids.index(s)
system_question_order[s] = -1 else:
request.event.settings.system_question_order = system_question_order system_question_order[s] = -1
request.event.log_action( request.event.settings.system_question_order = system_question_order
'pretix.event.settings', user=request.user, data={ request.event.log_action(
'system_question_order': system_question_order, 'pretix.event.settings', user=request.user, data={
} 'system_question_order': system_question_order,
) }
)
return HttpResponse() return HttpResponse()
@@ -805,6 +732,9 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['instance'] = Question(event=self.request.event) kwargs['instance'] = Question(event=self.request.event)
kwargs['instance'].container_type = self.request.GET.get('container_type', Question.ContainerType.ORDERPOSITION)
if kwargs['instance'].container_type not in Question.ContainerType.values:
raise PermissionDenied
return kwargs return kwargs
def get_success_url(self) -> str: def get_success_url(self) -> str:
+36 -8
View File
@@ -1177,6 +1177,8 @@ class OrderRefundView(OrderView):
manual_value = formats.sanitize_separators(manual_value) manual_value = formats.sanitize_separators(manual_value)
try: try:
manual_value = Decimal(manual_value) manual_value = Decimal(manual_value)
if manual_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1206,6 +1208,8 @@ class OrderRefundView(OrderView):
giftcard_value = formats.sanitize_separators(giftcard_value) giftcard_value = formats.sanitize_separators(giftcard_value)
try: try:
giftcard_value = Decimal(giftcard_value) giftcard_value = Decimal(giftcard_value)
if giftcard_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1255,6 +1259,8 @@ class OrderRefundView(OrderView):
offsetting_value = formats.sanitize_separators(offsetting_value) offsetting_value = formats.sanitize_separators(offsetting_value)
try: try:
offsetting_value = Decimal(offsetting_value) offsetting_value = Decimal(offsetting_value)
if offsetting_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1271,6 +1277,9 @@ class OrderRefundView(OrderView):
if offset_order.event.currency != self.request.event.currency: if offset_order.event.currency != self.request.event.currency:
messages.error(self.request, _('You entered an order in an event with a different currency.')) messages.error(self.request, _('You entered an order in an event with a different currency.'))
is_valid = False is_valid = False
if not self.request.user.has_event_permission(self.request.organizer, offset_order.event, 'event.orders:write', request=self.request):
messages.error(self.request, _('You entered an order in an event that you do not have access to.'))
is_valid = False
refunds.append(OrderRefund( refunds.append(OrderRefund(
order=order, order=order,
payment=None, payment=None,
@@ -1286,10 +1295,13 @@ class OrderRefundView(OrderView):
)) ))
for identifier, prov in self.request.event.get_payment_providers().items(): for identifier, prov in self.request.event.get_payment_providers().items():
# prof = process form, not a typo for prov(ider)
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0' prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
prof_value = formats.sanitize_separators(prof_value) prof_value = formats.sanitize_separators(prof_value)
try: try:
prof_value = Decimal(prof_value) prof_value = Decimal(prof_value)
if prof_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1313,6 +1325,8 @@ class OrderRefundView(OrderView):
value = formats.sanitize_separators(value) value = formats.sanitize_separators(value)
try: try:
value = Decimal(value) value = Decimal(value)
if value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1342,7 +1356,12 @@ class OrderRefundView(OrderView):
)) ))
any_success = False any_success = False
if refund_selected == full_refund and is_valid: if refund_selected != full_refund:
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
'amount.'))
is_valid = False
if is_valid:
for r in refunds: for r in refunds:
r.save() r.save()
order.log_action('pretix.event.order.refund.created', { order.log_action('pretix.event.order.refund.created', {
@@ -1414,9 +1433,6 @@ class OrderRefundView(OrderView):
) )
})) }))
return redirect(self.get_order_url()) return redirect(self.get_order_url())
else:
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
'amount.'))
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
if self.start_form.is_valid(): if self.start_form.is_valid():
@@ -1646,7 +1662,8 @@ class OrderCheckVATID(OrderView):
return redirect(self.get_order_url()) return redirect(self.get_order_url())
try: try:
normalized_id = validate_vat_id(ia.vat_id, str(ia.country)) requester_id = self.request.event.settings.invoice_address_from_vat_id
normalized_id = validate_vat_id(ia.vat_id, str(ia.country), requester_id)
with transaction.atomic(): with transaction.atomic():
ia.vat_id_validated = True ia.vat_id_validated = True
ia.vat_id = normalized_id ia.vat_id = normalized_id
@@ -2008,7 +2025,10 @@ class OrderChange(OrderView):
'issued_gift_cards', 'issued_gift_cards',
'addons', 'addons',
).annotate( ).annotate(
_seat_allowed=Exists(SeatCategoryMapping.objects.filter(subevent=OuterRef("subevent"), product=OuterRef("item"))) _seat_allowed=Exists(SeatCategoryMapping.objects.filter(
subevent=OuterRef("subevent") if self.request.event.has_subevents else None,
product=OuterRef("item"))
)
)) ))
for p in positions: for p in positions:
p.form = OrderPositionChangeForm( p.form = OrderPositionChangeForm(
@@ -2226,6 +2246,10 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
only_user_visible = False only_user_visible = False
all_optional = True all_optional = True
@property
def order_question_container(self):
return self.order
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['other_form'] = self.other_form ctx['other_form'] = self.other_form
@@ -2587,7 +2611,11 @@ class AnswerDownload(EventPermissionRequiredMixin, OrderViewMixin, ListView):
answid = kwargs.get('answer') answid = kwargs.get('answer')
token = request.GET.get('token', '') token = request.GET.get('token', '')
answer = get_object_or_404(QuestionAnswer, orderposition__order=self.order, id=answid) answer = get_object_or_404(
QuestionAnswer,
Q(orderposition__order=self.order) | Q(order=self.order),
id=answid
)
if not answer.file: if not answer.file:
raise Http404() raise Http404()
if not check_token(request, answer, token): if not check_token(request, answer, token):
@@ -2597,7 +2625,7 @@ class AnswerDownload(EventPermissionRequiredMixin, OrderViewMixin, ListView):
resp = FileResponse(answer.file, content_type=ftype or 'application/binary') resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format( resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(), self.order.code, self.request.event.slug.upper(), self.order.code,
answer.orderposition.positionid, answer.orderposition.positionid if answer.orderposition else '',
os.path.basename(answer.file.name).split('.', 1)[1] os.path.basename(answer.file.name).split('.', 1)[1]
) )
return resp return resp
+3 -3
View File
@@ -778,9 +778,9 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["events"] = self.request.user.get_events_with_permission( # Assumption: Who has access to modify organizer settings may see all events and disable/enable plugins
"event.settings.general:write", request=self.request # for them. Otherwise, inconsistent situations occur.
).filter(organizer=self.request.organizer) kwargs["events"] = self.request.organizer.events.all()
kwargs["initial"] = { kwargs["initial"] = {
"events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)') "events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)')
} }
+5
View File
@@ -27,6 +27,7 @@ from decimal import Decimal
from io import BytesIO from io import BytesIO
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.files import File from django.core.files import File
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
@@ -193,6 +194,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
c.expires = now() + timedelta(days=7) c.expires = now() + timedelta(days=7)
c.date = now() c.date = now()
c.filename = 'background_preview.pdf' c.filename = 'background_preview.pdf'
c.bind_to_session(request, "ticketoutput-pdf-background")
c.type = 'application/pdf' c.type = 'application/pdf'
c.save() c.save()
c.file.save('empty.pdf', ContentFile(buffer.read())) c.file.save('empty.pdf', ContentFile(buffer.read()))
@@ -218,6 +220,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
c.expires = now() + timedelta(days=7) c.expires = now() + timedelta(days=7)
c.date = now() c.date = now()
c.filename = 'background_preview.pdf' c.filename = 'background_preview.pdf'
c.bind_to_session(request, "ticketoutput-pdf-background")
c.type = 'application/pdf' c.type = 'application/pdf'
c.file = fileobj c.file = fileobj
c.save() c.save()
@@ -303,5 +306,7 @@ class FontsCSSView(TemplateView):
class PdfView(TemplateView): class PdfView(TemplateView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf") cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
if not cf.allowed_for_session(request, "ticketoutput-pdf-background"):
raise PermissionDenied()
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf') resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
return resp return resp
+6 -1
View File
@@ -431,7 +431,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
days = (self.copy_from.date_from.astimezone(tz).date() - value.astimezone(tz).date()).days days = (self.copy_from.date_from.astimezone(tz).date() - value.astimezone(tz).date()).days
return RelativeDateWrapper(RelativeDate( return RelativeDateWrapper(RelativeDate(
days=abs(days), days=abs(days),
base_date_name='date_from', base_date_name='event__date_from',
time=value.astimezone(tz).time(), time=value.astimezone(tz).time(),
minutes=None, minutes=None,
is_after=days < 0, is_after=days < 0,
@@ -1276,6 +1276,11 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
self._default_meta = self.request.event.meta_data self._default_meta = self.request.event.meta_data
for p in self.request.organizer.meta_properties.all(): for p in self.request.organizer.meta_properties.all():
if p.protected and not self.request.user.has_organizer_permission(
self.request.organizer, 'organizer.settings.general:write', request=self.request
):
continue
inst = SubEventMetaValue(property=p) inst = SubEventMetaValue(property=p)
if len(matches[p.id]) == 1 and matches[p.id][0]['c'] == total: if len(matches[p.id]) == 1 and matches[p.id][0]['c'] == total:
inst.value = matches[p.id][0]['value'] inst.value = matches[p.id][0]['value']
+14 -24
View File
@@ -59,6 +59,7 @@ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.generic import FormView, ListView, TemplateView, UpdateView from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django_otp import devices_for_user
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
@@ -85,7 +86,6 @@ from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
from pretix.helpers.security import session_reauth from pretix.helpers.security import session_reauth
from pretix.helpers.u2f import websafe_encode from pretix.helpers.u2f import websafe_encode
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -313,17 +313,7 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
except StaticDevice.DoesNotExist: except StaticDevice.DoesNotExist:
ctx['static_tokens_device'] = None ctx['static_tokens_device'] = None
ctx['devices'] = [] ctx['devices'] = [d for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)]
for dt in REAL_DEVICE_TYPES:
objs = list(dt.objects.filter(user=self.request.user, confirmed=True))
for obj in objs:
if dt == TOTPDevice:
obj.devicetype = 'totp'
elif dt == U2FDevice:
obj.devicetype = 'u2f'
elif dt == WebAuthnDevice:
obj.devicetype = 'webauthn'
ctx['devices'] += objs
ctx['obligatory'] = None ctx['obligatory'] = None
if settings.PRETIX_OBLIGATORY_2FA is True: if settings.PRETIX_OBLIGATORY_2FA is True:
@@ -342,9 +332,9 @@ class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
template_name = 'pretixcontrol/user/2fa_add.html' template_name = 'pretixcontrol/user/2fa_add.html'
def form_valid(self, form): def form_valid(self, form):
if form.cleaned_data['devicetype'] == 'totp': if form.cleaned_data['devicetype'] == 'otp_totp.totpdevice':
dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name']) dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
elif form.cleaned_data['devicetype'] == 'webauthn': elif form.cleaned_data['devicetype'] == 'pretixbase.webauthndevice':
if not self.request.is_secure(): if not self.request.is_secure():
messages.error(self.request, messages.error(self.request,
_('Security devices are only available if pretix is served via HTTPS.')) _('Security devices are only available if pretix is served via HTTPS.'))
@@ -364,11 +354,11 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
@cached_property @cached_property
def device(self): def device(self):
if self.kwargs['devicetype'] == 'totp': if self.kwargs['devicetype'] == 'otp_totp.totpdevice':
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True) return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
elif self.kwargs['devicetype'] == 'webauthn': elif self.kwargs['devicetype'] == 'pretixbase.webauthndevice':
return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True) return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
elif self.kwargs['devicetype'] == 'u2f': elif self.kwargs['devicetype'] == 'pretixbase.u2fdevice':
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True) return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -386,7 +376,7 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
msgs = [ msgs = [
_('A two-factor authentication device has been removed from your account.') _('A two-factor authentication device has been removed from your account.')
] ]
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES): if not any(d.confirmed for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)):
self.request.user.require_2fa = False self.request.user.require_2fa = False
self.request.user.save() self.request.user.save()
self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user) self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
@@ -461,7 +451,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
).first() ).first()
if credential_id_exists: if credential_id_exists:
messages.error(request, _('This security device is already registered.')) messages.error(request, _('This security device is already registered.'))
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={ return redirect(reverse('control:user.settings.2fa.confirm.pretixbase.webauthndevice', kwargs={
'device': self.device.pk 'device': self.device.pk
})) }))
@@ -475,7 +465,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
self.device.save() self.device.save()
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={ self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
'id': self.device.pk, 'id': self.device.pk,
'devicetype': 'u2f', 'devicetype': 'pretixbase.webauthndevice',
'name': self.device.name, 'name': self.device.name,
}) })
notices = [ notices = [
@@ -503,7 +493,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
except Exception: except Exception:
messages.error(request, _('The registration could not be completed. Please try again.')) messages.error(request, _('The registration could not be completed. Please try again.'))
logger.exception('WebAuthn registration failed') logger.exception('WebAuthn registration failed')
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={ return redirect(reverse('control:user.settings.2fa.confirm.pretixbase.webauthndevice', kwargs={
'device': self.device.pk 'device': self.device.pk
})) }))
@@ -537,7 +527,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={ self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
'id': self.device.pk, 'id': self.device.pk,
'name': self.device.name, 'name': self.device.name,
'devicetype': 'totp' 'devicetype': 'otp_totp.totpdevice'
}) })
notices = [ notices = [
_('A new two-factor authentication device has been added to your account.') _('A new two-factor authentication device has been added to your account.')
@@ -563,7 +553,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
else: else:
messages.error(request, _('The code you entered was not valid. If this problem persists, please check ' messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
'that the date and time of your phone are configured correctly.')) 'that the date and time of your phone are configured correctly.'))
return redirect(reverse('control:user.settings.2fa.confirm.totp', kwargs={ return redirect(reverse('control:user.settings.2fa.confirm.otp_totp.totpdevice', kwargs={
'device': self.device.pk 'device': self.device.pk
})) }))
@@ -594,7 +584,7 @@ class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_enable.html' template_name = 'pretixcontrol/user/2fa_enable.html'
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES): if not any(d.confirmed for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)):
messages.error(request, _('Please configure at least one device before enabling two-factor ' messages.error(request, _('Please configure at least one device before enabling two-factor '
'authentication.')) 'authentication.'))
return redirect(reverse('control:user.settings.2fa')) return redirect(reverse('control:user.settings.2fa'))
+23
View File
@@ -40,6 +40,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.generic import ListView, TemplateView from django.views.generic import ListView, TemplateView
from django_otp import devices_for_user
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from hijack import signals from hijack import signals
@@ -107,6 +108,9 @@ class UserEditView(AdministratorPermissionRequiredMixin, RecentAuthenticationReq
ctx['backend'] = ( ctx['backend'] = (
b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend
) )
ctx['devices'] = devices_for_user(self.object)
return ctx return ctx
def get_success_url(self): def get_success_url(self):
@@ -183,6 +187,25 @@ class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthent
return reverse('control:users.edit', kwargs=self.kwargs) return reverse('control:users.edit', kwargs=self.kwargs)
class Reset2FADriftThrottleView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, View):
def get(self, request, *args, **kwargs):
return redirect(reverse('control:users.edit', kwargs=self.kwargs))
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
self.object.totpdevice_set.update(drift=0, throttling_failure_timestamp=None, throttling_failure_count=0)
self.object.staticdevice_set.update(throttling_failure_timestamp=None, throttling_failure_count=0)
self.object.log_action('pretix.user.settings.2fa.resetdrift', user=self.request.user)
messages.success(request, _(
'The drift values for TOTP devices have been reset.'
))
return redirect(self.get_success_url())
def get_success_url(self):
return reverse('control:users.edit', kwargs=self.kwargs)
class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView): class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView):
template_name = "pretixcontrol/users/anonymize.html" template_name = "pretixcontrol/users/anonymize.html"
+4 -2
View File
@@ -144,7 +144,7 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
headers = [ headers = [
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'), _('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'), _('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'),
_('Comment') _('Comment'), _('Budget'), _('Budget used')
] ]
writer.writerow(headers) writer.writerow(headers)
@@ -170,7 +170,9 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
str(v.redeemed), str(v.redeemed),
str(v.max_usages), str(v.max_usages),
str(v.seat) if v.seat else "", str(v.seat) if v.seat else "",
str(v.comment) if v.comment else "" str(v.comment) if v.comment else "",
str(v.budget) if v.budget is not None else "",
str(v.budget_used) if v.budget is not None else "",
] ]
writer.writerow(row) writer.writerow(row)
+9
View File
@@ -288,6 +288,15 @@ def get_deterministic_ordering(model, ordering):
return ordering return ordering
@contextlib.contextmanager
def conditional_atomic(do_atomic, **kwargs):
if do_atomic:
with transaction.atomic(**kwargs):
yield
else:
yield
class IgnoreOnSQLiteMixin: class IgnoreOnSQLiteMixin:
# Mixin to allow defining PostgreSQL-specific indexes that will just not be created # Mixin to allow defining PostgreSQL-specific indexes that will just not be created
# on SQLite. SQLite is supported for testing only anyways! # on SQLite. SQLite is supported for testing only anyways!
+6 -2
View File
@@ -24,17 +24,21 @@ import hashlib
from django.core.signing import BadSignature, TimestampSigner from django.core.signing import BadSignature, TimestampSigner
class SafeDownloadSigner(TimestampSigner):
pass
def get_token(request, answer): def get_token(request, answer):
if not request.session.session_key: if not request.session.session_key:
request.session.create() request.session.create()
payload = '{}:{}'.format(request.session.session_key, answer.pk) payload = '{}:{}'.format(request.session.session_key, answer.pk)
signer = TimestampSigner() signer = SafeDownloadSigner()
return signer.sign(hashlib.sha1(payload.encode()).hexdigest()) return signer.sign(hashlib.sha1(payload.encode()).hexdigest())
def check_token(request, answer, token): def check_token(request, answer, token):
payload = hashlib.sha1('{}:{}'.format(request.session.session_key, answer.pk).encode()).hexdigest() payload = hashlib.sha1('{}:{}'.format(request.session.session_key, answer.pk).encode()).hexdigest()
signer = TimestampSigner() signer = SafeDownloadSigner()
try: try:
return payload == signer.unsign(token, max_age=3600 * 24) return payload == signer.unsign(token, max_age=3600 * 24)
except BadSignature: except BadSignature:
+1 -1
View File
@@ -29,7 +29,7 @@ from django.urls import reverse
def build_absolute_uri(urlname, args=None, kwargs=None): def build_absolute_uri(urlname, args=None, kwargs=None):
warnings.warn( warnings.warn(
'Usage of build_absolute_uri is confusing since there are many functions with that name. ' 'Usage of build_absolute_uri is confusing since there are many functions with that name. '
'Replace this usage with ', 'Replace this usage with mainreverse_absolute.',
DeprecationWarning DeprecationWarning
) )
return mainreverse_absolute(urlname, args, kwargs) return mainreverse_absolute(urlname, args, kwargs)
+210 -53
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-07-15 14:20+0000\n" "PO-Revision-Date: 2026-07-15 14:20+0000\n"
"Last-Translator: Llewelyn Barkhuizen <llewelyn@akademia.ac.za>\n" "Last-Translator: Llewelyn Barkhuizen <llewelyn@akademia.ac.za>\n"
"Language-Team: Afrikaans <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Afrikaans <https://translate.pretix.eu/projects/pretix/pretix/"
@@ -91,6 +91,10 @@ msgstr ""
msgid "Hebrew" msgid "Hebrew"
msgstr "" msgstr ""
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "" msgstr ""
@@ -410,6 +414,10 @@ msgstr ""
msgid "You cannot exchange a medium for a medium." msgid "You cannot exchange a medium for a medium."
msgstr "" msgstr ""
#: pretix/api/views/checkin.py
msgid "You cannot simulate a medium exchange."
msgstr ""
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@@ -838,8 +846,7 @@ msgstr ""
#: pretix/control/forms/organizer.py #: pretix/control/forms/organizer.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py #: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py #: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html #: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
@@ -856,7 +863,6 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/models/customers.py #: pretix/base/forms/questions.py pretix/base/models/customers.py
#: pretix/base/models/orders.py pretix/base/pdf.py #: pretix/base/models/orders.py pretix/base/pdf.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email" msgid "Attendee email"
msgstr "" msgstr ""
@@ -1587,9 +1593,8 @@ msgid "Invoice sender:"
msgstr "" msgstr ""
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/modelimport_orders.py pretix/base/models/customers.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/orders.py pretix/control/forms/filter.py
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -1601,8 +1606,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "ZIP code" msgid "ZIP code"
msgstr "" msgstr ""
@@ -1610,8 +1614,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "City" msgid "City"
msgstr "" msgstr ""
@@ -1634,8 +1637,7 @@ msgstr ""
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/email/login_notice.txt #: pretix/control/templates/pretixcontrol/email/login_notice.txt
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Country" msgid "Country"
@@ -1663,8 +1665,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Company" msgid "Company"
@@ -2451,7 +2452,8 @@ msgstr ""
msgid "Refund states" msgid "Refund states"
msgstr "" msgstr ""
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py #: pretix/base/exporters/orderlist.py pretix/base/models/items.py
#: pretix/base/models/orders.py
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html #: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html #: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
@@ -2956,13 +2958,11 @@ msgid ""
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid "Street"
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "Street and Number" msgid "State"
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
@@ -2995,6 +2995,16 @@ msgstr ""
msgid "Please enter a date and time no later than {max}." msgid "Please enter a date and time no later than {max}."
msgstr "" msgstr ""
#: pretix/base/forms/questions.py
msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr ""
#: pretix/base/forms/questions.py
msgid "Street and Number"
msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
@@ -5321,6 +5331,10 @@ msgstr ""
msgid "The count needs to be equal to or greater than zero." msgid "The count needs to be equal to or greater than zero."
msgstr "" msgstr ""
#: pretix/base/models/items.py pretix/base/models/orders.py
msgid "Order position"
msgstr ""
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Number" msgid "Number"
msgstr "" msgstr ""
@@ -5358,6 +5372,10 @@ msgstr ""
msgid "Country code (ISO 3166-1 alpha-2)" msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "" msgstr ""
#: pretix/base/models/items.py
msgid "Asked on"
msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
"The identifier may only contain letters, numbers, dots, dashes, and " "The identifier may only contain letters, numbers, dots, dashes, and "
@@ -5957,10 +5975,6 @@ msgstr ""
msgid "Value" msgid "Value"
msgstr "" msgstr ""
#: pretix/base/models/orders.py
msgid "Order position"
msgstr ""
#: pretix/base/models/orders.py #: pretix/base/models/orders.py
msgid "Cart ID (e.g. session key)" msgid "Cart ID (e.g. session key)"
msgstr "" msgstr ""
@@ -6869,7 +6883,7 @@ msgstr ""
#: pretix/base/payment.py #: pretix/base/payment.py
msgid "" msgid ""
"This payment is already being processed and can not be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "" msgstr ""
#: pretix/base/payment.py #: pretix/base/payment.py
@@ -7596,6 +7610,14 @@ msgstr ""
msgid "Presale end" msgid "Presale end"
msgstr "" msgstr ""
#: pretix/base/reldate.py
msgid "Order creation"
msgstr ""
#: pretix/base/reldate.py
msgid "Order expiry"
msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
msgstr "" msgstr ""
@@ -7620,6 +7642,26 @@ msgstr ""
msgid "Not set" msgid "Not set"
msgstr "" msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
msgstr "" msgstr ""
@@ -8705,7 +8747,7 @@ msgstr ""
msgid "Please transfer money to this bank account: 9999-9999-9999-9999" msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "" msgstr ""
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py #: pretix/base/services/placeholders.py
msgid "This value will be replaced based on dynamic parameters." msgid "This value will be replaced based on dynamic parameters."
msgstr "" msgstr ""
@@ -9286,6 +9328,25 @@ msgid ""
"to get a ticket, such as a box office." "to get a ticket, such as a box office."
msgstr "" msgstr ""
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid ""
"This text will be shown if the calendar or list of dates is empty, e.g. "
"because a month does not contain any dates or a filter chosen by the user "
"does not find any results. You can use this to advertise ways to get in "
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
msgstr "" msgstr ""
@@ -12427,6 +12488,22 @@ msgid ""
"card is issued in." "card is issued in."
msgstr "" msgstr ""
#: pretix/control/forms/event.py
msgid "Payment term"
msgstr ""
#: pretix/control/forms/event.py
msgid "same as above"
msgstr ""
#: pretix/control/forms/event.py
msgid "different payment term in days"
msgstr ""
#: pretix/control/forms/event.py
msgid "different payment term in minutes"
msgstr ""
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
msgstr "" msgstr ""
@@ -18577,6 +18654,7 @@ msgid "Top recommendation"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/event/plugins.html #: pretix/control/templates/pretixcontrol/event/plugins.html
#: pretix/control/templates/pretixcontrol/items/questions.html
#: pretix/control/templates/pretixcontrol/organizers/plugins.html #: pretix/control/templates/pretixcontrol/organizers/plugins.html
msgid "Experimental feature" msgid "Experimental feature"
msgstr "" msgstr ""
@@ -20228,6 +20306,24 @@ msgid ""
"dietary requirements." "dietary requirements."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new per-ticket question"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new order-level question"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-ticket questions"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
msgstr "" msgstr ""
@@ -20244,6 +20340,26 @@ msgstr ""
msgid "All personalized products" msgid "All personalized products"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-order questions"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "These questions are asked once per order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
#, python-format #, python-format
@@ -20766,6 +20882,13 @@ msgstr ""
msgid "(optional)" msgid "(optional)"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
msgid "Additional order information"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
msgstr "" msgstr ""
@@ -20793,6 +20916,23 @@ msgstr ""
msgid "Extend payment term" msgid "Extend payment term"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid "UNSAFE"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
@@ -21048,25 +21188,10 @@ msgstr ""
msgid "Ticket page" msgid "Ticket page"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
msgid "This question will be asked during check-in." msgid "This question will be asked during check-in."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid "UNSAFE"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/overview.html #: pretix/control/templates/pretixcontrol/orders/overview.html
#: pretix/plugins/reports/accountingreport.py #: pretix/plugins/reports/accountingreport.py
@@ -22304,6 +22429,15 @@ msgstr ""
msgid "Hardware model" msgid "Hardware model"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, python-format
msgid "Last seen: %(time)s"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
msgstr "" msgstr ""
@@ -25179,10 +25313,6 @@ msgstr ""
msgid "Not all objects have been selected." msgid "Not all objects have been selected."
msgstr "" msgstr ""
#: pretix/control/views/item.py
msgid "Street"
msgstr ""
#: pretix/control/views/item.py #: pretix/control/views/item.py
msgid "The requested question does not exist." msgid "The requested question does not exist."
msgstr "" msgstr ""
@@ -25328,7 +25458,7 @@ msgstr ""
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "" msgid ""
"We did not find DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
@@ -25718,7 +25848,8 @@ msgstr ""
msgid "Your message has been queued and will be sent to {}." msgid "Your message has been queued and will be sent to {}."
msgstr "" msgstr ""
#: pretix/control/views/orders.py pretix/presale/views/order.py #: pretix/control/views/orders.py pretix/presale/views/cart.py
#: pretix/presale/views/order.py
msgid "" msgid ""
"This link is no longer valid. Please go back, refresh the page, and try " "This link is no longer valid. Please go back, refresh the page, and try "
"again." "again."
@@ -27760,6 +27891,29 @@ msgid ""
"getting your money right away." "getting your money right away."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during "
"this time window. You can allow your customers to start another payment "
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
msgstr "" msgstr ""
@@ -27985,8 +28139,11 @@ msgstr ""
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
msgid "" msgid ""
"Our attempt to execute your payment via PayPal has failed. Please try again " "Your payment is being processed by PayPal. This takes longer than usual. You "
"or contact us." "can wait until PayPal acknowledges the payment or you can try paying again "
"with this or another payment method. This might result in you being charged "
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
@@ -30719,10 +30876,6 @@ msgstr[1] ""
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The price of this product was reduced because of an automatic discount." msgid "The price of this product was reduced because of an automatic discount."
msgstr "" msgstr ""
@@ -31120,6 +31273,10 @@ msgstr ""
msgid "Set price in %(currency)s for %(item)s" msgid "Set price in %(currency)s for %(item)s"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
+210 -53
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Automatically generated\n"
"Language-Team: none\n" "Language-Team: none\n"
@@ -89,6 +89,10 @@ msgstr ""
msgid "Hebrew" msgid "Hebrew"
msgstr "" msgstr ""
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "" msgstr ""
@@ -408,6 +412,10 @@ msgstr ""
msgid "You cannot exchange a medium for a medium." msgid "You cannot exchange a medium for a medium."
msgstr "" msgstr ""
#: pretix/api/views/checkin.py
msgid "You cannot simulate a medium exchange."
msgstr ""
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@@ -836,8 +844,7 @@ msgstr ""
#: pretix/control/forms/organizer.py #: pretix/control/forms/organizer.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py #: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py #: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html #: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
@@ -854,7 +861,6 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/models/customers.py #: pretix/base/forms/questions.py pretix/base/models/customers.py
#: pretix/base/models/orders.py pretix/base/pdf.py #: pretix/base/models/orders.py pretix/base/pdf.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email" msgid "Attendee email"
msgstr "" msgstr ""
@@ -1585,9 +1591,8 @@ msgid "Invoice sender:"
msgstr "" msgstr ""
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/modelimport_orders.py pretix/base/models/customers.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/orders.py pretix/control/forms/filter.py
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -1599,8 +1604,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "ZIP code" msgid "ZIP code"
msgstr "" msgstr ""
@@ -1608,8 +1612,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "City" msgid "City"
msgstr "" msgstr ""
@@ -1632,8 +1635,7 @@ msgstr ""
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/email/login_notice.txt #: pretix/control/templates/pretixcontrol/email/login_notice.txt
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Country" msgid "Country"
@@ -1661,8 +1663,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Company" msgid "Company"
@@ -2449,7 +2450,8 @@ msgstr ""
msgid "Refund states" msgid "Refund states"
msgstr "" msgstr ""
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py #: pretix/base/exporters/orderlist.py pretix/base/models/items.py
#: pretix/base/models/orders.py
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html #: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html #: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
@@ -2954,13 +2956,11 @@ msgid ""
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid "Street"
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "Street and Number" msgid "State"
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
@@ -2993,6 +2993,16 @@ msgstr ""
msgid "Please enter a date and time no later than {max}." msgid "Please enter a date and time no later than {max}."
msgstr "" msgstr ""
#: pretix/base/forms/questions.py
msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr ""
#: pretix/base/forms/questions.py
msgid "Street and Number"
msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
@@ -5319,6 +5329,10 @@ msgstr ""
msgid "The count needs to be equal to or greater than zero." msgid "The count needs to be equal to or greater than zero."
msgstr "" msgstr ""
#: pretix/base/models/items.py pretix/base/models/orders.py
msgid "Order position"
msgstr ""
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Number" msgid "Number"
msgstr "" msgstr ""
@@ -5356,6 +5370,10 @@ msgstr ""
msgid "Country code (ISO 3166-1 alpha-2)" msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "" msgstr ""
#: pretix/base/models/items.py
msgid "Asked on"
msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
"The identifier may only contain letters, numbers, dots, dashes, and " "The identifier may only contain letters, numbers, dots, dashes, and "
@@ -5955,10 +5973,6 @@ msgstr ""
msgid "Value" msgid "Value"
msgstr "" msgstr ""
#: pretix/base/models/orders.py
msgid "Order position"
msgstr ""
#: pretix/base/models/orders.py #: pretix/base/models/orders.py
msgid "Cart ID (e.g. session key)" msgid "Cart ID (e.g. session key)"
msgstr "" msgstr ""
@@ -6867,7 +6881,7 @@ msgstr ""
#: pretix/base/payment.py #: pretix/base/payment.py
msgid "" msgid ""
"This payment is already being processed and can not be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "" msgstr ""
#: pretix/base/payment.py #: pretix/base/payment.py
@@ -7594,6 +7608,14 @@ msgstr ""
msgid "Presale end" msgid "Presale end"
msgstr "" msgstr ""
#: pretix/base/reldate.py
msgid "Order creation"
msgstr ""
#: pretix/base/reldate.py
msgid "Order expiry"
msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
msgstr "" msgstr ""
@@ -7618,6 +7640,26 @@ msgstr ""
msgid "Not set" msgid "Not set"
msgstr "" msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
msgstr "" msgstr ""
@@ -8703,7 +8745,7 @@ msgstr ""
msgid "Please transfer money to this bank account: 9999-9999-9999-9999" msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "" msgstr ""
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py #: pretix/base/services/placeholders.py
msgid "This value will be replaced based on dynamic parameters." msgid "This value will be replaced based on dynamic parameters."
msgstr "" msgstr ""
@@ -9284,6 +9326,25 @@ msgid ""
"to get a ticket, such as a box office." "to get a ticket, such as a box office."
msgstr "" msgstr ""
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid ""
"This text will be shown if the calendar or list of dates is empty, e.g. "
"because a month does not contain any dates or a filter chosen by the user "
"does not find any results. You can use this to advertise ways to get in "
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
msgstr "" msgstr ""
@@ -12425,6 +12486,22 @@ msgid ""
"card is issued in." "card is issued in."
msgstr "" msgstr ""
#: pretix/control/forms/event.py
msgid "Payment term"
msgstr ""
#: pretix/control/forms/event.py
msgid "same as above"
msgstr ""
#: pretix/control/forms/event.py
msgid "different payment term in days"
msgstr ""
#: pretix/control/forms/event.py
msgid "different payment term in minutes"
msgstr ""
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
msgstr "" msgstr ""
@@ -18575,6 +18652,7 @@ msgid "Top recommendation"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/event/plugins.html #: pretix/control/templates/pretixcontrol/event/plugins.html
#: pretix/control/templates/pretixcontrol/items/questions.html
#: pretix/control/templates/pretixcontrol/organizers/plugins.html #: pretix/control/templates/pretixcontrol/organizers/plugins.html
msgid "Experimental feature" msgid "Experimental feature"
msgstr "" msgstr ""
@@ -20226,6 +20304,24 @@ msgid ""
"dietary requirements." "dietary requirements."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new per-ticket question"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new order-level question"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-ticket questions"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
msgstr "" msgstr ""
@@ -20242,6 +20338,26 @@ msgstr ""
msgid "All personalized products" msgid "All personalized products"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-order questions"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "These questions are asked once per order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
#, python-format #, python-format
@@ -20764,6 +20880,13 @@ msgstr ""
msgid "(optional)" msgid "(optional)"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
msgid "Additional order information"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
msgstr "" msgstr ""
@@ -20791,6 +20914,23 @@ msgstr ""
msgid "Extend payment term" msgid "Extend payment term"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid "UNSAFE"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
@@ -21046,25 +21186,10 @@ msgstr ""
msgid "Ticket page" msgid "Ticket page"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
msgid "This question will be asked during check-in." msgid "This question will be asked during check-in."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid "UNSAFE"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/overview.html #: pretix/control/templates/pretixcontrol/orders/overview.html
#: pretix/plugins/reports/accountingreport.py #: pretix/plugins/reports/accountingreport.py
@@ -22302,6 +22427,15 @@ msgstr ""
msgid "Hardware model" msgid "Hardware model"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, python-format
msgid "Last seen: %(time)s"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
msgstr "" msgstr ""
@@ -25177,10 +25311,6 @@ msgstr ""
msgid "Not all objects have been selected." msgid "Not all objects have been selected."
msgstr "" msgstr ""
#: pretix/control/views/item.py
msgid "Street"
msgstr ""
#: pretix/control/views/item.py #: pretix/control/views/item.py
msgid "The requested question does not exist." msgid "The requested question does not exist."
msgstr "" msgstr ""
@@ -25326,7 +25456,7 @@ msgstr ""
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "" msgid ""
"We did not find DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
@@ -25716,7 +25846,8 @@ msgstr ""
msgid "Your message has been queued and will be sent to {}." msgid "Your message has been queued and will be sent to {}."
msgstr "" msgstr ""
#: pretix/control/views/orders.py pretix/presale/views/order.py #: pretix/control/views/orders.py pretix/presale/views/cart.py
#: pretix/presale/views/order.py
msgid "" msgid ""
"This link is no longer valid. Please go back, refresh the page, and try " "This link is no longer valid. Please go back, refresh the page, and try "
"again." "again."
@@ -27758,6 +27889,29 @@ msgid ""
"getting your money right away." "getting your money right away."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during "
"this time window. You can allow your customers to start another payment "
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
msgstr "" msgstr ""
@@ -27983,8 +28137,11 @@ msgstr ""
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
msgid "" msgid ""
"Our attempt to execute your payment via PayPal has failed. Please try again " "Your payment is being processed by PayPal. This takes longer than usual. You "
"or contact us." "can wait until PayPal acknowledges the payment or you can try paying again "
"with this or another payment method. This might result in you being charged "
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
@@ -30717,10 +30874,6 @@ msgstr[1] ""
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The price of this product was reduced because of an automatic discount." msgid "The price of this product was reduced because of an automatic discount."
msgstr "" msgstr ""
@@ -31118,6 +31271,10 @@ msgstr ""
msgid "Set price in %(currency)s for %(item)s" msgid "Set price in %(currency)s for %(item)s"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
+251 -65
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-07-20 08:01+0000\n" "PO-Revision-Date: 2026-07-20 08:01+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n" "Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix/ar/" "Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix/ar/"
@@ -92,6 +92,10 @@ msgstr "اليونانية"
msgid "Hebrew" msgid "Hebrew"
msgstr "العبرية" msgstr "العبرية"
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "الأندونيسية" msgstr "الأندونيسية"
@@ -421,6 +425,12 @@ msgstr "الوسيط متصل بحدث آخر"
msgid "You cannot exchange a medium for a medium." msgid "You cannot exchange a medium for a medium."
msgstr "لا يمكنك استبدال هذا الطلب (مقاس متوسط بمقاس متوسط)." msgstr "لا يمكنك استبدال هذا الطلب (مقاس متوسط بمقاس متوسط)."
#: pretix/api/views/checkin.py
#, fuzzy
#| msgid "Product does not support medium exchange."
msgid "You cannot simulate a medium exchange."
msgstr "هذا المنتج لا يدعم استبدال الحجم المتوسط."
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@@ -866,8 +876,7 @@ msgstr "معلومات عن الحدث أو التاريخ"
#: pretix/control/forms/organizer.py #: pretix/control/forms/organizer.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py #: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py #: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html #: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
@@ -884,7 +893,6 @@ msgstr "المشارك"
#: pretix/base/forms/questions.py pretix/base/models/customers.py #: pretix/base/forms/questions.py pretix/base/models/customers.py
#: pretix/base/models/orders.py pretix/base/pdf.py #: pretix/base/models/orders.py pretix/base/pdf.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email" msgid "Attendee email"
msgstr "البريد الإلكتروني للمشارك" msgstr "البريد الإلكتروني للمشارك"
@@ -1635,9 +1643,8 @@ msgid "Invoice sender:"
msgstr "مرسل الفاتورة:" msgstr "مرسل الفاتورة:"
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/modelimport_orders.py pretix/base/models/customers.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/orders.py pretix/control/forms/filter.py
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -1649,8 +1656,7 @@ msgstr "العنوان"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "ZIP code" msgid "ZIP code"
msgstr "الرمز البريدي" msgstr "الرمز البريدي"
@@ -1658,8 +1664,7 @@ msgstr "الرمز البريدي"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "City" msgid "City"
msgstr "المدينة" msgstr "المدينة"
@@ -1682,8 +1687,7 @@ msgstr "الولاية"
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/email/login_notice.txt #: pretix/control/templates/pretixcontrol/email/login_notice.txt
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Country" msgid "Country"
@@ -1711,8 +1715,7 @@ msgstr "مستلم الفاتورة:"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Company" msgid "Company"
@@ -2521,7 +2524,8 @@ msgstr "‪حالات الدفع"
msgid "Refund states" msgid "Refund states"
msgstr "حالات استعادة المبلغ" msgstr "حالات استعادة المبلغ"
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py #: pretix/base/exporters/orderlist.py pretix/base/models/items.py
#: pretix/base/models/orders.py
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html #: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html #: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
@@ -3039,14 +3043,12 @@ msgstr ""
"قم بتحميل صورة صالحة. الملف الذي قمت بتحميله ليس صورة أو هو صورة تالفة." "قم بتحميل صورة صالحة. الملف الذي قمت بتحميله ليس صورة أو هو صورة تالفة."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid "Street"
"If you keep this empty, the ticket will be valid starting at the time of " msgstr "الشارع"
"purchase."
msgstr "إذا تركت هذا الحقل فارغًا، فستكون التذكرة صالحة اعتبارًا من وقت الشراء."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "Street and Number" msgid "State"
msgstr "الشارع والرقم" msgstr "ولاية"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
#, python-brace-format #, python-brace-format
@@ -3078,6 +3080,16 @@ msgstr "يرجى إدخال تاريخ ووقت لا يسبقان {min}."
msgid "Please enter a date and time no later than {max}." msgid "Please enter a date and time no later than {max}."
msgstr "يرجى إدخال تاريخ ووقت لا يتجاوزان {max}." msgstr "يرجى إدخال تاريخ ووقت لا يتجاوزان {max}."
#: pretix/base/forms/questions.py
msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr "إذا تركت هذا الحقل فارغًا، فستكون التذكرة صالحة اعتبارًا من وقت الشراء."
#: pretix/base/forms/questions.py
msgid "Street and Number"
msgstr "الشارع والرقم"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
@@ -5620,6 +5632,10 @@ msgstr "النوع الذي تم اختياره لا ينتمي إلى هذا ا
msgid "The count needs to be equal to or greater than zero." msgid "The count needs to be equal to or greater than zero."
msgstr "يجب أن يكون العدد مساويا للصفر أو أكبر منه." msgstr "يجب أن يكون العدد مساويا للصفر أو أكبر منه."
#: pretix/base/models/items.py pretix/base/models/orders.py
msgid "Order position"
msgstr "حالة الطلب"
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Number" msgid "Number"
msgstr "رقم" msgstr "رقم"
@@ -5657,6 +5673,10 @@ msgstr "التاريخ والوقت"
msgid "Country code (ISO 3166-1 alpha-2)" msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "رمز البلد (ISO 3166-1 alpha-2)" msgstr "رمز البلد (ISO 3166-1 alpha-2)"
#: pretix/base/models/items.py
msgid "Asked on"
msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
"The identifier may only contain letters, numbers, dots, dashes, and " "The identifier may only contain letters, numbers, dots, dashes, and "
@@ -6299,10 +6319,6 @@ msgstr "بطاقة هدايا"
msgid "Value" msgid "Value"
msgstr "القيمة" msgstr "القيمة"
#: pretix/base/models/orders.py
msgid "Order position"
msgstr "حالة الطلب"
#: pretix/base/models/orders.py #: pretix/base/models/orders.py
msgid "Cart ID (e.g. session key)" msgid "Cart ID (e.g. session key)"
msgstr "معرف عربة التسوق (مثل مفتاح الجلسة)" msgstr "معرف عربة التسوق (مثل مفتاح الجلسة)"
@@ -7297,8 +7313,11 @@ msgid "The payment for this invoice has already been received."
msgstr "تم استلام دفعة هذه الفاتورة سابقا." msgstr "تم استلام دفعة هذه الفاتورة سابقا."
#: pretix/base/payment.py #: pretix/base/payment.py
#, fuzzy
#| msgid ""
#| "This payment is already being processed and can not be canceled any more."
msgid "" msgid ""
"This payment is already being processed and can not be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "يجري حالياً معالجة عملية الدفع هذه، ولا يمكن إلغاؤها في الوقت الحالي." msgstr "يجري حالياً معالجة عملية الدفع هذه، ولا يمكن إلغاؤها في الوقت الحالي."
#: pretix/base/payment.py #: pretix/base/payment.py
@@ -8060,6 +8079,18 @@ msgstr "بداية إعلان ما قبل البيع"
msgid "Presale end" msgid "Presale end"
msgstr "نهاية إعلان ما قبل البيع" msgstr "نهاية إعلان ما قبل البيع"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order email"
msgid "Order creation"
msgstr "البريد الإلكتروني الخاص بالطلب"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order expired"
msgid "Order expiry"
msgstr "الطلب منتهي الصلاحية"
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
msgstr "قبل" msgstr "قبل"
@@ -8084,6 +8115,26 @@ msgstr "وقت نسبي:"
msgid "Not set" msgid "Not set"
msgstr "غير محدد" msgstr "غير محدد"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
msgstr "عشوائي (افتراضي، يعمل مع جميع تطبيقات pretix)" msgstr "عشوائي (افتراضي، يعمل مع جميع تطبيقات pretix)"
@@ -9446,7 +9497,7 @@ msgstr "تم خصم المبلغ من بطاقتك."
msgid "Please transfer money to this bank account: 9999-9999-9999-9999" msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "يرجى تحويل المبلغ إلى هذا الحساب المصرفي: 9999-9999-9999-9999" msgstr "يرجى تحويل المبلغ إلى هذا الحساب المصرفي: 9999-9999-9999-9999"
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py #: pretix/base/services/placeholders.py
msgid "This value will be replaced based on dynamic parameters." msgid "This value will be replaced based on dynamic parameters."
msgstr "سيتم استبدال هذه القيمة بناءً على معلمات ديناميكية." msgstr "سيتم استبدال هذه القيمة بناءً على معلمات ديناميكية."
@@ -10142,6 +10193,25 @@ msgstr ""
"لهذه الفعالية. يمكنك استخدامه لوصف الخيارات الأخرى للحصول على تذكرة ، على " "لهذه الفعالية. يمكنك استخدامه لوصف الخيارات الأخرى للحصول على تذكرة ، على "
"سبيل المثال شباك التذاكر." "سبيل المثال شباك التذاكر."
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid ""
"This text will be shown if the calendar or list of dates is empty, e.g. "
"because a month does not contain any dates or a filter chosen by the user "
"does not find any results. You can use this to advertise ways to get in "
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
msgstr "نص للتوجيه" msgstr "نص للتوجيه"
@@ -13966,6 +14036,30 @@ msgstr ""
"لقد قمت بتعيين صلاحية بطاقات الهدايا لتكون صالحة لمدة {} سنوات بالإضافة إلى " "لقد قمت بتعيين صلاحية بطاقات الهدايا لتكون صالحة لمدة {} سنوات بالإضافة إلى "
"السنة التي تم إصدار البطاقة فيها." "السنة التي تم إصدار البطاقة فيها."
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment date"
msgid "Payment term"
msgstr "يوم الدفع او الاستحقاق"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "(Same as above)"
msgid "same as above"
msgstr "(كما ورد أعلاه)"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in days"
msgid "different payment term in days"
msgstr "مدة السداد بالأيام"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in minutes"
msgid "different payment term in minutes"
msgstr "مدة السداد بالدقائق"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
msgstr "الأسعار شاملة الضريبة" msgstr "الأسعار شاملة الضريبة"
@@ -20716,6 +20810,7 @@ msgid "Top recommendation"
msgstr "التوصية الأولى" msgstr "التوصية الأولى"
#: pretix/control/templates/pretixcontrol/event/plugins.html #: pretix/control/templates/pretixcontrol/event/plugins.html
#: pretix/control/templates/pretixcontrol/items/questions.html
#: pretix/control/templates/pretixcontrol/organizers/plugins.html #: pretix/control/templates/pretixcontrol/organizers/plugins.html
msgid "Experimental feature" msgid "Experimental feature"
msgstr "ميزة تجريبية" msgstr "ميزة تجريبية"
@@ -22576,6 +22671,30 @@ msgstr ""
"أسئلة تسمح الحضور لملء بيانات إضافية عن تذاكرهم. إذا كنت توفير الغذاء، قد " "أسئلة تسمح الحضور لملء بيانات إضافية عن تذاكرهم. إذا كنت توفير الغذاء، قد "
"يكون مثال واحد أن يسأل المستخدمين حول المتطلبات الغذائية." "يكون مثال واحد أن يسأل المستخدمين حول المتطلبات الغذائية."
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new per-ticket question"
msgstr "إنشاء سؤال جديد"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new order-level question"
msgstr "إنشاء سؤال جديد"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Delete question"
msgid "Per-ticket questions"
msgstr "مسألة الحذف"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
msgstr "إنشاء سؤال جديد" msgstr "إنشاء سؤال جديد"
@@ -22592,6 +22711,30 @@ msgstr "طرح خلال التسجيل في مكتب الاستقبال"
msgid "All personalized products" msgid "All personalized products"
msgstr "جميع المنتجات المخصصة" msgstr "جميع المنتجات المخصصة"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Include questions"
msgid "Per-order questions"
msgstr "تضمين الأسئلة"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "The question has been reordered."
msgid "These questions are asked once per order."
msgstr "تمت إعادة ترتيب السؤال."
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
#, python-format #, python-format
@@ -23178,6 +23321,15 @@ msgstr "معلومات الفاتورة"
msgid "(optional)" msgid "(optional)"
msgstr "(اختياري)" msgstr "(اختياري)"
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
#, fuzzy
#| msgid "Additional information"
msgid "Additional order information"
msgstr "معلومة اضافية"
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
msgstr "أجل حذف" msgstr "أجل حذف"
@@ -23207,6 +23359,25 @@ msgstr "نعم، وتنكر ترتيب"
msgid "Extend payment term" msgid "Extend payment term"
msgstr "تمديد فترة السداد" msgstr "تمديد فترة السداد"
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
"وقد تم تحميل هذا الملف من قبل المستخدم، ويمكن أن تحتوي على فيروسات أو غيرها "
"من المحتويات الضارة."
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid "UNSAFE"
msgstr "UNSAFE"
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "not answered"
msgstr "بلا جواب"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
@@ -23479,27 +23650,10 @@ msgstr "%(datetime_range)s صالح"
msgid "Ticket page" msgid "Ticket page"
msgstr "صفحة التذاكر" msgstr "صفحة التذاكر"
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "not answered"
msgstr "بلا جواب"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
msgid "This question will be asked during check-in." msgid "This question will be asked during check-in."
msgstr "سوف يتم طرح هذا السؤال أثناء التسجيل في مكتب الاستقبال." msgstr "سوف يتم طرح هذا السؤال أثناء التسجيل في مكتب الاستقبال."
#: pretix/control/templates/pretixcontrol/order/index.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
"وقد تم تحميل هذا الملف من قبل المستخدم، ويمكن أن تحتوي على فيروسات أو غيرها "
"من المحتويات الضارة."
#: pretix/control/templates/pretixcontrol/order/index.html
msgid "UNSAFE"
msgstr "UNSAFE"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/overview.html #: pretix/control/templates/pretixcontrol/orders/overview.html
#: pretix/plugins/reports/accountingreport.py #: pretix/plugins/reports/accountingreport.py
@@ -24852,6 +25006,16 @@ msgstr "توصيل جهاز"
msgid "Hardware model" msgid "Hardware model"
msgstr "نموذج الأجهزة" msgstr "نموذج الأجهزة"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, fuzzy, python-format
#| msgid "Begin: %(time)s"
msgid "Last seen: %(time)s"
msgstr "البداية: %(time)s"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
msgstr "لم يتم تهيئة بعد" msgstr "لم يتم تهيئة بعد"
@@ -28045,10 +28209,6 @@ msgstr "تم تحديث ترتيب الفئات."
msgid "Not all objects have been selected." msgid "Not all objects have been selected."
msgstr "لم يتم تحديد جميع العناصر." msgstr "لم يتم تحديد جميع العناصر."
#: pretix/control/views/item.py
msgid "Street"
msgstr "الشارع"
#: pretix/control/views/item.py #: pretix/control/views/item.py
msgid "The requested question does not exist." msgid "The requested question does not exist."
msgstr "عدم وجود مسألة المطلوبة." msgstr "عدم وجود مسألة المطلوبة."
@@ -28265,7 +28425,7 @@ msgstr ""
#| "You should update the DNS settings of your domain to include this system " #| "You should update the DNS settings of your domain to include this system "
#| "in the SPF record." #| "in the SPF record."
msgid "" msgid ""
"We did not find DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
@@ -28706,7 +28866,8 @@ msgstr "الموضوع: {subject}"
msgid "Your message has been queued and will be sent to {}." msgid "Your message has been queued and will be sent to {}."
msgstr "وقد اصطف رسالتك وسوف يتم ارسالهم الى {}." msgstr "وقد اصطف رسالتك وسوف يتم ارسالهم الى {}."
#: pretix/control/views/orders.py pretix/presale/views/order.py #: pretix/control/views/orders.py pretix/presale/views/cart.py
#: pretix/presale/views/order.py
msgid "" msgid ""
"This link is no longer valid. Please go back, refresh the page, and try " "This link is no longer valid. Please go back, refresh the page, and try "
"again." "again."
@@ -30934,6 +31095,29 @@ msgstr ""
"امنح عملاءك إمكانية الشراء الآن (حتى حد معين) والدفع على أقساط متعددة أو في " "امنح عملاءك إمكانية الشراء الآن (حتى حد معين) والدفع على أقساط متعددة أو في "
"غضون 30 يومًا. وأنت، بصفتك التاجر، ستحصل على أموالك على الفور." "غضون 30 يومًا. وأنت، بصفتك التاجر، ستحصل على أموالك على الفور."
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during "
"this time window. You can allow your customers to start another payment "
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
msgstr "-- تلقائي --" msgstr "-- تلقائي --"
@@ -31184,11 +31368,12 @@ msgstr "يرجى اختيار طريقة الدفع."
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
msgid "" msgid ""
"Our attempt to execute your payment via PayPal has failed. Please try again " "Your payment is being processed by PayPal. This takes longer than usual. You "
"or contact us." "can wait until PayPal acknowledges the payment or you can try paying again "
"with this or another payment method. This might result in you being charged "
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr "" msgstr ""
"لقد فشلت محاولتنا لتنفيذ عملية الدفع الخاصة بك عبر PayPal. يرجى المحاولة مرة "
"أخرى أو الاتصال بنا."
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
msgid "" msgid ""
@@ -34214,10 +34399,6 @@ msgstr[5] "تم استخدام هذه التذكرة %(count)s مرة."
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "لم يتم تقديم اسم الحضور" msgstr "لم يتم تقديم اسم الحضور"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
msgstr "الصورة التي قمت بتحميلها سابقًا"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The price of this product was reduced because of an automatic discount." msgid "The price of this product was reduced because of an automatic discount."
msgstr "تم تخفيض سعر هذا المنتج بسبب خصم تلقائي." msgstr "تم تخفيض سعر هذا المنتج بسبب خصم تلقائي."
@@ -34651,6 +34832,10 @@ msgstr "%(value)sبدون ضرائب"
msgid "Set price in %(currency)s for %(item)s" msgid "Set price in %(currency)s for %(item)s"
msgstr "تحديد السعر بـ %(currency)s لـ %(item)s" msgstr "تحديد السعر بـ %(currency)s لـ %(item)s"
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "The image you previously uploaded"
msgstr "الصورة التي قمت بتحميلها سابقًا"
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
@@ -36381,6 +36566,13 @@ msgstr "الوصول إلى الكتابة"
msgid "Kosovo" msgid "Kosovo"
msgstr "كوسوفو" msgstr "كوسوفو"
#~ msgid ""
#~ "Our attempt to execute your payment via PayPal has failed. Please try "
#~ "again or contact us."
#~ msgstr ""
#~ "لقد فشلت محاولتنا لتنفيذ عملية الدفع الخاصة بك عبر PayPal. يرجى المحاولة "
#~ "مرة أخرى أو الاتصال بنا."
#~ msgid "All actions" #~ msgid "All actions"
#~ msgstr "جميع الإجراءات" #~ msgstr "جميع الإجراءات"
@@ -36504,9 +36696,6 @@ msgstr "كوسوفو"
#~ msgid "Please do not use special characters in names." #~ msgid "Please do not use special characters in names."
#~ msgstr "الرجاء إدخال اسم أقصر." #~ msgstr "الرجاء إدخال اسم أقصر."
#~ msgid "State"
#~ msgstr "ولاية"
#, fuzzy #, fuzzy
#~| msgid "Printing date and time" #~| msgid "Printing date and time"
#~ msgid "Program times: date and time" #~ msgid "Program times: date and time"
@@ -37097,9 +37286,6 @@ msgstr "كوسوفو"
#~ msgid "Badge design" #~ msgid "Badge design"
#~ msgstr "تصميم شارة" #~ msgstr "تصميم شارة"
#~ msgid "(Same as above)"
#~ msgstr "(كما ورد أعلاه)"
#~ msgid "Translation in development" #~ msgid "Translation in development"
#~ msgstr "الترجمة في التنمية" #~ msgstr "الترجمة في التنمية"
+210 -53
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2024-12-11 01:00+0000\n" "PO-Revision-Date: 2024-12-11 01:00+0000\n"
"Last-Translator: Neriman Memmedli <nerim4n@pm.me>\n" "Last-Translator: Neriman Memmedli <nerim4n@pm.me>\n"
"Language-Team: Azerbaijani <https://translate.pretix.eu/projects/pretix/" "Language-Team: Azerbaijani <https://translate.pretix.eu/projects/pretix/"
@@ -91,6 +91,10 @@ msgstr "Yunanca"
msgid "Hebrew" msgid "Hebrew"
msgstr "" msgstr ""
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "İndoneziya dili" msgstr "İndoneziya dili"
@@ -410,6 +414,10 @@ msgstr ""
msgid "You cannot exchange a medium for a medium." msgid "You cannot exchange a medium for a medium."
msgstr "" msgstr ""
#: pretix/api/views/checkin.py
msgid "You cannot simulate a medium exchange."
msgstr ""
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@@ -838,8 +846,7 @@ msgstr ""
#: pretix/control/forms/organizer.py #: pretix/control/forms/organizer.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py #: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py #: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html #: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
@@ -856,7 +863,6 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/models/customers.py #: pretix/base/forms/questions.py pretix/base/models/customers.py
#: pretix/base/models/orders.py pretix/base/pdf.py #: pretix/base/models/orders.py pretix/base/pdf.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email" msgid "Attendee email"
msgstr "" msgstr ""
@@ -1587,9 +1593,8 @@ msgid "Invoice sender:"
msgstr "" msgstr ""
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/modelimport_orders.py pretix/base/models/customers.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/orders.py pretix/control/forms/filter.py
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -1601,8 +1606,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "ZIP code" msgid "ZIP code"
msgstr "" msgstr ""
@@ -1610,8 +1614,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "City" msgid "City"
msgstr "" msgstr ""
@@ -1634,8 +1637,7 @@ msgstr ""
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/email/login_notice.txt #: pretix/control/templates/pretixcontrol/email/login_notice.txt
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Country" msgid "Country"
@@ -1663,8 +1665,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Company" msgid "Company"
@@ -2451,7 +2452,8 @@ msgstr ""
msgid "Refund states" msgid "Refund states"
msgstr "" msgstr ""
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py #: pretix/base/exporters/orderlist.py pretix/base/models/items.py
#: pretix/base/models/orders.py
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html #: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html #: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
@@ -2956,13 +2958,11 @@ msgid ""
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid "Street"
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "Street and Number" msgid "State"
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
@@ -2995,6 +2995,16 @@ msgstr ""
msgid "Please enter a date and time no later than {max}." msgid "Please enter a date and time no later than {max}."
msgstr "" msgstr ""
#: pretix/base/forms/questions.py
msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr ""
#: pretix/base/forms/questions.py
msgid "Street and Number"
msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
@@ -5321,6 +5331,10 @@ msgstr ""
msgid "The count needs to be equal to or greater than zero." msgid "The count needs to be equal to or greater than zero."
msgstr "" msgstr ""
#: pretix/base/models/items.py pretix/base/models/orders.py
msgid "Order position"
msgstr ""
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Number" msgid "Number"
msgstr "" msgstr ""
@@ -5358,6 +5372,10 @@ msgstr ""
msgid "Country code (ISO 3166-1 alpha-2)" msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "" msgstr ""
#: pretix/base/models/items.py
msgid "Asked on"
msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
"The identifier may only contain letters, numbers, dots, dashes, and " "The identifier may only contain letters, numbers, dots, dashes, and "
@@ -5957,10 +5975,6 @@ msgstr ""
msgid "Value" msgid "Value"
msgstr "" msgstr ""
#: pretix/base/models/orders.py
msgid "Order position"
msgstr ""
#: pretix/base/models/orders.py #: pretix/base/models/orders.py
msgid "Cart ID (e.g. session key)" msgid "Cart ID (e.g. session key)"
msgstr "" msgstr ""
@@ -6869,7 +6883,7 @@ msgstr ""
#: pretix/base/payment.py #: pretix/base/payment.py
msgid "" msgid ""
"This payment is already being processed and can not be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "" msgstr ""
#: pretix/base/payment.py #: pretix/base/payment.py
@@ -7596,6 +7610,14 @@ msgstr ""
msgid "Presale end" msgid "Presale end"
msgstr "" msgstr ""
#: pretix/base/reldate.py
msgid "Order creation"
msgstr ""
#: pretix/base/reldate.py
msgid "Order expiry"
msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
msgstr "" msgstr ""
@@ -7620,6 +7642,26 @@ msgstr ""
msgid "Not set" msgid "Not set"
msgstr "" msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
msgstr "" msgstr ""
@@ -8705,7 +8747,7 @@ msgstr ""
msgid "Please transfer money to this bank account: 9999-9999-9999-9999" msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "" msgstr ""
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py #: pretix/base/services/placeholders.py
msgid "This value will be replaced based on dynamic parameters." msgid "This value will be replaced based on dynamic parameters."
msgstr "" msgstr ""
@@ -9286,6 +9328,25 @@ msgid ""
"to get a ticket, such as a box office." "to get a ticket, such as a box office."
msgstr "" msgstr ""
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid ""
"This text will be shown if the calendar or list of dates is empty, e.g. "
"because a month does not contain any dates or a filter chosen by the user "
"does not find any results. You can use this to advertise ways to get in "
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
msgstr "" msgstr ""
@@ -12427,6 +12488,22 @@ msgid ""
"card is issued in." "card is issued in."
msgstr "" msgstr ""
#: pretix/control/forms/event.py
msgid "Payment term"
msgstr ""
#: pretix/control/forms/event.py
msgid "same as above"
msgstr ""
#: pretix/control/forms/event.py
msgid "different payment term in days"
msgstr ""
#: pretix/control/forms/event.py
msgid "different payment term in minutes"
msgstr ""
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
msgstr "" msgstr ""
@@ -18577,6 +18654,7 @@ msgid "Top recommendation"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/event/plugins.html #: pretix/control/templates/pretixcontrol/event/plugins.html
#: pretix/control/templates/pretixcontrol/items/questions.html
#: pretix/control/templates/pretixcontrol/organizers/plugins.html #: pretix/control/templates/pretixcontrol/organizers/plugins.html
msgid "Experimental feature" msgid "Experimental feature"
msgstr "" msgstr ""
@@ -20228,6 +20306,24 @@ msgid ""
"dietary requirements." "dietary requirements."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new per-ticket question"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new order-level question"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-ticket questions"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
msgstr "" msgstr ""
@@ -20244,6 +20340,26 @@ msgstr ""
msgid "All personalized products" msgid "All personalized products"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-order questions"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "These questions are asked once per order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
#, python-format #, python-format
@@ -20766,6 +20882,13 @@ msgstr ""
msgid "(optional)" msgid "(optional)"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
msgid "Additional order information"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
msgstr "" msgstr ""
@@ -20793,6 +20916,23 @@ msgstr ""
msgid "Extend payment term" msgid "Extend payment term"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid "UNSAFE"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
@@ -21048,25 +21188,10 @@ msgstr ""
msgid "Ticket page" msgid "Ticket page"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
msgid "This question will be asked during check-in." msgid "This question will be asked during check-in."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid "UNSAFE"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/overview.html #: pretix/control/templates/pretixcontrol/orders/overview.html
#: pretix/plugins/reports/accountingreport.py #: pretix/plugins/reports/accountingreport.py
@@ -22304,6 +22429,15 @@ msgstr ""
msgid "Hardware model" msgid "Hardware model"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, python-format
msgid "Last seen: %(time)s"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
msgstr "" msgstr ""
@@ -25179,10 +25313,6 @@ msgstr ""
msgid "Not all objects have been selected." msgid "Not all objects have been selected."
msgstr "" msgstr ""
#: pretix/control/views/item.py
msgid "Street"
msgstr ""
#: pretix/control/views/item.py #: pretix/control/views/item.py
msgid "The requested question does not exist." msgid "The requested question does not exist."
msgstr "" msgstr ""
@@ -25328,7 +25458,7 @@ msgstr ""
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "" msgid ""
"We did not find DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
@@ -25718,7 +25848,8 @@ msgstr ""
msgid "Your message has been queued and will be sent to {}." msgid "Your message has been queued and will be sent to {}."
msgstr "" msgstr ""
#: pretix/control/views/orders.py pretix/presale/views/order.py #: pretix/control/views/orders.py pretix/presale/views/cart.py
#: pretix/presale/views/order.py
msgid "" msgid ""
"This link is no longer valid. Please go back, refresh the page, and try " "This link is no longer valid. Please go back, refresh the page, and try "
"again." "again."
@@ -27762,6 +27893,29 @@ msgid ""
"getting your money right away." "getting your money right away."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during "
"this time window. You can allow your customers to start another payment "
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
msgstr "" msgstr ""
@@ -27987,8 +28141,11 @@ msgstr ""
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
msgid "" msgid ""
"Our attempt to execute your payment via PayPal has failed. Please try again " "Your payment is being processed by PayPal. This takes longer than usual. You "
"or contact us." "can wait until PayPal acknowledges the payment or you can try paying again "
"with this or another payment method. This might result in you being charged "
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
@@ -30721,10 +30878,6 @@ msgstr[1] ""
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The price of this product was reduced because of an automatic discount." msgid "The price of this product was reduced because of an automatic discount."
msgstr "" msgstr ""
@@ -31122,6 +31275,10 @@ msgstr ""
msgid "Set price in %(currency)s for %(item)s" msgid "Set price in %(currency)s for %(item)s"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
+270 -85
View File
@@ -7,9 +7,9 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-06-25 14:00+0000\n" "PO-Revision-Date: 2026-09-20 14:00+0000\n"
"Last-Translator: Kim Lozano <joaquim.lozano@upc.edu>\n" "Last-Translator: Rita Gimenez <barcelonamusictech@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix/"
"ca/>\n" "ca/>\n"
"Language: ca\n" "Language: ca\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.6.1\n" "X-Generator: Weblate 2026.9.1\n"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "English" msgid "English"
@@ -37,11 +37,11 @@ msgstr "Àrab"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Basque" msgid "Basque"
msgstr "" msgstr "Basc"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr "Català"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Chinese (simplified)" msgid "Chinese (simplified)"
@@ -49,11 +49,11 @@ msgstr "Xinès (simplificat)"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Chinese (traditional)" msgid "Chinese (traditional)"
msgstr "" msgstr "Xinès (tradicional)"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Czech" msgid "Czech"
msgstr "" msgstr "Txec"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Croatian" msgid "Croatian"
@@ -81,7 +81,7 @@ msgstr "Finlandès"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Galician" msgid "Galician"
msgstr "" msgstr "Gallec"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Greek" msgid "Greek"
@@ -89,11 +89,15 @@ msgstr "Grec"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Hebrew" msgid "Hebrew"
msgstr "" msgstr "Hebreu"
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr "Hongarès"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "" msgstr "Indonesi"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Italian" msgid "Italian"
@@ -101,7 +105,7 @@ msgstr "Italià"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Japanese" msgid "Japanese"
msgstr "" msgstr "Japonès"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Latvian" msgid "Latvian"
@@ -109,7 +113,7 @@ msgstr "Letó"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Norwegian Bokmål" msgid "Norwegian Bokmål"
msgstr "" msgstr "Noruec"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Polish" msgid "Polish"
@@ -125,7 +129,7 @@ msgstr "Portuguès (Brasil)"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Romanian" msgid "Romanian"
msgstr "" msgstr "Romanès"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Russian" msgid "Russian"
@@ -133,11 +137,11 @@ msgstr "Rus"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Slovak" msgid "Slovak"
msgstr "" msgstr "Eslovac"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr "Suec"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Spanish" msgid "Spanish"
@@ -145,11 +149,11 @@ msgstr "Espanyol"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Spanish (Latin America)" msgid "Spanish (Latin America)"
msgstr "" msgstr "Espanyol (Llatinoamèrica)"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Thai" msgid "Thai"
msgstr "" msgstr "Tailandès"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Turkish" msgid "Turkish"
@@ -157,13 +161,15 @@ msgstr "Turc"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Ukrainian" msgid "Ukrainian"
msgstr "" msgstr "Ucraïnès"
#: pretix/api/auth/devicesecurity.py #: pretix/api/auth/devicesecurity.py
msgid "" msgid ""
"Full device access (reading and changing orders and gift cards, reading of " "Full device access (reading and changing orders and gift cards, reading of "
"products and settings)" "products and settings)"
msgstr "" msgstr ""
"Accés total al dispositiu (lectura i modificació de comandes i targetes "
"regal, lectura de productes i configuració)"
#: pretix/api/auth/devicesecurity.py #: pretix/api/auth/devicesecurity.py
msgid "pretixSCAN" msgid "pretixSCAN"
@@ -305,7 +311,7 @@ msgstr "La cistella és buida."
#: pretix/api/serializers/item.py #: pretix/api/serializers/item.py
msgid "The program end must not be empty." msgid "The program end must not be empty."
msgstr "" msgstr "El final del programa no ha de ser buit."
#: pretix/api/serializers/item.py pretix/base/models/items.py #: pretix/api/serializers/item.py pretix/base/models/items.py
#, fuzzy #, fuzzy
@@ -335,7 +341,7 @@ msgstr ""
#: pretix/api/serializers/item.py #: pretix/api/serializers/item.py
msgid "Only admission products can currently be personalized." msgid "Only admission products can currently be personalized."
msgstr "" msgstr "Actualment, només es poden personalitzar els productes d'accés."
#: pretix/api/serializers/item.py #: pretix/api/serializers/item.py
msgid "" msgid ""
@@ -430,7 +436,7 @@ msgstr ""
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py #: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
#, python-format #, python-format
msgid "You've been invited to join %(organizer)s" msgid "You've been invited to join %(organizer)s"
msgstr "" msgstr "Has estat convidat/da a unir-te a %(organizer)s"
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py #: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
msgid "This user already has been invited for this team." msgid "This user already has been invited for this team."
@@ -452,7 +458,7 @@ msgstr ""
#: pretix/api/views/checkin.py #: pretix/api/views/checkin.py
msgid "Medium connected to other event" msgid "Medium connected to other event"
msgstr "" msgstr "Mitjà connectat a un altre esdeveniment"
#: pretix/api/views/checkin.py #: pretix/api/views/checkin.py
#, fuzzy #, fuzzy
@@ -460,6 +466,12 @@ msgstr ""
msgid "You cannot exchange a medium for a medium." msgid "You cannot exchange a medium for a medium."
msgstr "No podeu modificar aquesta comanda." msgstr "No podeu modificar aquesta comanda."
#: pretix/api/views/checkin.py
#, fuzzy
#| msgid "You cannot change this order."
msgid "You cannot simulate a medium exchange."
msgstr "No podeu modificar aquesta comanda."
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@@ -632,6 +644,8 @@ msgid ""
"This includes product added or deleted and changes to nested objects like " "This includes product added or deleted and changes to nested objects like "
"variations or bundles." "variations or bundles."
msgstr "" msgstr ""
"Això inclou productes afegits o eliminats i canvis en objectes imbricats, "
"com ara variacions o lots."
#: pretix/api/webhooks.py #: pretix/api/webhooks.py
#, fuzzy #, fuzzy
@@ -644,6 +658,9 @@ msgid ""
"This includes related events like creation, deletion, opening or closing of " "This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability." "quotas. No webhook is sent for changes to the resulting availability."
msgstr "" msgstr ""
"Això inclou esdeveniments relacionats, com ara la creació, l'eliminació, "
"l'obertura o el tancament de quotes. No s'envia cap webhook per als canvis "
"en la disponibilitat resultant."
#: pretix/api/webhooks.py #: pretix/api/webhooks.py
#, fuzzy #, fuzzy
@@ -959,8 +976,7 @@ msgstr "Enviar registre"
#: pretix/control/forms/organizer.py #: pretix/control/forms/organizer.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py #: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py #: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html #: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
@@ -979,7 +995,6 @@ msgstr "Nom de l'assistent"
#: pretix/base/forms/questions.py pretix/base/models/customers.py #: pretix/base/forms/questions.py pretix/base/models/customers.py
#: pretix/base/models/orders.py pretix/base/pdf.py #: pretix/base/models/orders.py pretix/base/pdf.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email" msgid "Attendee email"
msgstr "Correu electrònic de l'assistent" msgstr "Correu electrònic de l'assistent"
@@ -1799,9 +1814,8 @@ msgid "Invoice sender:"
msgstr "Emissor de la factura:" msgstr "Emissor de la factura:"
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/modelimport_orders.py pretix/base/models/customers.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/orders.py pretix/control/forms/filter.py
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -1813,8 +1827,7 @@ msgstr "Adreça"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "ZIP code" msgid "ZIP code"
msgstr "Codi postal" msgstr "Codi postal"
@@ -1822,8 +1835,7 @@ msgstr "Codi postal"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "City" msgid "City"
msgstr "Ciutat" msgstr "Ciutat"
@@ -1846,8 +1858,7 @@ msgstr "Estat"
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/email/login_notice.txt #: pretix/control/templates/pretixcontrol/email/login_notice.txt
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Country" msgid "Country"
@@ -1875,8 +1886,7 @@ msgstr "Receptor de la factura:"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Company" msgid "Company"
@@ -2735,7 +2745,8 @@ msgstr "Estats del pagament"
msgid "Refund states" msgid "Refund states"
msgstr "Estats del reemborsament" msgstr "Estats del reemborsament"
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py #: pretix/base/exporters/orderlist.py pretix/base/models/items.py
#: pretix/base/models/orders.py
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html #: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html #: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
@@ -3283,14 +3294,14 @@ msgstr ""
"està corromput." "està corromput."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid "Street"
"If you keep this empty, the ticket will be valid starting at the time of " msgstr "Carrer"
"purchase."
msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "Street and Number" #, fuzzy
msgstr "Carrer i número" #| msgid "Status"
msgid "State"
msgstr "Estat"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
@@ -3327,6 +3338,16 @@ msgstr "Si us plau, introduïu un canal de venda vàlid."
msgid "Please enter a date and time no later than {max}." msgid "Please enter a date and time no later than {max}."
msgstr "Si us plau, introduïu un nom més curt." msgstr "Si us plau, introduïu un nom més curt."
#: pretix/base/forms/questions.py
msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr ""
#: pretix/base/forms/questions.py
msgid "Street and Number"
msgstr "Carrer i número"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
@@ -6046,6 +6067,10 @@ msgstr "La variació triada no pertany a aquest element."
msgid "The count needs to be equal to or greater than zero." msgid "The count needs to be equal to or greater than zero."
msgstr "El recompte ha de ser igual o major que zero." msgstr "El recompte ha de ser igual o major que zero."
#: pretix/base/models/items.py pretix/base/models/orders.py
msgid "Order position"
msgstr "Posició de la comanda"
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Number" msgid "Number"
msgstr "Número" msgstr "Número"
@@ -6083,6 +6108,10 @@ msgstr "Dia i hora"
msgid "Country code (ISO 3166-1 alpha-2)" msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "Codi de país (ISO 3166-1 alpha-2)" msgstr "Codi de país (ISO 3166-1 alpha-2)"
#: pretix/base/models/items.py
msgid "Asked on"
msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
#, fuzzy #, fuzzy
#| msgid "The property name may only contain letters, numbers and underscores." #| msgid "The property name may only contain letters, numbers and underscores."
@@ -6748,10 +6777,6 @@ msgstr "Targeta regal"
msgid "Value" msgid "Value"
msgstr "Valor" msgstr "Valor"
#: pretix/base/models/orders.py
msgid "Order position"
msgstr "Posició de la comanda"
#: pretix/base/models/orders.py #: pretix/base/models/orders.py
msgid "Cart ID (e.g. session key)" msgid "Cart ID (e.g. session key)"
msgstr "Identificador del carretó (ex. clau de sessió)" msgstr "Identificador del carretó (ex. clau de sessió)"
@@ -7771,7 +7796,7 @@ msgstr "Ja s'ha rebut el pagament per a aquesta factura."
#: pretix/base/payment.py #: pretix/base/payment.py
msgid "" msgid ""
"This payment is already being processed and can not be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "" msgstr ""
#: pretix/base/payment.py #: pretix/base/payment.py
@@ -8633,6 +8658,18 @@ msgstr "Inici de la prevenda"
msgid "Presale end" msgid "Presale end"
msgstr "Finalització de la prevenda" msgstr "Finalització de la prevenda"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order details"
msgid "Order creation"
msgstr "Detalls de la comanda"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order expired"
msgid "Order expiry"
msgstr "Ha expirat la comanda"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, fuzzy #, fuzzy
#| msgid "days before" #| msgid "days before"
@@ -8659,6 +8696,26 @@ msgstr "Temps relatiu:"
msgid "Not set" msgid "Not set"
msgstr "No s'ha establert" msgstr "No s'ha establert"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
msgstr "Aleatori (per defecte, funciona amb totes les apps de pretix)" msgstr "Aleatori (per defecte, funciona amb totes les apps de pretix)"
@@ -10003,7 +10060,7 @@ msgstr ""
"Si us plau, feu una transferència a aquest compte bancari: " "Si us plau, feu una transferència a aquest compte bancari: "
"9999-9999-9999-9999" "9999-9999-9999-9999"
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py #: pretix/base/services/placeholders.py
msgid "This value will be replaced based on dynamic parameters." msgid "This value will be replaced based on dynamic parameters."
msgstr "" msgstr ""
@@ -10743,6 +10800,25 @@ msgstr ""
"la finestra de temps de venda. Podeu utilitzar-ho per descriure altres " "la finestra de temps de venda. Podeu utilitzar-ho per descriure altres "
"opcions per obtenir un tiquet, com per exemple una oficina." "opcions per obtenir un tiquet, com per exemple una oficina."
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid ""
"This text will be shown if the calendar or list of dates is empty, e.g. "
"because a month does not contain any dates or a filter chosen by the user "
"does not find any results. You can use this to advertise ways to get in "
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
msgstr "Text de guia" msgstr "Text de guia"
@@ -14757,6 +14833,30 @@ msgid ""
"card is issued in." "card is issued in."
msgstr "" msgstr ""
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment date"
msgid "Payment term"
msgstr "Data de pagament"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "(Same as above)"
msgid "same as above"
msgstr "(Igual que aquí al damunt)"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in days"
msgid "different payment term in days"
msgstr "Termini de pagament en dies"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in minutes"
msgid "different payment term in minutes"
msgstr "Termini de pagament en minuts"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy #, fuzzy
#| msgid "including all taxes" #| msgid "including all taxes"
@@ -21746,6 +21846,7 @@ msgid "Top recommendation"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/event/plugins.html #: pretix/control/templates/pretixcontrol/event/plugins.html
#: pretix/control/templates/pretixcontrol/items/questions.html
#: pretix/control/templates/pretixcontrol/organizers/plugins.html #: pretix/control/templates/pretixcontrol/organizers/plugins.html
#, fuzzy #, fuzzy
#| msgid "Use feature" #| msgid "Use feature"
@@ -23594,6 +23695,30 @@ msgid ""
"dietary requirements." "dietary requirements."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new per-ticket question"
msgstr "Crear una nova pregunta"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new order-level question"
msgstr "Crear una nova pregunta"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Delete question"
msgid "Per-ticket questions"
msgstr "Esborrar pregunta"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
msgstr "Crear una nova pregunta" msgstr "Crear una nova pregunta"
@@ -23612,6 +23737,30 @@ msgstr ""
msgid "All personalized products" msgid "All personalized products"
msgstr "Tots els productes d'admissió" msgstr "Tots els productes d'admissió"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Required question"
msgid "Per-order questions"
msgstr "Pregunta obligatòria"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "The task has been completed."
msgid "These questions are asked once per order."
msgstr "S'ha completat la tasca."
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
#, python-format #, python-format
@@ -24158,6 +24307,15 @@ msgstr "Dades de la factura"
msgid "(optional)" msgid "(optional)"
msgstr "(opcional)" msgstr "(opcional)"
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
#, fuzzy
#| msgid "Meta information"
msgid "Additional order information"
msgstr "Informació meta"
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
msgstr "Esborrar comanda" msgstr "Esborrar comanda"
@@ -24185,6 +24343,23 @@ msgstr "Sí, denegar la comanda"
msgid "Extend payment term" msgid "Extend payment term"
msgstr "Ampliar el termini de pagament" msgstr "Ampliar el termini de pagament"
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid "UNSAFE"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
@@ -24479,25 +24654,10 @@ msgstr "Entrada: %(datetime)s"
msgid "Ticket page" msgid "Ticket page"
msgstr "Tiquet" msgstr "Tiquet"
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
msgid "This question will be asked during check-in." msgid "This question will be asked during check-in."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid "UNSAFE"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/overview.html #: pretix/control/templates/pretixcontrol/orders/overview.html
#: pretix/plugins/reports/accountingreport.py #: pretix/plugins/reports/accountingreport.py
@@ -25887,6 +26047,16 @@ msgstr "Connectar un dispositiu"
msgid "Hardware model" msgid "Hardware model"
msgstr "Model de hardware" msgstr "Model de hardware"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, fuzzy, python-format
#| msgid "Begin: %(time)s"
msgid "Last seen: %(time)s"
msgstr "Inici: %(time)s"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
msgstr "Encara no està inicialitzat" msgstr "Encara no està inicialitzat"
@@ -29031,10 +29201,6 @@ msgstr ""
msgid "Not all objects have been selected." msgid "Not all objects have been selected."
msgstr "Aquesta posició de la comanda s'ha cancel·lat." msgstr "Aquesta posició de la comanda s'ha cancel·lat."
#: pretix/control/views/item.py
msgid "Street"
msgstr "Carrer"
#: pretix/control/views/item.py #: pretix/control/views/item.py
msgid "The requested question does not exist." msgid "The requested question does not exist."
msgstr "" msgstr ""
@@ -29182,7 +29348,7 @@ msgstr ""
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "" msgid ""
"We did not find DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
@@ -29616,7 +29782,8 @@ msgstr ""
msgid "Your message has been queued and will be sent to {}." msgid "Your message has been queued and will be sent to {}."
msgstr "" msgstr ""
#: pretix/control/views/orders.py pretix/presale/views/order.py #: pretix/control/views/orders.py pretix/presale/views/cart.py
#: pretix/presale/views/order.py
msgid "" msgid ""
"This link is no longer valid. Please go back, refresh the page, and try " "This link is no longer valid. Please go back, refresh the page, and try "
"again." "again."
@@ -31911,6 +32078,29 @@ msgid ""
"getting your money right away." "getting your money right away."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during "
"this time window. You can allow your customers to start another payment "
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
#, fuzzy #, fuzzy
#| msgid "Stripe account" #| msgid "Stripe account"
@@ -32177,8 +32367,11 @@ msgstr "Si us plau, trieu com voleu fer el pagament."
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
msgid "" msgid ""
"Our attempt to execute your payment via PayPal has failed. Please try again " "Your payment is being processed by PayPal. This takes longer than usual. You "
"or contact us." "can wait until PayPal acknowledges the payment or you can try paying again "
"with this or another payment method. This might result in you being charged "
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
@@ -35320,10 +35513,6 @@ msgstr[1] "Ja s'ha validat aquest tiquet."
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "Mostres els noms dels assistents a les factures" msgstr "Mostres els noms dels assistents a les factures"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The price of this product was reduced because of an automatic discount." msgid "The price of this product was reduced because of an automatic discount."
msgstr "" msgstr ""
@@ -35779,6 +35968,10 @@ msgstr "Valor total (sense imposts)"
msgid "Set price in %(currency)s for %(item)s" msgid "Set price in %(currency)s for %(item)s"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
@@ -37705,11 +37898,6 @@ msgstr "Kosovo"
#~ msgid "Please do not use special characters in names." #~ msgid "Please do not use special characters in names."
#~ msgstr "Si us plau, introduïu un nom més curt." #~ msgstr "Si us plau, introduïu un nom més curt."
#, fuzzy
#~| msgid "Status"
#~ msgid "State"
#~ msgstr "Estat"
#, fuzzy #, fuzzy
#~| msgid "Printing date and time" #~| msgid "Printing date and time"
#~ msgid "Program times: date and time" #~ msgid "Program times: date and time"
@@ -38154,9 +38342,6 @@ msgstr "Kosovo"
#~ msgid "Badge design" #~ msgid "Badge design"
#~ msgstr "Disseny de la tarja d'identificació" #~ msgstr "Disseny de la tarja d'identificació"
#~ msgid "(Same as above)"
#~ msgstr "(Igual que aquí al damunt)"
#~ msgid "Translation in development" #~ msgid "Translation in development"
#~ msgstr "Traducció en progrés" #~ msgstr "Traducció en progrés"
+249 -66
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-01-29 19:42+0000\n" "PO-Revision-Date: 2026-01-29 19:42+0000\n"
"Last-Translator: Jiří Pastrňák <jiri@pastrnak.email>\n" "Last-Translator: Jiří Pastrňák <jiri@pastrnak.email>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/" "Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/"
@@ -91,6 +91,10 @@ msgstr "Řečtina"
msgid "Hebrew" msgid "Hebrew"
msgstr "Hebrejština" msgstr "Hebrejština"
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "Indonéština" msgstr "Indonéština"
@@ -438,6 +442,12 @@ msgstr "Médium je napojené na jinou událost"
msgid "You cannot exchange a medium for a medium." msgid "You cannot exchange a medium for a medium."
msgstr "Tuto objednávku nelze změnit." msgstr "Tuto objednávku nelze změnit."
#: pretix/api/views/checkin.py
#, fuzzy
#| msgid "You cannot change this order."
msgid "You cannot simulate a medium exchange."
msgstr "Tuto objednávku nelze změnit."
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@@ -901,8 +911,7 @@ msgstr "Odeslat informace o obnovení"
#: pretix/control/forms/organizer.py #: pretix/control/forms/organizer.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py #: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py #: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html #: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
@@ -921,7 +930,6 @@ msgstr "Jméno účastníka"
#: pretix/base/forms/questions.py pretix/base/models/customers.py #: pretix/base/forms/questions.py pretix/base/models/customers.py
#: pretix/base/models/orders.py pretix/base/pdf.py #: pretix/base/models/orders.py pretix/base/pdf.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email" msgid "Attendee email"
msgstr "Email účastníka" msgstr "Email účastníka"
@@ -1708,9 +1716,8 @@ msgid "Invoice sender:"
msgstr "Odesílatel faktury:" msgstr "Odesílatel faktury:"
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/modelimport_orders.py pretix/base/models/customers.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/orders.py pretix/control/forms/filter.py
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -1722,8 +1729,7 @@ msgstr "Adresa"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "ZIP code" msgid "ZIP code"
msgstr "PSČ" msgstr "PSČ"
@@ -1731,8 +1737,7 @@ msgstr "PSČ"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "City" msgid "City"
msgstr "Město" msgstr "Město"
@@ -1755,8 +1760,7 @@ msgstr "Stát"
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/email/login_notice.txt #: pretix/control/templates/pretixcontrol/email/login_notice.txt
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Country" msgid "Country"
@@ -1784,8 +1788,7 @@ msgstr "Příjemce faktury:"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Company" msgid "Company"
@@ -2598,7 +2601,8 @@ msgstr "Stavy platby"
msgid "Refund states" msgid "Refund states"
msgstr "Stavy vrácení peněz" msgstr "Stavy vrácení peněz"
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py #: pretix/base/exporters/orderlist.py pretix/base/models/items.py
#: pretix/base/models/orders.py
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html #: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html #: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
@@ -3117,16 +3121,12 @@ msgstr ""
"byl poškozený." "byl poškozený."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid "Street"
"If you keep this empty, the ticket will be valid starting at the time of " msgstr "Ulice"
"purchase."
msgstr ""
"Pokud toto políčko necháte prázdné, bude vstupenka platná od okamžiku "
"zakoupení."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "Street and Number" msgid "State"
msgstr "Ulice a číslo popisné" msgstr "Stav"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
@@ -3163,6 +3163,18 @@ msgstr "Zadejte prosím platný prodejní kanál."
msgid "Please enter a date and time no later than {max}." msgid "Please enter a date and time no later than {max}."
msgstr "Zadejte prosím správný výsledek." msgstr "Zadejte prosím správný výsledek."
#: pretix/base/forms/questions.py
msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr ""
"Pokud toto políčko necháte prázdné, bude vstupenka platná od okamžiku "
"zakoupení."
#: pretix/base/forms/questions.py
msgid "Street and Number"
msgstr "Ulice a číslo popisné"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
@@ -5725,6 +5737,10 @@ msgstr "Tato varianta nepatří k tomuto produktu."
msgid "The count needs to be equal to or greater than zero." msgid "The count needs to be equal to or greater than zero."
msgstr "Počet musí být alespoň nula." msgstr "Počet musí být alespoň nula."
#: pretix/base/models/items.py pretix/base/models/orders.py
msgid "Order position"
msgstr "Objednaný produkt"
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Number" msgid "Number"
msgstr "Číslo" msgstr "Číslo"
@@ -5762,6 +5778,10 @@ msgstr "Datum a čas"
msgid "Country code (ISO 3166-1 alpha-2)" msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "Země (ISO3166-1-alpha-2-kód zemí)" msgstr "Země (ISO3166-1-alpha-2-kód zemí)"
#: pretix/base/models/items.py
msgid "Asked on"
msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
"The identifier may only contain letters, numbers, dots, dashes, and " "The identifier may only contain letters, numbers, dots, dashes, and "
@@ -6420,10 +6440,6 @@ msgstr "Dárkový poukaz"
msgid "Value" msgid "Value"
msgstr "Hodnota" msgstr "Hodnota"
#: pretix/base/models/orders.py
msgid "Order position"
msgstr "Objednaný produkt"
#: pretix/base/models/orders.py #: pretix/base/models/orders.py
msgid "Cart ID (e.g. session key)" msgid "Cart ID (e.g. session key)"
msgstr "ID košíku (např. klíč relace)" msgstr "ID košíku (např. klíč relace)"
@@ -7414,7 +7430,7 @@ msgstr "Platba za tuto fakturu již byla přijata."
#, fuzzy #, fuzzy
#| msgid "This payment can not be canceled at the moment." #| msgid "This payment can not be canceled at the moment."
msgid "" msgid ""
"This payment is already being processed and can not be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "Tuto platbu nelze dočasně zrušit." msgstr "Tuto platbu nelze dočasně zrušit."
#: pretix/base/payment.py #: pretix/base/payment.py
@@ -8212,6 +8228,18 @@ msgstr "Zahájení předprodeje"
msgid "Presale end" msgid "Presale end"
msgstr "Konec předprodeje" msgstr "Konec předprodeje"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order details"
msgid "Order creation"
msgstr "Podrobnosti o objednávce"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order expired"
msgid "Order expiry"
msgstr "Objednávka vypršela"
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
msgstr "před" msgstr "před"
@@ -8236,6 +8264,26 @@ msgstr "Relativní čas:"
msgid "Not set" msgid "Not set"
msgstr "Nenastaveno" msgstr "Nenastaveno"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
msgstr "Náhodný (výchozí, funguje se všemi aplikacemi pretix)" msgstr "Náhodný (výchozí, funguje se všemi aplikacemi pretix)"
@@ -9529,7 +9577,7 @@ msgstr "Částka byla stržena z vaší karty."
msgid "Please transfer money to this bank account: 9999-9999-9999-9999" msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "Peníze prosím převeďte na tento bankovní účet: 9999-9999-9999-9999" msgstr "Peníze prosím převeďte na tento bankovní účet: 9999-9999-9999-9999"
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py #: pretix/base/services/placeholders.py
msgid "This value will be replaced based on dynamic parameters." msgid "This value will be replaced based on dynamic parameters."
msgstr "" msgstr ""
@@ -10228,6 +10276,25 @@ msgstr ""
"prodeje pro tuto akci. Můžete jej použít k popisu dalších možností, jak " "prodeje pro tuto akci. Můžete jej použít k popisu dalších možností, jak "
"získat vstupenku, například na pokladně." "získat vstupenku, například na pokladně."
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid ""
"This text will be shown if the calendar or list of dates is empty, e.g. "
"because a month does not contain any dates or a filter chosen by the user "
"does not find any results. You can use this to advertise ways to get in "
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
msgstr "Nápověda" msgstr "Nápověda"
@@ -14111,6 +14178,30 @@ msgstr ""
"Nastavili jste platnost dárkových karet na {} let plus rok vydání dárkové " "Nastavili jste platnost dárkových karet na {} let plus rok vydání dárkové "
"karty." "karty."
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment date"
msgid "Payment term"
msgstr "Datum splatnosti"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "(Same as above)"
msgid "same as above"
msgstr "(Stejně jako výše)"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in days"
msgid "different payment term in days"
msgstr "Doba splatnosti ve dnech"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in minutes"
msgid "different payment term in minutes"
msgstr "Platební lhůta v minutách"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy #, fuzzy
#| msgid "Price including add-ons" #| msgid "Price including add-ons"
@@ -20975,6 +21066,7 @@ msgid "Top recommendation"
msgstr "Doporučené rozšíření" msgstr "Doporučené rozšíření"
#: pretix/control/templates/pretixcontrol/event/plugins.html #: pretix/control/templates/pretixcontrol/event/plugins.html
#: pretix/control/templates/pretixcontrol/items/questions.html
#: pretix/control/templates/pretixcontrol/organizers/plugins.html #: pretix/control/templates/pretixcontrol/organizers/plugins.html
msgid "Experimental feature" msgid "Experimental feature"
msgstr "Experimentální funkce" msgstr "Experimentální funkce"
@@ -22720,6 +22812,30 @@ msgid ""
"dietary requirements." "dietary requirements."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new customer"
msgid "Create a new per-ticket question"
msgstr "Vytvořit nového zákazníka"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new event"
msgid "Create a new order-level question"
msgstr "Vytvořit novou událost"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Delete question"
msgid "Per-ticket questions"
msgstr "Smazat otázku"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
msgstr "" msgstr ""
@@ -22736,6 +22852,30 @@ msgstr ""
msgid "All personalized products" msgid "All personalized products"
msgstr "Všechny personalizované produkty" msgstr "Všechny personalizované produkty"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Include questions"
msgid "Per-order questions"
msgstr "Zobrazit otázky"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "The question has been reordered."
msgid "These questions are asked once per order."
msgstr "Otázka byla přeřazena."
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
#, python-format #, python-format
@@ -23276,6 +23416,15 @@ msgstr "Fakturační údaje"
msgid "(optional)" msgid "(optional)"
msgstr "(volitelné)" msgstr "(volitelné)"
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
#, fuzzy
#| msgid "Additional information"
msgid "Additional order information"
msgstr "Další informace"
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
msgstr "" msgstr ""
@@ -23303,6 +23452,23 @@ msgstr ""
msgid "Extend payment term" msgid "Extend payment term"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid "UNSAFE"
msgstr "NEZABEZPEČENÉ"
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "not answered"
msgstr "není odpovězeno"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
@@ -23579,25 +23745,10 @@ msgstr "Platný %(datetime_range)s"
msgid "Ticket page" msgid "Ticket page"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "not answered"
msgstr "není odpovězeno"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
msgid "This question will be asked during check-in." msgid "This question will be asked during check-in."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid "UNSAFE"
msgstr "NEZABEZPEČENÉ"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/overview.html #: pretix/control/templates/pretixcontrol/orders/overview.html
#: pretix/plugins/reports/accountingreport.py #: pretix/plugins/reports/accountingreport.py
@@ -24925,6 +25076,16 @@ msgstr "Připojit zařízení"
msgid "Hardware model" msgid "Hardware model"
msgstr "Model hardwaru" msgstr "Model hardwaru"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, fuzzy, python-format
#| msgid "Begin: %(time)s"
msgid "Last seen: %(time)s"
msgstr "Začátek: %(time)s"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
msgstr "Zatím neinicializováno" msgstr "Zatím neinicializováno"
@@ -27992,10 +28153,6 @@ msgstr ""
msgid "Not all objects have been selected." msgid "Not all objects have been selected."
msgstr "" msgstr ""
#: pretix/control/views/item.py
msgid "Street"
msgstr "Ulice"
#: pretix/control/views/item.py #: pretix/control/views/item.py
msgid "The requested question does not exist." msgid "The requested question does not exist."
msgstr "" msgstr ""
@@ -28187,7 +28344,7 @@ msgstr ""
#| "You should update the DNS settings of your domain to include this system " #| "You should update the DNS settings of your domain to include this system "
#| "in the SPF record." #| "in the SPF record."
msgid "" msgid ""
"We did not find DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
@@ -28593,7 +28750,8 @@ msgstr "Předmět: {subject}"
msgid "Your message has been queued and will be sent to {}." msgid "Your message has been queued and will be sent to {}."
msgstr "" msgstr ""
#: pretix/control/views/orders.py pretix/presale/views/order.py #: pretix/control/views/orders.py pretix/presale/views/cart.py
#: pretix/presale/views/order.py
msgid "" msgid ""
"This link is no longer valid. Please go back, refresh the page, and try " "This link is no longer valid. Please go back, refresh the page, and try "
"again." "again."
@@ -30772,6 +30930,29 @@ msgid ""
"getting your money right away." "getting your money right away."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during "
"this time window. You can allow your customers to start another payment "
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
msgstr "-- Automaticky --" msgstr "-- Automaticky --"
@@ -31009,13 +31190,13 @@ msgid "Please select how you want to pay."
msgstr "Vyberte, jak chcete platit." msgstr "Vyberte, jak chcete platit."
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
#, fuzzy
msgid "" msgid ""
"Our attempt to execute your payment via PayPal has failed. Please try again " "Your payment is being processed by PayPal. This takes longer than usual. You "
"or contact us." "can wait until PayPal acknowledges the payment or you can try paying again "
"with this or another payment method. This might result in you being charged "
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr "" msgstr ""
"Náš pokus o provedení vaší platby přes PayPal se nezdařil. Zkuste to prosím "
"znovu nebo kontaktujte nás."
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
msgid "" msgid ""
@@ -33950,10 +34131,6 @@ msgstr[2] "Vstupenka byla použita %(count)s krát."
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "Nebylo uvedeno žádné jméno účastníka" msgstr "Nebylo uvedeno žádné jméno účastníka"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
msgstr "Obrázek, který jste předtím nahráli"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The price of this product was reduced because of an automatic discount." msgid "The price of this product was reduced because of an automatic discount."
msgstr "Cena tohoto produktu byla snížena díky automatické slevě." msgstr "Cena tohoto produktu byla snížena díky automatické slevě."
@@ -34380,6 +34557,10 @@ msgstr "%(value)s Celková cena (bez DPH)"
msgid "Set price in %(currency)s for %(item)s" msgid "Set price in %(currency)s for %(item)s"
msgstr "Nastavení ceny v %(currency)s pro %(item)s" msgstr "Nastavení ceny v %(currency)s pro %(item)s"
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "The image you previously uploaded"
msgstr "Obrázek, který jste předtím nahráli"
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
@@ -36150,6 +36331,14 @@ msgstr "Přístup k zápisu"
msgid "Kosovo" msgid "Kosovo"
msgstr "Kosovo" msgstr "Kosovo"
#, fuzzy
#~ msgid ""
#~ "Our attempt to execute your payment via PayPal has failed. Please try "
#~ "again or contact us."
#~ msgstr ""
#~ "Náš pokus o provedení vaší platby přes PayPal se nezdařil. Zkuste to "
#~ "prosím znovu nebo kontaktujte nás."
#~ msgid "All actions" #~ msgid "All actions"
#~ msgstr "Všechny akce" #~ msgstr "Všechny akce"
@@ -36247,9 +36436,6 @@ msgstr "Kosovo"
#~ msgid "Please do not use special characters in names." #~ msgid "Please do not use special characters in names."
#~ msgstr "Ve jméně není dovoleno používat speciální znaky." #~ msgstr "Ve jméně není dovoleno používat speciální znaky."
#~ msgid "State"
#~ msgstr "Stav"
#, fuzzy #, fuzzy
#~| msgid "Printing date and time" #~| msgid "Printing date and time"
#~ msgid "Program times: date and time" #~ msgid "Program times: date and time"
@@ -36796,9 +36982,6 @@ msgstr "Kosovo"
#~ msgid "minutes before" #~ msgid "minutes before"
#~ msgstr "Minuty před" #~ msgstr "Minuty před"
#~ msgid "(Same as above)"
#~ msgstr "(Stejně jako výše)"
#~ msgid "Translation in development" #~ msgid "Translation in development"
#~ msgstr "Překlad je v průběhu" #~ msgstr "Překlad je v průběhu"
+220 -54
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2023-11-14 11:00+0000\n" "PO-Revision-Date: 2023-11-14 11:00+0000\n"
"Last-Translator: Charliecoleg <DM229135@colegsirgar.ac.uk>\n" "Last-Translator: Charliecoleg <DM229135@colegsirgar.ac.uk>\n"
"Language-Team: Welsh <https://translate.pretix.eu/projects/pretix/pretix/cy/" "Language-Team: Welsh <https://translate.pretix.eu/projects/pretix/pretix/cy/"
@@ -92,6 +92,10 @@ msgstr "Groeg"
msgid "Hebrew" msgid "Hebrew"
msgstr "" msgstr ""
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "Indonesaidd" msgstr "Indonesaidd"
@@ -415,6 +419,10 @@ msgstr ""
msgid "You cannot exchange a medium for a medium." msgid "You cannot exchange a medium for a medium."
msgstr "" msgstr ""
#: pretix/api/views/checkin.py
msgid "You cannot simulate a medium exchange."
msgstr ""
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@@ -853,8 +861,7 @@ msgstr ""
#: pretix/control/forms/organizer.py #: pretix/control/forms/organizer.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py #: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py #: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html #: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
@@ -871,7 +878,6 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/models/customers.py #: pretix/base/forms/questions.py pretix/base/models/customers.py
#: pretix/base/models/orders.py pretix/base/pdf.py #: pretix/base/models/orders.py pretix/base/pdf.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email" msgid "Attendee email"
msgstr "" msgstr ""
@@ -1609,9 +1615,8 @@ msgid "Invoice sender:"
msgstr "" msgstr ""
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/modelimport_orders.py pretix/base/models/customers.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/orders.py pretix/control/forms/filter.py
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -1623,8 +1628,7 @@ msgstr "Cyfeiriad"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "ZIP code" msgid "ZIP code"
msgstr "" msgstr ""
@@ -1632,8 +1636,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "City" msgid "City"
msgstr "Dinas" msgstr "Dinas"
@@ -1656,8 +1659,7 @@ msgstr "Talaith"
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/email/login_notice.txt #: pretix/control/templates/pretixcontrol/email/login_notice.txt
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Country" msgid "Country"
@@ -1685,8 +1687,7 @@ msgstr ""
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Company" msgid "Company"
@@ -2473,7 +2474,8 @@ msgstr ""
msgid "Refund states" msgid "Refund states"
msgstr "" msgstr ""
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py #: pretix/base/exporters/orderlist.py pretix/base/models/items.py
#: pretix/base/models/orders.py
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html #: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html #: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
@@ -2978,14 +2980,15 @@ msgid ""
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid "Street"
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr "" msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "Street and Number" #, fuzzy
msgstr "" #| msgctxt "address"
#| msgid "State"
msgid "State"
msgstr "Talaith"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
#, python-brace-format #, python-brace-format
@@ -3017,6 +3020,16 @@ msgstr ""
msgid "Please enter a date and time no later than {max}." msgid "Please enter a date and time no later than {max}."
msgstr "" msgstr ""
#: pretix/base/forms/questions.py
msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr ""
#: pretix/base/forms/questions.py
msgid "Street and Number"
msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
@@ -5349,6 +5362,10 @@ msgstr ""
msgid "The count needs to be equal to or greater than zero." msgid "The count needs to be equal to or greater than zero."
msgstr "" msgstr ""
#: pretix/base/models/items.py pretix/base/models/orders.py
msgid "Order position"
msgstr ""
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Number" msgid "Number"
msgstr "" msgstr ""
@@ -5386,6 +5403,10 @@ msgstr ""
msgid "Country code (ISO 3166-1 alpha-2)" msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "" msgstr ""
#: pretix/base/models/items.py
msgid "Asked on"
msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
"The identifier may only contain letters, numbers, dots, dashes, and " "The identifier may only contain letters, numbers, dots, dashes, and "
@@ -5985,10 +6006,6 @@ msgstr ""
msgid "Value" msgid "Value"
msgstr "" msgstr ""
#: pretix/base/models/orders.py
msgid "Order position"
msgstr ""
#: pretix/base/models/orders.py #: pretix/base/models/orders.py
msgid "Cart ID (e.g. session key)" msgid "Cart ID (e.g. session key)"
msgstr "" msgstr ""
@@ -6904,7 +6921,7 @@ msgstr ""
#: pretix/base/payment.py #: pretix/base/payment.py
msgid "" msgid ""
"This payment is already being processed and can not be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "" msgstr ""
#: pretix/base/payment.py #: pretix/base/payment.py
@@ -7640,6 +7657,16 @@ msgstr ""
msgid "Presale end" msgid "Presale end"
msgstr "" msgstr ""
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Create"
msgid "Order creation"
msgstr "Creu"
#: pretix/base/reldate.py
msgid "Order expiry"
msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
msgstr "" msgstr ""
@@ -7664,6 +7691,26 @@ msgstr ""
msgid "Not set" msgid "Not set"
msgstr "" msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
msgstr "" msgstr ""
@@ -8761,7 +8808,7 @@ msgstr ""
msgid "Please transfer money to this bank account: 9999-9999-9999-9999" msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "" msgstr ""
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py #: pretix/base/services/placeholders.py
msgid "This value will be replaced based on dynamic parameters." msgid "This value will be replaced based on dynamic parameters."
msgstr "" msgstr ""
@@ -9342,6 +9389,25 @@ msgid ""
"to get a ticket, such as a box office." "to get a ticket, such as a box office."
msgstr "" msgstr ""
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid ""
"This text will be shown if the calendar or list of dates is empty, e.g. "
"because a month does not contain any dates or a filter chosen by the user "
"does not find any results. You can use this to advertise ways to get in "
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
msgstr "" msgstr ""
@@ -12498,6 +12564,24 @@ msgid ""
"card is issued in." "card is issued in."
msgstr "" msgstr ""
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment"
msgid "Payment term"
msgstr "Taliad"
#: pretix/control/forms/event.py
msgid "same as above"
msgstr ""
#: pretix/control/forms/event.py
msgid "different payment term in days"
msgstr ""
#: pretix/control/forms/event.py
msgid "different payment term in minutes"
msgstr ""
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
msgstr "" msgstr ""
@@ -18686,6 +18770,7 @@ msgid "Top recommendation"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/event/plugins.html #: pretix/control/templates/pretixcontrol/event/plugins.html
#: pretix/control/templates/pretixcontrol/items/questions.html
#: pretix/control/templates/pretixcontrol/organizers/plugins.html #: pretix/control/templates/pretixcontrol/organizers/plugins.html
msgid "Experimental feature" msgid "Experimental feature"
msgstr "" msgstr ""
@@ -20370,6 +20455,24 @@ msgid ""
"dietary requirements." "dietary requirements."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new per-ticket question"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new order-level question"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-ticket questions"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
msgstr "" msgstr ""
@@ -20386,6 +20489,26 @@ msgstr ""
msgid "All personalized products" msgid "All personalized products"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-order questions"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "These questions are asked once per order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
#, python-format #, python-format
@@ -20914,6 +21037,15 @@ msgstr ""
msgid "(optional)" msgid "(optional)"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
#, fuzzy
#| msgid "Additional fee"
msgid "Additional order information"
msgstr "Ffi ychwanegol"
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
msgstr "" msgstr ""
@@ -20941,6 +21073,23 @@ msgstr ""
msgid "Extend payment term" msgid "Extend payment term"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid "UNSAFE"
msgstr "ANNIOGEL"
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
@@ -21196,25 +21345,10 @@ msgstr ""
msgid "Ticket page" msgid "Ticket page"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "not answered"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
msgid "This question will be asked during check-in." msgid "This question will be asked during check-in."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid "UNSAFE"
msgstr "ANNIOGEL"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/overview.html #: pretix/control/templates/pretixcontrol/orders/overview.html
#: pretix/plugins/reports/accountingreport.py #: pretix/plugins/reports/accountingreport.py
@@ -22466,6 +22600,15 @@ msgstr ""
msgid "Hardware model" msgid "Hardware model"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, python-format
msgid "Last seen: %(time)s"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
msgstr "" msgstr ""
@@ -25367,10 +25510,6 @@ msgstr ""
msgid "Not all objects have been selected." msgid "Not all objects have been selected."
msgstr "" msgstr ""
#: pretix/control/views/item.py
msgid "Street"
msgstr ""
#: pretix/control/views/item.py #: pretix/control/views/item.py
msgid "The requested question does not exist." msgid "The requested question does not exist."
msgstr "" msgstr ""
@@ -25524,7 +25663,7 @@ msgstr ""
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "" msgid ""
"We did not find DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
@@ -25914,7 +26053,8 @@ msgstr ""
msgid "Your message has been queued and will be sent to {}." msgid "Your message has been queued and will be sent to {}."
msgstr "" msgstr ""
#: pretix/control/views/orders.py pretix/presale/views/order.py #: pretix/control/views/orders.py pretix/presale/views/cart.py
#: pretix/presale/views/order.py
msgid "" msgid ""
"This link is no longer valid. Please go back, refresh the page, and try " "This link is no longer valid. Please go back, refresh the page, and try "
"again." "again."
@@ -27971,6 +28111,29 @@ msgid ""
"getting your money right away." "getting your money right away."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during "
"this time window. You can allow your customers to start another payment "
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
msgstr "" msgstr ""
@@ -28196,8 +28359,11 @@ msgstr ""
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
msgid "" msgid ""
"Our attempt to execute your payment via PayPal has failed. Please try again " "Your payment is being processed by PayPal. This takes longer than usual. You "
"or contact us." "can wait until PayPal acknowledges the payment or you can try paying again "
"with this or another payment method. This might result in you being charged "
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
@@ -30946,10 +31112,6 @@ msgstr[1] ""
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The price of this product was reduced because of an automatic discount." msgid "The price of this product was reduced because of an automatic discount."
msgstr "" msgstr ""
@@ -31356,6 +31518,10 @@ msgstr ""
msgid "Set price in %(currency)s for %(item)s" msgid "Set price in %(currency)s for %(item)s"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
+243 -63
View File
@@ -3,11 +3,11 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-07-31 18:00+0000\n" "PO-Revision-Date: 2026-07-31 18:00+0000\n"
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n" "Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
"da/>\n" ">\n"
"Language: da\n" "Language: da\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -87,6 +87,10 @@ msgstr "Græsk"
msgid "Hebrew" msgid "Hebrew"
msgstr "hebræisk" msgstr "hebræisk"
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "Indonesisk" msgstr "Indonesisk"
@@ -429,6 +433,12 @@ msgstr "Medie er tilknyttet et andet arrangement"
msgid "You cannot exchange a medium for a medium." msgid "You cannot exchange a medium for a medium."
msgstr "Du kan ikke bytte en medium mod en anden medium." msgstr "Du kan ikke bytte en medium mod en anden medium."
#: pretix/api/views/checkin.py
#, fuzzy
#| msgid "Product does not support medium exchange."
msgid "You cannot simulate a medium exchange."
msgstr "Produktet understøtter ikke udskiftning af medier."
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@@ -878,8 +888,7 @@ msgstr "Oplysninger om begivenhed eller dato"
#: pretix/control/forms/organizer.py #: pretix/control/forms/organizer.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py #: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py #: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html #: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
@@ -896,7 +905,6 @@ msgstr "Deltager"
#: pretix/base/forms/questions.py pretix/base/models/customers.py #: pretix/base/forms/questions.py pretix/base/models/customers.py
#: pretix/base/models/orders.py pretix/base/pdf.py #: pretix/base/models/orders.py pretix/base/pdf.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email" msgid "Attendee email"
msgstr "E-mailadresse" msgstr "E-mailadresse"
@@ -1644,9 +1652,8 @@ msgid "Invoice sender:"
msgstr "Fakturaafsender:" msgstr "Fakturaafsender:"
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/modelimport_orders.py pretix/base/models/customers.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/orders.py pretix/control/forms/filter.py
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -1658,8 +1665,7 @@ msgstr "Adresse"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "ZIP code" msgid "ZIP code"
msgstr "Postnummer" msgstr "Postnummer"
@@ -1667,8 +1673,7 @@ msgstr "Postnummer"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "City" msgid "City"
msgstr "By" msgstr "By"
@@ -1691,8 +1696,7 @@ msgstr "Stat / region"
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/email/login_notice.txt #: pretix/control/templates/pretixcontrol/email/login_notice.txt
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Country" msgid "Country"
@@ -1720,8 +1724,7 @@ msgstr "Fakturamodtager:"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Company" msgid "Company"
@@ -2534,7 +2537,8 @@ msgstr "Status på betalinger"
msgid "Refund states" msgid "Refund states"
msgstr "Status på tilbagebetalinger" msgstr "Status på tilbagebetalinger"
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py #: pretix/base/exporters/orderlist.py pretix/base/models/items.py
#: pretix/base/models/orders.py
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html #: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html #: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
@@ -3053,16 +3057,13 @@ msgstr ""
"eller den var beskadiget." "eller den var beskadiget."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" #, fuzzy
"If you keep this empty, the ticket will be valid starting at the time of " msgid "Street"
"purchase." msgstr "Afsenderadresse"
msgstr ""
"Hvis du lader dette felt stå tomt, vil billetten være gyldig direkte efter "
"køb."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "Street and Number" msgid "State"
msgstr "Gadenavn og nummer" msgstr "Stat eller region"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
#, python-brace-format #, python-brace-format
@@ -3096,6 +3097,18 @@ msgstr ""
msgid "Please enter a date and time no later than {max}." msgid "Please enter a date and time no later than {max}."
msgstr "Please enter a date and time no later than {max}." msgstr "Please enter a date and time no later than {max}."
#: pretix/base/forms/questions.py
msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr ""
"Hvis du lader dette felt stå tomt, vil billetten være gyldig direkte efter "
"køb."
#: pretix/base/forms/questions.py
msgid "Street and Number"
msgstr "Gadenavn og nummer"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
@@ -5704,6 +5717,10 @@ msgstr "Den valgte variation tilhører ikke dette arrangement."
msgid "The count needs to be equal to or greater than zero." msgid "The count needs to be equal to or greater than zero."
msgstr "Antallet skal være lig med eller større end nul." msgstr "Antallet skal være lig med eller større end nul."
#: pretix/base/models/items.py pretix/base/models/orders.py
msgid "Order position"
msgstr "Bestillingspost"
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Number" msgid "Number"
msgstr "Antal" msgstr "Antal"
@@ -5741,6 +5758,10 @@ msgstr "Dato og tidspunkt"
msgid "Country code (ISO 3166-1 alpha-2)" msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "Landekode (ISO 3166-1 alpha-2)" msgstr "Landekode (ISO 3166-1 alpha-2)"
#: pretix/base/models/items.py
msgid "Asked on"
msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
"The identifier may only contain letters, numbers, dots, dashes, and " "The identifier may only contain letters, numbers, dots, dashes, and "
@@ -6396,10 +6417,6 @@ msgstr "Gavekort"
msgid "Value" msgid "Value"
msgstr "Værdi" msgstr "Værdi"
#: pretix/base/models/orders.py
msgid "Order position"
msgstr "Bestillingspost"
#: pretix/base/models/orders.py #: pretix/base/models/orders.py
msgid "Cart ID (e.g. session key)" msgid "Cart ID (e.g. session key)"
msgstr "Kurv-id (fx sessionsnøgle)" msgstr "Kurv-id (fx sessionsnøgle)"
@@ -7424,8 +7441,11 @@ msgid "The payment for this invoice has already been received."
msgstr "Betalingen for denne faktura er allerede modtaget." msgstr "Betalingen for denne faktura er allerede modtaget."
#: pretix/base/payment.py #: pretix/base/payment.py
#, fuzzy
#| msgid ""
#| "This payment is already being processed and can not be canceled any more."
msgid "" msgid ""
"This payment is already being processed and can not be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "" msgstr ""
"Denne betaling er allerede under behandling og kan ikke længere annulleres." "Denne betaling er allerede under behandling og kan ikke længere annulleres."
@@ -8195,6 +8215,18 @@ msgstr "Forsalgs starttidspunkt"
msgid "Presale end" msgid "Presale end"
msgstr "Forsalgs sluttidspunkt" msgstr "Forsalgs sluttidspunkt"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order email"
msgid "Order creation"
msgstr "Bestillings-e-mail"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order expired"
msgid "Order expiry"
msgstr "Bestilling udløbet"
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
msgstr "før" msgstr "før"
@@ -8219,6 +8251,26 @@ msgstr "Relativt tidspunkt:"
msgid "Not set" msgid "Not set"
msgstr "Ikke sat" msgstr "Ikke sat"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
msgstr "Tilfældigt (standard, virker med alle Pretix-apps)" msgstr "Tilfældigt (standard, virker med alle Pretix-apps)"
@@ -9488,7 +9540,7 @@ msgstr "Beløbet er blevet trukket på dit kort."
msgid "Please transfer money to this bank account: 9999-9999-9999-9999" msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "Overfør venligst beløbet til denne bankkonto: 9999-9999-9999-9999" msgstr "Overfør venligst beløbet til denne bankkonto: 9999-9999-9999-9999"
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py #: pretix/base/services/placeholders.py
msgid "This value will be replaced based on dynamic parameters." msgid "This value will be replaced based on dynamic parameters."
msgstr "Denne værdi vil blive erstattet på baggrund af dynamiske parametre." msgstr "Denne værdi vil blive erstattet på baggrund af dynamiske parametre."
@@ -10196,6 +10248,25 @@ msgstr ""
"for dette arrangement er afsluttet. Du kan bruge den til at beskrive andre " "for dette arrangement er afsluttet. Du kan bruge den til at beskrive andre "
"muligheder for at få en billet, f.eks. et billetkontor." "muligheder for at få en billet, f.eks. et billetkontor."
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid ""
"This text will be shown if the calendar or list of dates is empty, e.g. "
"because a month does not contain any dates or a filter chosen by the user "
"does not find any results. You can use this to advertise ways to get in "
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
msgstr "Vejledende tekst" msgstr "Vejledende tekst"
@@ -14120,6 +14191,28 @@ msgstr ""
"Du har konfigureret vouchers til at være gyldige i {} år plus det år, hvor " "Du har konfigureret vouchers til at være gyldige i {} år plus det år, hvor "
"voucheren udstedes." "voucheren udstedes."
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment date"
msgid "Payment term"
msgstr "Betalingsdato"
#: pretix/control/forms/event.py
msgid "same as above"
msgstr ""
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in days"
msgid "different payment term in days"
msgstr "Betalingsfrist i dage"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in minutes"
msgid "different payment term in minutes"
msgstr "Betalingsfrist i minutter"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
msgstr "Priser inkl. moms" msgstr "Priser inkl. moms"
@@ -20841,6 +20934,7 @@ msgid "Top recommendation"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/event/plugins.html #: pretix/control/templates/pretixcontrol/event/plugins.html
#: pretix/control/templates/pretixcontrol/items/questions.html
#: pretix/control/templates/pretixcontrol/organizers/plugins.html #: pretix/control/templates/pretixcontrol/organizers/plugins.html
#, fuzzy #, fuzzy
msgid "Experimental feature" msgid "Experimental feature"
@@ -22620,6 +22714,30 @@ msgid ""
"dietary requirements." "dietary requirements."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new per-ticket question"
msgstr "Opret spørgsmål"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new order-level question"
msgstr "Opret spørgsmål"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Delete question"
msgid "Per-ticket questions"
msgstr "Sket spørgsmål"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
msgstr "Opret spørgsmål" msgstr "Opret spørgsmål"
@@ -22638,6 +22756,29 @@ msgstr ""
msgid "All personalized products" msgid "All personalized products"
msgstr "Alle produkter" msgstr "Alle produkter"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Include questions"
msgid "Per-order questions"
msgstr "Medtag spørgsmål"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
msgid "These questions are asked once per order."
msgstr "Spørgsmålet er blevet slettet."
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
#, python-format #, python-format
@@ -23207,6 +23348,14 @@ msgstr "Faktureringsoplysninger"
msgid "(optional)" msgid "(optional)"
msgstr "(valgfri)" msgstr "(valgfri)"
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
#, fuzzy
msgid "Additional order information"
msgstr "Metaoplysninger"
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
#, fuzzy #, fuzzy
msgid "Delete order" msgid "Delete order"
@@ -23241,6 +23390,23 @@ msgstr "Ja, tilbagebetal bestilling"
msgid "Extend payment term" msgid "Extend payment term"
msgstr "Udvid betalingsfrist" msgstr "Udvid betalingsfrist"
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid "UNSAFE"
msgstr "USIKKER"
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "not answered"
msgstr "ikke besvaret"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
@@ -23520,25 +23686,10 @@ msgstr "Book fra %(start_date)s"
msgid "Ticket page" msgid "Ticket page"
msgstr "Billetnavn" msgstr "Billetnavn"
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "not answered"
msgstr "ikke besvaret"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
msgid "This question will be asked during check-in." msgid "This question will be asked during check-in."
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
msgid "UNSAFE"
msgstr "USIKKER"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/overview.html #: pretix/control/templates/pretixcontrol/orders/overview.html
#: pretix/plugins/reports/accountingreport.py #: pretix/plugins/reports/accountingreport.py
@@ -24907,6 +25058,16 @@ msgstr ""
msgid "Hardware model" msgid "Hardware model"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, fuzzy, python-format
#| msgid "Begin: %(time)s"
msgid "Last seen: %(time)s"
msgstr "Start: %(time)s"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
#, fuzzy #, fuzzy
msgid "Not yet initialized" msgid "Not yet initialized"
@@ -28049,11 +28210,6 @@ msgstr "Rækkefølgen af kategorier er blevet ændret."
msgid "Not all objects have been selected." msgid "Not all objects have been selected."
msgstr "Spørgsmålet er blevet slettet." msgstr "Spørgsmålet er blevet slettet."
#: pretix/control/views/item.py
#, fuzzy
msgid "Street"
msgstr "Afsenderadresse"
#: pretix/control/views/item.py #: pretix/control/views/item.py
msgid "The requested question does not exist." msgid "The requested question does not exist."
msgstr "" msgstr ""
@@ -28204,7 +28360,7 @@ msgstr ""
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "" msgid ""
"We did not find DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
@@ -28638,7 +28794,8 @@ msgstr "Emne: {subject}"
msgid "Your message has been queued and will be sent to {}." msgid "Your message has been queued and will be sent to {}."
msgstr "Din besked vil blive sendt til {}." msgstr "Din besked vil blive sendt til {}."
#: pretix/control/views/orders.py pretix/presale/views/order.py #: pretix/control/views/orders.py pretix/presale/views/cart.py
#: pretix/presale/views/order.py
msgid "" msgid ""
"This link is no longer valid. Please go back, refresh the page, and try " "This link is no longer valid. Please go back, refresh the page, and try "
"again." "again."
@@ -30880,6 +31037,29 @@ msgid ""
"getting your money right away." "getting your money right away."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during "
"this time window. You can allow your customers to start another payment "
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
#, fuzzy #, fuzzy
msgid "-- Automatic --" msgid "-- Automatic --"
@@ -31127,8 +31307,11 @@ msgstr "Vælg venligst hvordan du vil betale."
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
msgid "" msgid ""
"Our attempt to execute your payment via PayPal has failed. Please try again " "Your payment is being processed by PayPal. This takes longer than usual. You "
"or contact us." "can wait until PayPal acknowledges the payment or you can try paying again "
"with this or another payment method. This might result in you being charged "
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr "" msgstr ""
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
@@ -34168,10 +34351,6 @@ msgstr[1] "Denne bestilling er allerede blevet tilbagebetalt."
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "Vis deltagernavne på fakturaer" msgstr "Vis deltagernavne på fakturaer"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The price of this product was reduced because of an automatic discount." msgid "The price of this product was reduced because of an automatic discount."
msgstr "" msgstr ""
@@ -34597,6 +34776,10 @@ msgstr "Samlet værdi"
msgid "Set price in %(currency)s for %(item)s" msgid "Set price in %(currency)s for %(item)s"
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "The image you previously uploaded"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
@@ -36461,9 +36644,6 @@ msgstr "Kosovo"
#~ msgid "Please do not use special characters in names." #~ msgid "Please do not use special characters in names."
#~ msgstr "Brug venligst ikke specialtegn i navne." #~ msgstr "Brug venligst ikke specialtegn i navne."
#~ msgid "State"
#~ msgstr "Stat eller region"
#~ msgid "Program times: date and time" #~ msgid "Program times: date and time"
#~ msgstr "Programtider: dato og klokkeslæt" #~ msgstr "Programtider: dato og klokkeslæt"
+263 -78
View File
@@ -4,8 +4,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-30 13:52+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-07-30 18:31+0000\n" "PO-Revision-Date: 2026-09-14 23:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n" "Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n" "de/>\n"
@@ -14,7 +14,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.7.1\n" "X-Generator: Weblate 2026.9.1\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n" "X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: pretix/_base_settings.py #: pretix/_base_settings.py
@@ -89,6 +89,10 @@ msgstr "Griechisch"
msgid "Hebrew" msgid "Hebrew"
msgstr "Hebräisch" msgstr "Hebräisch"
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr "Ungarisch"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "Indonesisch" msgstr "Indonesisch"
@@ -429,6 +433,10 @@ msgstr "Medium ist mit anderer Veranstaltung verbunden"
msgid "You cannot exchange a medium for a medium." msgid "You cannot exchange a medium for a medium."
msgstr "Sie können kein Medium gegen ein Medium eintauschen." msgstr "Sie können kein Medium gegen ein Medium eintauschen."
#: pretix/api/views/checkin.py
msgid "You cannot simulate a medium exchange."
msgstr "Ein Medientausch kann nicht simuliert werden."
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@@ -883,8 +891,7 @@ msgstr "Veranstaltungs- oder Termininformationen"
#: pretix/control/forms/organizer.py #: pretix/control/forms/organizer.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py #: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/checkinlists/exporters.py
#: pretix/plugins/ticketoutputpdf/exporters.py #: pretix/plugins/ticketoutputpdf/exporters.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html #: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
@@ -901,7 +908,6 @@ msgstr "Teilnehmer*in"
#: pretix/base/forms/questions.py pretix/base/models/customers.py #: pretix/base/forms/questions.py pretix/base/models/customers.py
#: pretix/base/models/orders.py pretix/base/pdf.py #: pretix/base/models/orders.py pretix/base/pdf.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Attendee email" msgid "Attendee email"
msgstr "E-Mail Teilnehmer*in" msgstr "E-Mail Teilnehmer*in"
@@ -1648,9 +1654,8 @@ msgid "Invoice sender:"
msgstr "Rechnungsabsender:" msgstr "Rechnungsabsender:"
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/modelimport_orders.py pretix/base/models/customers.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/orders.py pretix/control/forms/filter.py
#: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -1662,8 +1667,7 @@ msgstr "Adresse"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "ZIP code" msgid "ZIP code"
msgstr "Postleitzahl" msgstr "Postleitzahl"
@@ -1671,8 +1675,7 @@ msgstr "Postleitzahl"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/base/models/customers.py pretix/base/models/orders.py #: pretix/base/models/customers.py pretix/base/models/orders.py
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
msgid "City" msgid "City"
msgstr "Stadt" msgstr "Stadt"
@@ -1695,8 +1698,7 @@ msgstr "Bundesstaat"
#: pretix/base/settings.py pretix/control/forms/filter.py #: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/email/login_notice.txt #: pretix/control/templates/pretixcontrol/email/login_notice.txt
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Country" msgid "Country"
@@ -1724,8 +1726,7 @@ msgstr "Rechnungsempfänger:"
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py #: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py #: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
#: pretix/plugins/reports/exporters.py
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Company" msgid "Company"
@@ -2541,7 +2542,8 @@ msgstr "Zahlungsstatus"
msgid "Refund states" msgid "Refund states"
msgstr "Erstattungsstatus" msgstr "Erstattungsstatus"
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py #: pretix/base/exporters/orderlist.py pretix/base/models/items.py
#: pretix/base/models/orders.py
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html #: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
#: pretix/control/templates/pretixcontrol/organizers/customer.html #: pretix/control/templates/pretixcontrol/organizers/customer.html
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html #: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
@@ -3063,15 +3065,12 @@ msgid ""
msgstr "Die hochgeladene Datei ist kein Bild oder ist beschädigt." msgstr "Die hochgeladene Datei ist kein Bild oder ist beschädigt."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid "Street"
"If you keep this empty, the ticket will be valid starting at the time of " msgstr "Straße"
"purchase."
msgstr ""
"Wenn Sie dieses Feld leer lassen, ist das Ticket ab dem Kaufzeitpunkt gültig."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "Street and Number" msgid "State"
msgstr "Straße und Hausnummer" msgstr "Zustand"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
#, python-brace-format #, python-brace-format
@@ -3103,6 +3102,17 @@ msgstr "Bitte geben Sie einen Zeitpunkt nicht vor {min} ein."
msgid "Please enter a date and time no later than {max}." msgid "Please enter a date and time no later than {max}."
msgstr "Bitte geben Sie einen Zeitpunkt nicht nach {max} ein." msgstr "Bitte geben Sie einen Zeitpunkt nicht nach {max} ein."
#: pretix/base/forms/questions.py
msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr ""
"Wenn Sie dieses Feld leer lassen, ist das Ticket ab dem Kaufzeitpunkt gültig."
#: pretix/base/forms/questions.py
msgid "Street and Number"
msgstr "Straße und Hausnummer"
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgid "" msgid ""
"Optional, but depending on the country you reside in we might need to charge " "Optional, but depending on the country you reside in we might need to charge "
@@ -5746,6 +5756,10 @@ msgstr "Diese Variante gehört nicht zu diesem Produkt."
msgid "The count needs to be equal to or greater than zero." msgid "The count needs to be equal to or greater than zero."
msgstr "Die Anzahl muss mindestens null sein." msgstr "Die Anzahl muss mindestens null sein."
#: pretix/base/models/items.py pretix/base/models/orders.py
msgid "Order position"
msgstr "Bestellposition"
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Number" msgid "Number"
msgstr "Zahl" msgstr "Zahl"
@@ -5783,6 +5797,10 @@ msgstr "Datum und Uhrzeit"
msgid "Country code (ISO 3166-1 alpha-2)" msgid "Country code (ISO 3166-1 alpha-2)"
msgstr "Land (ISO3166-1-alpha-2-Ländercode)" msgstr "Land (ISO3166-1-alpha-2-Ländercode)"
#: pretix/base/models/items.py
msgid "Asked on"
msgstr "Gefragt bei"
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
"The identifier may only contain letters, numbers, dots, dashes, and " "The identifier may only contain letters, numbers, dots, dashes, and "
@@ -6445,10 +6463,6 @@ msgstr "Wertgutschein / Geschenkgutschein"
msgid "Value" msgid "Value"
msgstr "Wert" msgstr "Wert"
#: pretix/base/models/orders.py
msgid "Order position"
msgstr "Bestellposition"
#: pretix/base/models/orders.py #: pretix/base/models/orders.py
msgid "Cart ID (e.g. session key)" msgid "Cart ID (e.g. session key)"
msgstr "Warenkorb-ID (z.B. Session-ID)" msgstr "Warenkorb-ID (z.B. Session-ID)"
@@ -7488,7 +7502,7 @@ msgstr "Die Rechnung wurde bereits beglichen."
#: pretix/base/payment.py #: pretix/base/payment.py
msgid "" msgid ""
"This payment is already being processed and can not be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "" msgstr ""
"Diese Zahlung wird bereits verarbeitet und kann nicht mehr abgebrochen " "Diese Zahlung wird bereits verarbeitet und kann nicht mehr abgebrochen "
"werden." "werden."
@@ -8267,6 +8281,14 @@ msgstr "Start des Vorverkaufs"
msgid "Presale end" msgid "Presale end"
msgstr "Ende des Vorverkaufs" msgstr "Ende des Vorverkaufs"
#: pretix/base/reldate.py
msgid "Order creation"
msgstr "Bestelldatum"
#: pretix/base/reldate.py
msgid "Order expiry"
msgstr "Zahlungsfrist"
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
msgstr "bevor" msgstr "bevor"
@@ -8291,6 +8313,26 @@ msgstr "Relativ:"
msgid "Not set" msgid "Not set"
msgstr "Nicht gesetzt" msgstr "Nicht gesetzt"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr "Ein relatives Datum kann nicht als \"vor {}\" ausgedrückt werden"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr "Ein relatives Datum kann nicht als \"nach {}\" ausgedrückt werden"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr "Ein relativer Zeitpunkt kann nicht als \"vor {}\" ausgedrückt werden"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr "Ein relativer Zeitpunkt kann nicht als \"nach {}\" ausgedrückt werden"
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
msgstr "Zufällig (Standard, funktioniert mit allen pretix-Apps)" msgstr "Zufällig (Standard, funktioniert mit allen pretix-Apps)"
@@ -9591,7 +9633,7 @@ msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "" msgstr ""
"Bitte überweisen Sie den vollen Betrag auf das Bankkonto 9999-9999-9999-" "Bitte überweisen Sie den vollen Betrag auf das Bankkonto 9999-9999-9999-"
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py #: pretix/base/services/placeholders.py
msgid "This value will be replaced based on dynamic parameters." msgid "This value will be replaced based on dynamic parameters."
msgstr "" msgstr ""
"Dieser Textbaustein wird aufgrund verschiedener Parameter dynamisch " "Dieser Textbaustein wird aufgrund verschiedener Parameter dynamisch "
@@ -10306,6 +10348,30 @@ msgstr ""
"Verkaufszeitraum beendet ist. Sie können hier andere Wege ein Ticket zu " "Verkaufszeitraum beendet ist. Sie können hier andere Wege ein Ticket zu "
"bekommen, wie z.B. eine Abendkasse, beschreiben." "bekommen, wie z.B. eine Abendkasse, beschreiben."
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr "Keine Termine passen zu Ihren Kriterien."
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr "Text für leere Terminanzeige"
#: pretix/base/settings.py
msgctxt "subevents"
msgid ""
"This text will be shown if the calendar or list of dates is empty, e.g. "
"because a month does not contain any dates or a filter chosen by the user "
"does not find any results. You can use this to advertise ways to get in "
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
"Dieser Text wird im Kalender oder der Terminliste angezeigt, wenn sie leer "
"ist, weil z.B. der Monat keine Termine enthält oder der vom Nutzer gesetzte "
"Filter keine Treffer hat. Sie können hier Wege bewerben, wie man Kontakt für "
"zusätzliche Termine aufnehmen kann. Wir empfehlen nicht mehr als ein oder "
"zwei Sätze."
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
msgstr "Hilfetext" msgstr "Hilfetext"
@@ -11642,7 +11708,7 @@ msgid ""
msgstr "" msgstr ""
"Hallo,\n" "Hallo,\n"
"\n" "\n"
"Sie erhalten diese Nachricht weil Sie einen neuen Link zu Ihrer Bestellung " "Sie erhalten diese Nachricht, weil Sie einen neuen Link zu Ihrer Bestellung "
"für\n" "für\n"
"{event} angefordert haben.\n" "{event} angefordert haben.\n"
"\n" "\n"
@@ -11672,8 +11738,8 @@ msgid ""
msgstr "" msgstr ""
"Hallo,\n" "Hallo,\n"
"\n" "\n"
"Sie erhalten diese Nachricht weil Sie einen neuen Link zu Ihren Bestellungen " "Sie erhalten diese Nachricht, weil Sie einen neuen Link zu Ihren "
"für\n" "Bestellungen für\n"
"{event} angefordert haben. Sie finden Ihre Bestellungen unter folgenden " "{event} angefordert haben. Sie finden Ihre Bestellungen unter folgenden "
"Links:\n" "Links:\n"
"\n" "\n"
@@ -14287,6 +14353,22 @@ msgstr ""
"Sie haben eingestellt, dass Wertgutscheine {} Jahre nach Ende des Jahres der " "Sie haben eingestellt, dass Wertgutscheine {} Jahre nach Ende des Jahres der "
"Ausstellung ablaufen." "Ausstellung ablaufen."
#: pretix/control/forms/event.py
msgid "Payment term"
msgstr "Zahlungsfrist"
#: pretix/control/forms/event.py
msgid "same as above"
msgstr "wie oben"
#: pretix/control/forms/event.py
msgid "different payment term in days"
msgstr "abweichendes Zahlungsziel in Tagen"
#: pretix/control/forms/event.py
msgid "different payment term in minutes"
msgstr "abweichendes Zahlungsziel in Minuten"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
msgstr "Preis inklusive Steuern" msgstr "Preis inklusive Steuern"
@@ -16850,8 +16932,8 @@ msgid ""
"%(max_redeemed)s times." "%(max_redeemed)s times."
msgstr "" msgstr ""
"Sie können die maximale Anzahl Einlösungen nicht auf %(max_usages)s " "Sie können die maximale Anzahl Einlösungen nicht auf %(max_usages)s "
"reduzieren, weil mindestens einer der gewählten Gutscheine bereits %" "reduzieren, weil mindestens einer der gewählten Gutscheine bereits "
"(max_redeemed)s mal eingelöst wurde." "%(max_redeemed)s mal eingelöst wurde."
#: pretix/control/forms/vouchers.py #: pretix/control/forms/vouchers.py
msgid "" msgid ""
@@ -21204,6 +21286,7 @@ msgid "Top recommendation"
msgstr "Empfohlene Erweiterung" msgstr "Empfohlene Erweiterung"
#: pretix/control/templates/pretixcontrol/event/plugins.html #: pretix/control/templates/pretixcontrol/event/plugins.html
#: pretix/control/templates/pretixcontrol/items/questions.html
#: pretix/control/templates/pretixcontrol/organizers/plugins.html #: pretix/control/templates/pretixcontrol/organizers/plugins.html
msgid "Experimental feature" msgid "Experimental feature"
msgstr "Experimentelle Funktion" msgstr "Experimentelle Funktion"
@@ -23115,6 +23198,26 @@ msgstr ""
"auszufüllen. Wenn Ihre Veranstaltung Verpflegung beinhaltet, könnten Sie " "auszufüllen. Wenn Ihre Veranstaltung Verpflegung beinhaltet, könnten Sie "
"z.B. nach Allergien Ihrer Teilnehmer fragen." "z.B. nach Allergien Ihrer Teilnehmer fragen."
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new per-ticket question"
msgstr "Neue Frage für jedes Ticket erstellen"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new order-level question"
msgstr "Neue Frage für jede Bestellung erstellen"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-ticket questions"
msgstr "Fragen pro Ticket"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
"Diese Fragen werden für jedes Ticket abgefragt, also ggf. mehrfach pro "
"Bestellung."
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
msgstr "Neue Frage erstellen" msgstr "Neue Frage erstellen"
@@ -23131,6 +23234,30 @@ msgstr "Beim Check-in fragen"
msgid "All personalized products" msgid "All personalized products"
msgstr "Alle personalisierten Produkte" msgstr "Alle personalisierten Produkte"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Per-order questions"
msgstr "Fragen pro Bestellung"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr ""
"Diese Funktion befindet sich in aktiver Entwicklung und wird sich über die "
"nächsten Monate stark verändern."
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr ""
"Fragen pro Bestellung werden derzeit von pretixPOS weder für Anzeige noch "
"Abfrage unterstützt."
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "These questions are asked once per order."
msgstr "Diese Fragen werden einmal pro Bestellung gestellt."
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
#, python-format #, python-format
@@ -23735,6 +23862,13 @@ msgstr "Rechnungsinformationen"
msgid "(optional)" msgid "(optional)"
msgstr "(optional)" msgstr "(optional)"
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
msgid "Additional order information"
msgstr "Zusätzliche Bestellinformationen"
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
msgstr "Bestellung löschen" msgstr "Bestellung löschen"
@@ -23764,6 +23898,25 @@ msgstr "Ja, Bestellung ablehnen"
msgid "Extend payment term" msgid "Extend payment term"
msgstr "Zahlungsfrist verändern" msgstr "Zahlungsfrist verändern"
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
"Diese Datei wurde von einem Nutzer hochgeladen und kann Viren oder anderen "
"schädlichen Inhalt enthalten."
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
msgid "UNSAFE"
msgstr "UNSICHER"
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "not answered"
msgstr "nicht beantwortet"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
@@ -24048,27 +24201,10 @@ msgstr "Gültig %(datetime_range)s"
msgid "Ticket page" msgid "Ticket page"
msgstr "Ticket-Seite" msgstr "Ticket-Seite"
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "not answered"
msgstr "nicht beantwortet"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
msgid "This question will be asked during check-in." msgid "This question will be asked during check-in."
msgstr "Diese Frage wird beim Check-In gestellt." msgstr "Diese Frage wird beim Check-In gestellt."
#: pretix/control/templates/pretixcontrol/order/index.html
msgid ""
"This file has been uploaded by a user and could contain viruses or other "
"malicious content."
msgstr ""
"Diese Datei wurde von einem Nutzer hochgeladen und kann Viren oder anderen "
"schädlichen Inhalt enthalten."
#: pretix/control/templates/pretixcontrol/order/index.html
msgid "UNSAFE"
msgstr "UNSICHER"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/overview.html #: pretix/control/templates/pretixcontrol/orders/overview.html
#: pretix/plugins/reports/accountingreport.py #: pretix/plugins/reports/accountingreport.py
@@ -25440,6 +25576,15 @@ msgstr "Neues Gerät verbinden"
msgid "Hardware model" msgid "Hardware model"
msgstr "Hardware-Modell" msgstr "Hardware-Modell"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, python-format
msgid "Last seen: %(time)s"
msgstr "Zuletzt gesehen: %(time)s Uhr"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr "Kein kürzlicher Kontakt"
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
msgstr "Noch nicht installiert" msgstr "Noch nicht installiert"
@@ -28735,10 +28880,6 @@ msgstr "Die Reihenfolge der Kategorien wurde gespeichert."
msgid "Not all objects have been selected." msgid "Not all objects have been selected."
msgstr "Es wurden nicht alle Objekte ausgewählt." msgstr "Es wurden nicht alle Objekte ausgewählt."
#: pretix/control/views/item.py
msgid "Street"
msgstr "Straße"
#: pretix/control/views/item.py #: pretix/control/views/item.py
msgid "The requested question does not exist." msgid "The requested question does not exist."
msgstr "Die ausgewählte Frage existiert nicht." msgstr "Die ausgewählte Frage existiert nicht."
@@ -28913,11 +29054,11 @@ msgstr ""
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "" msgid ""
"We did not find DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
"Wir haben keinen DMARC-Eintrag für die Domain gefunden Es gibt daher eine " "Wir haben keinen DMARC-Eintrag für die Domain gefunden. Es gibt daher eine "
"hohe Chance, dass die E-Mails abgelehnt werden oder im Spam landen. Sie " "hohe Chance, dass die E-Mails abgelehnt werden oder im Spam landen. Sie "
"sollten die DNS-Einstellungen Ihrer Domain anpassen." "sollten die DNS-Einstellungen Ihrer Domain anpassen."
@@ -29377,7 +29518,8 @@ msgstr "Betreff: {subject}"
msgid "Your message has been queued and will be sent to {}." msgid "Your message has been queued and will be sent to {}."
msgstr "Die Nachricht wurde gespeichert und wird an {} versendet." msgstr "Die Nachricht wurde gespeichert und wird an {} versendet."
#: pretix/control/views/orders.py pretix/presale/views/order.py #: pretix/control/views/orders.py pretix/presale/views/cart.py
#: pretix/presale/views/order.py
msgid "" msgid ""
"This link is no longer valid. Please go back, refresh the page, and try " "This link is no longer valid. Please go back, refresh the page, and try "
"again." "again."
@@ -31658,6 +31800,37 @@ msgstr ""
"Limit) in mehreren Raten oder binnen 30 Tagen zu bezahlen. Sie, als der " "Limit) in mehreren Raten oder binnen 30 Tagen zu bezahlen. Sie, als der "
"Händler, erhalten Ihr Geld jedoch sofort." "Händler, erhalten Ihr Geld jedoch sofort."
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr "Erlaube weitere Zahlungsversuche während PayPal eine Zahlung überprüft"
#: pretix/plugins/paypal2/payment.py
msgid ""
"PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during "
"this time window. You can allow your customers to start another payment "
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
"PayPals Betrugserkennung blockiert die Verarbeitung von Zahlungen in "
"Einzelfällen für signifikante Zeiträume. Die Zahlung wird in diesem Zeitraum "
"als \"ausstehend\" angezeigt. Sie können Ihren Kunden erlauben, in diesem "
"Zeitfenster weitere Zahlungsversuche zu starten. Dies kann dazu führen, dass "
"mehrere Zahlungen stattfinden, wenn der ursprüngliche Versuch freigegeben "
"wird."
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr "Zeitlimit für weitere Zahlungsversuche"
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr ""
"Zeit in Minuten nach denen ein weitere Zahlungsversuch möglich ist während "
"der vorherige noch untersucht wird."
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
msgstr "-- Automatisch --" msgstr "-- Automatisch --"
@@ -31928,11 +32101,18 @@ msgstr "Bitte wählen Sie die gewünschte Zahlungsmethode aus."
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html #: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
msgid "" msgid ""
"Our attempt to execute your payment via PayPal has failed. Please try again " "Your payment is being processed by PayPal. This takes longer than usual. You "
"or contact us." "can wait until PayPal acknowledges the payment or you can try paying again "
"with this or another payment method. This might result in you being charged "
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr "" msgstr ""
"Unser Versuch, die Zahlung mit PayPal auszuführen, ist gescheitert. Bitte " "Ihre Zahlung wird von PayPal verarbeitet. Dies braucht länger als üblich. "
"probieren Sie es erneut oder kontaktieren Sie uns." "Sie können warten, bis PayPal die Zahlung bestätigt oder Sie können die "
"Zahlung erneut mit dieser oder einer anderen Zahlungsmethode versuchen. Dies "
"kann dazu führen, dass Ihnen der Betrag doppelt abgebucht wird, wenn PayPal "
"den ursprünglichen Versuch bestätigt. Bitte kontaktieren Sie uns in diesem "
"Fall."
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
msgid "" msgid ""
@@ -34989,10 +35169,6 @@ msgstr[1] "Das Ticket wurde %(count)s-mal eingelöst."
msgid "No attendee name provided" msgid "No attendee name provided"
msgstr "Name der teilnehmenden Person nicht angegeben" msgstr "Name der teilnehmenden Person nicht angegeben"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The image you previously uploaded"
msgstr "Das zuvor hochgeladene Bild"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html #: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "The price of this product was reduced because of an automatic discount." msgid "The price of this product was reduced because of an automatic discount."
msgstr "" msgstr ""
@@ -35434,6 +35610,10 @@ msgstr "%(value)s netto"
msgid "Set price in %(currency)s for %(item)s" msgid "Set price in %(currency)s for %(item)s"
msgstr "Preis in %(currency)s für %(item)s setzen" msgstr "Preis in %(currency)s für %(item)s setzen"
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
msgid "The image you previously uploaded"
msgstr "Das zuvor hochgeladene Bild"
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
@@ -37237,6 +37417,21 @@ msgstr "Schreibzugriff"
msgid "Kosovo" msgid "Kosovo"
msgstr "Kosovo" msgstr "Kosovo"
#, python-brace-format
#~ msgid "A relative date cannot be expressed as 'before' for '{}'"
#~ msgstr "Ein relatives Datum kann nicht als \"vor {}\" ausgedrückt werden"
#, python-brace-format
#~ msgid "A relative date cannot be expressed as 'after' for '{}'"
#~ msgstr "Ein relatives Datum kann nicht als \"nach {}\" ausgedrückt werden"
#~ msgid ""
#~ "Our attempt to execute your payment via PayPal has failed. Please try "
#~ "again or contact us."
#~ msgstr ""
#~ "Unser Versuch, die Zahlung mit PayPal auszuführen, ist gescheitert. Bitte "
#~ "probieren Sie es erneut oder kontaktieren Sie uns."
#~ msgid "All actions" #~ msgid "All actions"
#~ msgstr "Alle Aktionen" #~ msgstr "Alle Aktionen"
@@ -37386,9 +37581,6 @@ msgstr "Kosovo"
#~ msgid "Please do not use special characters in names." #~ msgid "Please do not use special characters in names."
#~ msgstr "Bitte verwenden Sie keine Sonderzeichen in Namen." #~ msgstr "Bitte verwenden Sie keine Sonderzeichen in Namen."
#~ msgid "State"
#~ msgstr "Zustand"
#~ msgid "Program times: date and time" #~ msgid "Program times: date and time"
#~ msgstr "Programmzeiten: Datum und Uhrzeit" #~ msgstr "Programmzeiten: Datum und Uhrzeit"
@@ -38116,9 +38308,6 @@ msgstr "Kosovo"
#~ msgid "Badge design" #~ msgid "Badge design"
#~ msgstr "Badge-Design" #~ msgstr "Badge-Design"
#~ msgid "(Same as above)"
#~ msgstr "(wie oben)"
#~ msgid "Translation in development" #~ msgid "Translation in development"
#~ msgstr "Übersetzung in Arbeit" #~ msgstr "Übersetzung in Arbeit"
@@ -38817,10 +39006,6 @@ msgstr "Kosovo"
#~ msgstr "" #~ msgstr ""
#~ "Wir haben Ihnen in den letzten 24 Stunden bereits eine E-Mail geschickt." #~ "Wir haben Ihnen in den letzten 24 Stunden bereits eine E-Mail geschickt."
#~ msgctxt "attendee_data"
#~ msgid "Asked"
#~ msgstr "Abfragen"
#~ msgid "Underpaid" #~ msgid "Underpaid"
#~ msgstr "Unterzahlt" #~ msgstr "Unterzahlt"
+2
View File
@@ -293,6 +293,7 @@ Pago
parsen parsen
Pay Pay
PayPal PayPal
PayPals
PayPal-Account PayPal-Account
PayPal-Accounts PayPal-Accounts
PayPal-Konto PayPal-Konto
@@ -340,6 +341,7 @@ pretix-Kontos
pretix-Logo pretix-Logo
pretix-Logos pretix-Logos
pretix-Plugins pretix-Plugins
pretixPOS
pretixPRINT pretixPRINT
pretixPRINT-Version pretixPRINT-Version
pretixSCAN pretixSCAN

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