Compare commits

...

161 Commits

Author SHA1 Message Date
Raphael Michel
df3fc1bef1 Remove fail-fast 2024-10-25 13:19:48 +02:00
Raphael Michel
022cd8dff7 Try n=3 but no reruns 2024-10-25 12:54:38 +02:00
Raphael Michel
e9c3af9016 Stable order of event lists 2024-10-25 12:54:38 +02:00
Raphael Michel
6744371417 Fix tz bug 2024-10-25 12:54:38 +02:00
Raphael Michel
b4005b7fe5 Add debug modes for test running 2024-10-25 12:54:38 +02:00
Raphael Michel
ff22b893f1 Experimental fixture 2024-10-25 12:54:38 +02:00
Raphael Michel
e37aba4f80 Remove mention of travis CI, it is long dead 2024-10-25 12:54:38 +02:00
Raphael Michel
f9ae2fd62a Use uv for all jobs 2024-10-25 12:54:38 +02:00
Raphael Michel
621c84951f Add -y to all apt lines 2024-10-25 12:54:38 +02:00
Raphael Michel
4d9f03dd2f use uv pip --system 2024-10-25 12:54:38 +02:00
Raphael Michel
9ac9a5aeef Speed up process with uv 2024-10-25 12:54:38 +02:00
Raphael Michel
d5714e12f8 Allow running with "act" 2024-10-25 12:54:38 +02:00
Raphael Michel
4f2cae5880 Map PostgreSQL port 2024-10-25 12:54:38 +02:00
Raphael Michel
c9913a0153 GH Actions: Fix PostgreSQL issues 2024-10-25 12:54:38 +02:00
Raphael Michel
39c3aef7bc Fix fakeredis usage 2024-10-25 12:54:25 +02:00
dependabot[bot]
cf3087453c Update fakeredis requirement from ==2.24.* to ==2.26.* (#4551)
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.24.0...v2.26.0)

---
updated-dependencies:
- dependency-name: fakeredis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 12:28:22 +02:00
Raphael Michel
7a870ee521 Fix test that does not work on postgresql 2024-10-25 00:02:52 +02:00
dependabot[bot]
3922290633 Update dnspython requirement from ==2.6.* to ==2.7.* (#4511)
Updates the requirements on [dnspython](https://github.com/rthalley/dnspython) to permit the latest version.
- [Release notes](https://github.com/rthalley/dnspython/releases)
- [Changelog](https://github.com/rthalley/dnspython/blob/main/doc/whatsnew.rst)
- [Commits](https://github.com/rthalley/dnspython/compare/v2.6.0rc1...v2.7.0)

---
updated-dependencies:
- dependency-name: dnspython
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 22:54:28 +02:00
dependabot[bot]
8aa13d7e3e Update sentry-sdk requirement from ==2.15.* to ==2.17.* (#4536)
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.15.0...2.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 22:53:58 +02:00
Raphael Michel
22e9a6eb92 Add warning to backend if cronjob is not running (#4550) 2024-10-24 22:50:34 +02:00
arjan-s
2b6f82502e Translations: Update Dutch
Currently translated at 85.6% (4936 of 5762 strings)

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

powered by weblate
2024-10-24 13:06:26 +02:00
Mira
a10bf2a939 Include "Cross-selling condition products" option when cloning category (#4534)
* Include "Cross-selling condition products" option when cloning category

* Add test case

* Remove print
2024-10-23 17:12:03 +02:00
arjan-s
a80b7087d9 Translations: Update Dutch
Currently translated at 75.8% (176 of 232 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
arjan-s
4b143e98eb Translations: Update Dutch
Currently translated at 85.6% (4935 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
Yasunobu YesNo Kawaguchi
bdb8b597d0 Translations: Update Japanese
Currently translated at 3.4% (198 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
Ryo Tagami
b1c9f40bc8 Translations: Update Japanese
Currently translated at 3.4% (198 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
a3b6a008b5 Translations: Update Spanish
Currently translated at 99.5% (231 of 232 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
9ce05e5cb9 Translations: Update Spanish
Currently translated at 96.4% (5560 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
f306527981 Translations: Update Spanish
Currently translated at 95.1% (5485 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
3e17ff9faa Translations: Update Spanish
Currently translated at 91.7% (5288 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
2a16cd4655 Translations: Update Spanish
Currently translated at 90.7% (5228 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
Ryo Tagami
d1078da5bf Translations: Update Japanese
Currently translated at 3.3% (195 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
483e7bc4ad Translations: Update Spanish
Currently translated at 89.8% (5175 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
Mira
401218b0a3 API: Allow filtering subevents by is_public (#4544) 2024-10-23 16:42:22 +02:00
Raphael Michel
19175258fd API: Fix event cloning, limit_sales_channels should never be set when all_sales_channels is set (Z#23169537) 2024-10-23 16:30:00 +02:00
Mira
22c36b89da Show proper error message in case of add-ons that are required, sold-out and hidden (#4545) 2024-10-21 16:46:32 +02:00
Mira
2697ed0c5d QuestionView: Carry over subevent filter to order list (#4543) 2024-10-21 14:45:04 +02:00
pretix translation bot
f81d820a02 Update translations (#4537)
Co-authored-by: Patrick Chilton <chpatrick@gmail.com>
Co-authored-by: Lovro <lovrogrilc@gmail.com>
Co-authored-by: CVZ-es <damien.bremont@casadevelazquez.org>
Co-authored-by: Ryo Tagami <rtagami@airstrip.jp>
Co-authored-by: Ryo <saremba@rami.io>
2024-10-21 13:28:34 +02:00
Mira
f8df66e621 Redirect to next step if AddOnsStep would show up empty (Z#23167007) (#4530) 2024-10-21 13:27:43 +02:00
Mira
2d9bfc80dc Fix "Your order qualifies for a discount" notice for discounted variation products (#4541) 2024-10-21 13:26:20 +02:00
Martin Gross
17b2e95569 Questions: Add SubEvent-Filter; Renew Filter-Form Layout (Z#23168996) (#4538) 2024-10-21 13:25:18 +02:00
Martin Gross
e49f938eb3 BulkVouchers: Append mail-recipient to comment instead of overwriting it (Z#23168852) 2024-10-16 10:37:06 +02:00
Martin Gross
8d63906341 Remove nl_BE from setup.cfg 2024-10-15 16:22:50 +02:00
Martin Gross
cfefa1aad0 Remove be_NL again, again, again 2024-10-15 16:17:20 +02:00
Martin Gross
1d16049dc5 add "Cross" and "Selling" (as in: "Cross-Selling") to de/de_Informal wordlist 2024-10-15 14:48:25 +02:00
Martin Gross
8452899edd add "Cross-Selling" to de/de_Informal wordlist 2024-10-15 14:43:31 +02:00
pretix translation bot
d67ebc0f80 Update translations (#4529) 2024-10-15 14:03:10 +02:00
Mira Weller
0e87f03e1e Update po files
[CI skip]

Signed-off-by: Mira Weller <weller@rami.io>
2024-10-15 13:14:42 +02:00
Mira Weller
868408ea55 Fix typo 2024-10-15 13:13:43 +02:00
Mira Weller
fc75cd35f8 Update po files
[CI skip]

Signed-off-by: Mira Weller <weller@rami.io>
2024-10-15 13:06:53 +02:00
Mira
a3e2540331 Display "Normal category" instead of "None" in category list (#4526) 2024-10-15 12:14:15 +02:00
Raphael Michel
99ce7effde Add ticket renderer RPC API (Z#23165429) (#4525)
---------

Co-authored-by: Mira Weller <weller@rami.io>
2024-10-15 12:11:09 +02:00
Raphael Michel
0d645fc4c5 Hotfix broken query away 2024-10-14 18:17:30 +02:00
Mira
359df1f51e Cross selling (#4185)
Product categories can now be marked as "cross-selling categories", causing them to 
appear in the add-on checkout step as additional recommendations, depending on 
their cross-selling visibility (always, only if certain products are already in the cart, or 
only if they qualify for a discount according to discount rules).

---------

Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-10-14 14:39:49 +02:00
Raphael Michel
7607cc5d2f Fix indentation 2024-10-14 09:26:56 +02:00
Raphael Michel
40c8d014df Waiting list: Redirect to shop if no products can be awaited (Z#23168172) (#4517)
* Waiting list: Redirect to shop if no products can be awaited (Z#23168172)

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

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

---------

Co-authored-by: Mira <weller@rami.io>
2024-10-14 09:26:06 +02:00
Mira
c10efc692d Let organizers specify a helptext for the custom invoice field (Z#23167497) (#4520) 2024-10-14 09:22:58 +02:00
Raphael Michel
8f0a277c7b Fix tax rule calculation of negative amounts (PRETIXEU-ANN) 2024-10-11 15:28:07 +02:00
George Hickman
9dc38e42d8 Add device_changed signal (#4412)
This provides both the original and updated version of the Device so
subscribers can see the changes.
2024-10-11 11:08:23 +02:00
Raphael Michel
bfd88d1496 Docs: Fix wrong field name in example 2024-10-10 13:51:33 +02:00
Patrick Chilton
be6bd501bd Translations: Update Hungarian
Currently translated at 10.5% (608 of 5745 strings)

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

powered by weblate
2024-10-10 09:16:51 +02:00
Raphael Michel
d160c9fd67 Fix crash in checkin list action view (PRETIXEU-AN8) 2024-10-09 17:11:10 +02:00
Raphael Michel
221f14cc21 API: Fix crash PRETIXEU-AN5 2024-10-09 12:25:36 +02:00
Felix Schäfer
1dda2eb4fb Fix reauth loops with redirect style authentication plugins (#4512)
* Test reauth with redirect style auth #4498

* Fix reauth loops with redirect style auth #4498
2024-10-09 09:24:49 +02:00
Davide Manzella
30f2e99020 Translations: Update Italian
Currently translated at 22.2% (1279 of 5745 strings)

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

powered by weblate
2024-10-09 09:16:52 +02:00
Raphael Michel
8efe276ed0 Fix negative prices in bundles when tax rate is 0 (#4513) 2024-10-09 08:16:01 +02:00
Raphael Michel
61b25acdd2 Fix email confirm hash in templates 2024-10-07 17:54:40 +02:00
Raphael Michel
6cc9529d9a Authentication: Support for fallback secret keys in get_session_auth_hash (#4481)
* Authentication: Support for fallback secret keys in get_session_auth_hash

* Update src/pretix/presale/utils.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-10-07 16:58:37 +02:00
Raphael Michel
cdc5401dc2 Allow to set fallback secret keys (#4482) 2024-10-07 16:31:24 +02:00
Raphael Michel
1334a570e4 Generate email confirmation secret from tagged_secret (#4480) 2024-10-07 13:58:08 +02:00
Raphael Michel
7a66aea2cb Voucher update: Allow to remove seat 2024-10-07 11:42:28 +02:00
dependabot[bot]
ee77a5e447 Bump @rollup/plugin-node-resolve from 15.2.3 to 15.3.0 in /src/pretix/static/npm_dir (#4499)
Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve) from 15.2.3 to 15.3.0.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/node-resolve-v15.3.0/packages/node-resolve)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-node-resolve"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 11:36:23 +02:00
Raphael Michel
827e127568 Fix #4365 -- Missing date in timeline 2024-10-04 16:17:52 +02:00
dependabot[bot]
ce0e0d7fd1 Update qrcode requirement from ==7.4.* to ==8.0 (#4500)
Updates the requirements on [qrcode](https://github.com/lincolnloop/python-qrcode) to permit the latest version.
- [Changelog](https://github.com/lincolnloop/python-qrcode/blob/main/CHANGES.rst)
- [Commits](https://github.com/lincolnloop/python-qrcode/compare/v7.4...v8.0)

---
updated-dependencies:
- dependency-name: qrcode
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-04 16:03:04 +02:00
dependabot[bot]
152a956dc5 Update sentry-sdk requirement from ==2.14.* to ==2.15.*
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.14.0...2.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-04 11:53:13 +02:00
Patrick Chilton
68e2c355e6 Translations: Update Hungarian
Currently translated at 10.3% (594 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
171615558f Translations: Update Hungarian
Currently translated at 8.8% (506 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
a1765910ea Translations: Update Hungarian
Currently translated at 45.2% (105 of 232 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
417277958b Translations: Update Hungarian
Currently translated at 8.7% (504 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
0d50494e89 Translations: Update Hungarian
Currently translated at 43.9% (102 of 232 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
c6f634ce72 Translations: Update Hungarian
Currently translated at 7.9% (456 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
adc78c14ab Translations: Update Hungarian
Currently translated at 2.9% (170 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
dependabot[bot]
b4ca2bdbb4 Update pycryptodome requirement from ==3.20.* to ==3.21.* (#4504)
Updates the requirements on [pycryptodome](https://github.com/Legrandin/pycryptodome) to permit the latest version.
- [Release notes](https://github.com/Legrandin/pycryptodome/releases)
- [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst)
- [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.20.0x...v3.21.0)

---
updated-dependencies:
- dependency-name: pycryptodome
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-04 11:48:04 +02:00
dependabot[bot]
9a7ff592af Update redis requirement from ==5.0.* to ==5.1.* (#4491)
Updates the requirements on [redis](https://github.com/redis/redis-py) to permit the latest version.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v5.0.0b1...v5.1.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 16:30:49 +02:00
Rosariocastellana
548b54cca6 Translations: Update Italian
Currently translated at 20.6% (1188 of 5745 strings)

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

powered by weblate
2024-09-30 13:51:23 +02:00
Anarion Dunedain
e736791446 Translations: Update Polish
Currently translated at 100.0% (5745 of 5745 strings)

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

powered by weblate
2024-09-30 13:51:23 +02:00
Raphael Michel
7bd945b2e6 Waiting list: Add warning when sold out products are hidden 2024-09-30 13:24:43 +02:00
Raphael Michel
a07d5aaf05 Add-on step: Minimal performance improvement 2024-09-27 13:26:30 +02:00
Raphael Michel
0cf1a32902 Bump version to 2024.10.0.dev0 2024-09-27 09:46:47 +02:00
Raphael Michel
be6aae8577 Bump version to 2024.9.0 2024-09-27 09:46:18 +02:00
Mira
fe80f5fb78 Utils for internal plugin (#4483)
* Add full_code property to OrderPosition

* Add inline "json_script" as supported data source for select2

* Use shorter OrderPosition.code
2024-09-26 19:29:33 +02:00
Raphael Michel
a2c15ad89e Check-in: Prevent duplicate auto check-outs (Z#23167003) (#4488) 2024-09-26 17:54:27 +02:00
Raphael Michel
cab0f37830 Translations: Update German
Currently translated at 100.0% (5745 of 5745 strings)

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

powered by weblate
2024-09-26 17:51:57 +02:00
Raphael Michel
0423980058 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5745 of 5745 strings)

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

powered by weblate
2024-09-26 17:51:57 +02:00
Raphael Michel
63983b1b68 Translations: Update spellcheck lists 2024-09-26 16:52:28 +02:00
Raphael Michel
61241c2a1e Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-09-26 13:23:02 +02:00
Raphael Michel
4069c61054 runperiodic: Allow to list all tasks 2024-09-26 10:32:53 +02:00
Raphael Michel
9bf4fb2d0f Sendmail: Fix incorrect filtering of subevents when mailing to attendees (Z#23166274) (#4474) 2024-09-25 17:55:33 +02:00
Raphael Michel
ff910f293f Item list: Show tax rate even for products with 0 price
Might be neccessary for price input, variations, etc
2024-09-25 12:42:45 +02:00
Raphael Michel
74f7bec617 Change wording in a headline to avoid double-using a term 2024-09-25 12:42:45 +02:00
Albizuri
467a35e353 Translations: Update Basque
Currently translated at 56.0% (3213 of 5737 strings)

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

powered by weblate
2024-09-24 10:13:22 +02:00
dependabot[bot]
770c13a4f0 Update pypdf requirement from ==4.3.* to ==5.0.* (#4471)
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/4.3.0...5.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 11:34:23 +02:00
dependabot[bot]
5373d4d8ba Bump django-bootstrap3 from 24.2 to 24.3
Bumps [django-bootstrap3](https://github.com/zostera/django-bootstrap3) from 24.2 to 24.3.
- [Changelog](https://github.com/zostera/django-bootstrap3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zostera/django-bootstrap3/compare/v24.2...v24.3)

---
updated-dependencies:
- dependency-name: django-bootstrap3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-19 11:28:15 +02:00
Raphael Michel
42e673b5f6 Fix missing JS file name 2024-09-19 09:39:49 +02:00
Albizuri
7af2f2a87b Translations: Update Basque
Currently translated at 56.0% (3213 of 5737 strings)

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

powered by weblate
2024-09-18 16:25:32 +02:00
Tinna Sandström
e408521769 Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-18 16:25:32 +02:00
Raphael Michel
8ed0d36346 Fix another wrong waiting_list_active reference 2024-09-18 10:28:41 +02:00
Raphael Michel
14cbe99667 Stripe: Fix a leftover } 2024-09-17 17:54:57 +02:00
Raphael Michel
b059995eff Widget: Fix waiting list disable time based on subevent 2024-09-17 17:39:22 +02:00
Raphael Michel
100e8d0a4b Fix #4454 -- Resolve deprecation warning from celery 2024-09-17 13:36:35 +02:00
Raphael Michel
eb92e4d8e6 Render progress info on non-javascript fallback page for celery tasks (#4452)
* Render progress info on non-javascript fallback page for celery tasks

* Review notes
2024-09-17 13:29:27 +02:00
Raphael Michel
32d6ded003 Stricten password validation to match PCI DSS requirements (#4467)
* Stricten password validation to match PCI DSS requirements

* Review fix

* Fix a file header
2024-09-17 13:29:17 +02:00
Raphael Michel
aa07533693 Order import: Allow to set check-in attention and text (Z#23165806) (#4469) 2024-09-17 11:50:19 +02:00
dependabot[bot]
e7d01f91a6 Bump @babel/preset-env from 7.25.3 to 7.25.4 in /src/pretix/static/npm_dir (#4438)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.25.3 to 7.25.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.4/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-17 11:42:33 +02:00
Raphael Michel
9616369f07 Formally deprecate eu_reverse_charge (#4470) 2024-09-17 11:39:32 +02:00
dependabot[bot]
af606090ba Update sentry-sdk requirement from ==2.13.* to ==2.14.* (#4455)
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.13.0...2.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 16:46:28 +02:00
Svyatoslav
931f3eca1b Translations: Update Estonian
Currently translated at 0.1% (1 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Svyatoslav
36f306120e Translations: Update Latvian
Currently translated at 37.2% (2134 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Svyatoslav
a3ba0c97e9 Translations: Update Russian
Currently translated at 19.1% (1097 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Richard Schreiber
484d24b66c Translations: Add Estonian 2024-09-16 16:44:46 +02:00
Tinna Sandström
2d39d3cc8e Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Kristian Feldsam
78b1adf423 Translations: Update Slovak
Currently translated at 29.3% (68 of 232 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Kristian Feldsam
c3eedcc396 Translations: Update Slovak
Currently translated at 92.5% (5307 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Tinna Sandström
682c328390 Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Wikinaut
5230827f5e Translations: Update German
Currently translated at 100.0% (5736 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Arthur Nunes
dad9915435 Translations: Update Portuguese (Brazil)
Currently translated at 13.2% (759 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Wikinaut
a9d2c1eb34 Translations: Update German
Currently translated at 100.0% (5736 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Tinna Sandström
66fe45a478 Translations: Update Swedish
Currently translated at 86.2% (200 of 232 strings)

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

powered by weblate
2024-09-12 09:32:41 +02:00
Raphael Michel
24e2b1b9ab Import: Parse iso dates 2024-09-12 09:32:25 +02:00
Martin Gross
eebdce80cd OIDC: Respect token_endpoint_auth_methods_supported (Z#2164777) (#4459) 2024-09-11 16:40:42 +02:00
Kristian Feldsam
09af95ec20 Translations: Update Slovak
Currently translated at 92.1% (5283 of 5736 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Tinna Sandström
1ade674beb Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Kristian Feldsam
76ff59f9c2 Translations: Update Slovak
Currently translated at 6.4% (15 of 232 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Kristian Feldsam
0986522c2f Translations: Update Slovak
Currently translated at 6.0% (14 of 232 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Kristian Feldsam
91f4e731da Translations: Update Slovak
Currently translated at 92.0% (5280 of 5736 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Raphael Michel
98709286c6 Order cancellation: Fix crash on deleted order (PRETIXEU-AHP) 2024-09-10 14:00:11 +02:00
Raphael Michel
667c2555b2 AsyncMixin: Fix crash on invalid query string (PRETIXEU-AHG) 2024-09-10 14:00:08 +02:00
Raphael Michel
6f5acb1ca7 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Raphael Michel
65ec3e3fd6 Translations: Update German
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Richard Schreiber
1a8d0a973d Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5736 of 5736 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Richard Schreiber
3c94631405 Translations: Update German
Currently translated at 100.0% (5736 of 5736 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Kristian Feldsam
1dda7732a5 Translations: Update Slovak
Currently translated at 4.7% (11 of 232 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Kristian Feldsam
33accf5f99 Translations: Update Slovak
Currently translated at 92.0% (5279 of 5736 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Tinna Sandström
be2efd9df2 Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Ahmad AlHarthi
fe69137a4e Translations: Update Arabic
Currently translated at 65.1% (3736 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ayden Jahola
7ccfb3a27a Translations: Update Arabic
Currently translated at 65.1% (3736 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ayden Jahola
b7205622dc Translations: Update Arabic
Currently translated at 65.1% (3736 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ahmad AlHarthi
44da5b81b1 Translations: Update Arabic
Currently translated at 65.1% (3736 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Martin Gross
5a058342a6 Translations: Update Portuguese (Portugal)
Currently translated at 56.8% (132 of 232 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Martin Gross
2d15dc7ce5 Translations: Update Portuguese (Portugal)
Currently translated at 56.8% (132 of 232 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Martin Gross
dd4ccc864e Translations: Update Portuguese (Portugal)
Currently translated at 86.7% (4977 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Kristian Feldsam
b812f0affe Translations: Update Slovak
Currently translated at 3.8% (9 of 232 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Kristian Feldsam
2af4183ce6 Translations: Update Slovak
Currently translated at 92.0% (5279 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Albizuri
8ac0b93ca5 Translations: Update Basque
Currently translated at 55.9% (3209 of 5737 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ronisson Cabral
51a1193f32 Translations: Update Portuguese (Portugal)
Currently translated at 56.0% (130 of 232 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ronisson Cabral
002da2c9b7 Translations: Update Portuguese (Portugal)
Currently translated at 86.2% (4948 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Martin Gross
9a2ebe4e95 Refunds: Fix sortkey to work with not provided BICs (Z#23165187) (#4451) 2024-09-09 10:48:53 +02:00
Albizuri
bc6da2512a Translations: Update Basque
Currently translated at 48.4% (113 of 233 strings)

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

powered by weblate
2024-09-06 10:51:38 +02:00
Albizuri
6378dc69b8 Translations: Update Basque
Currently translated at 56.3% (3231 of 5737 strings)

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

powered by weblate
2024-09-06 10:51:38 +02:00
Richard Schreiber
2b53d04a19 Improve labels in mail settings for incomplete payments (#4444) 2024-09-06 08:53:08 +02:00
Richard Schreiber
7efe7b5ff7 Docs: fix typo for SSO provider instead of client 2024-09-05 13:02:20 +02:00
213 changed files with 117321 additions and 76238 deletions

View File

@@ -38,7 +38,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext unzip
run: sudo apt update && sudo apt install -y gettext unzip
- name: Install Python dependencies
run: pip3 install -U setuptools build pip check-manifest
- name: Run check-manifest

View File

@@ -37,7 +37,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt update && sudo apt install enchant-2 hunspell aspell-en
run: sudo apt update && sudo apt install -y enchant-2 hunspell aspell-en
- name: Install Dependencies
run: pip3 install -Ur requirements.txt
working-directory: ./doc

View File

@@ -35,9 +35,9 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt update && sudo apt install gettext
run: sudo apt update && sudo apt install -y gettext
- name: Install Dependencies
run: pip3 install -e ".[dev]"
run: pip3 install uv && uv pip install --system -e ".[dev]"
- name: Compile messages
run: python manage.py compilemessages
working-directory: ./src
@@ -60,9 +60,9 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
run: sudo apt update && sudo apt install -y enchant-2 hunspell hunspell-de-de aspell-en aspell-de
- name: Install Dependencies
run: pip3 install -e ".[dev]"
run: pip3 install uv && uv pip install --system -e ".[dev]"
- name: Spellcheck translations
run: potypo
working-directory: ./src

View File

@@ -35,7 +35,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" psycopg2-binary
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
- name: Run isort
run: isort -c .
working-directory: ./src
@@ -55,7 +55,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" psycopg2-binary
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
- name: Run flake8
run: flake8 .
working-directory: ./src

View File

@@ -22,6 +22,7 @@ jobs:
runs-on: ubuntu-22.04
name: Tests
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11"]
database: [sqlite, postgres]
@@ -30,15 +31,21 @@ jobs:
python-version: "3.9"
- database: sqlite
python-version: "3.10"
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: pretix
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '15'
postgresql db: 'pretix'
postgresql user: 'postgres'
postgresql password: 'postgres'
if: matrix.database == 'postgres'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
@@ -50,9 +57,9 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext
run: sudo apt update && sudo apt install -y gettext
- name: Install Python dependencies
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
- name: Run checks
run: python manage.py check
working-directory: ./src
@@ -64,10 +71,10 @@ jobs:
run: make all compress
- name: Run tests
working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 --stepwise -vv -p no:sugar --cov=./ --cov-report=xml --reruns 0 tests
- name: Run concurrency tests
working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
if: matrix.database == 'postgres'
- name: Upload coverage
uses: codecov/codecov-action@v1

View File

@@ -10,7 +10,7 @@ tests:
- cd src
- python manage.py check
- make all compress
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
except:
- pypi
pypi:

View File

@@ -294,6 +294,10 @@ Example::
setting is not provided, pretix will generate a random secret on the first start
and will store it in the filesystem for later usage.
``secret_fallback0`` ... ``secret_fallback9``
Prior versions of the secret to be used by Django for signing and verification purposes that will still
be accepted but no longer be used for new signing.
``debug``
Whether or not to run in debug mode. Default is ``False``.

View File

@@ -23,6 +23,22 @@ position integer An integer, use
is_addon boolean If ``true``, items within this category are not on sale
on their own but the category provides a source for
defining add-ons for other products.
cross_selling_mode string If ``null``, cross-selling is disabled for this category.
If ``"only"``, it is only visible in the cross-selling
step.
If ``"both"``, it is visible on the normal index page
as well.
Only available if ``is_addon`` is ``false``.
cross_selling_condition string Only relevant if ``cross_selling_mode`` is not ``null``.
If ``"always"``, always show in cross-selling step.
If ``"products"``, only show if the cart contains one of
the products listed in ``cross_selling_match_products``.
If ``"discounts"``, only show products that qualify for
a discount according to discount rules.
cross_selling_match_products list of integer Only relevant if ``cross_selling_condition`` is
``"products"``. Internal ID of the items of which at
least one needs to be in the cart for this category to
be shown.
===================================== ========================== =======================================================
@@ -60,7 +76,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
]
}
@@ -102,7 +121,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -130,7 +152,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
**Example response**:
@@ -147,7 +172,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
:param organizer: The ``slug`` field of the organizer of the event to create a category for
@@ -193,7 +221,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": true
"is_addon": true,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -51,7 +51,7 @@ Endpoints
"results": [
{
"identifier": "web",
"name": {
"label": {
"en": "Online shop"
},
"type": "web",
@@ -88,7 +88,7 @@ Endpoints
{
"identifier": "web",
"name": {
"label": {
"en": "Online shop"
},
"type": "web",
@@ -116,7 +116,7 @@ Endpoints
{
"identifier": "api.custom",
"name": {
"label": {
"en": "Custom integration"
},
"type": "api",
@@ -133,7 +133,7 @@ Endpoints
{
"identifier": "api.custom",
"name": {
"label": {
"en": "Custom integration"
},
"type": "api",
@@ -178,7 +178,7 @@ Endpoints
{
"identifier": "web",
"name": {
"label": {
"en": "Online shop"
},
"type": "web",

View File

@@ -136,6 +136,7 @@ Endpoints
}
:query page: The page number in case of a multi-page result set, default is 1
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
@@ -467,6 +468,7 @@ Endpoints
}
:query page: The page number in case of a multi-page result set, default is 1
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.

View File

@@ -20,8 +20,9 @@ internal_name string An optional nam
rate decimal (string) Tax rate in percent
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
the specified product price
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will
be ignored if custom rules are set.
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
are applied. Will be ignored if custom rules are set.
Use custom rules instead.
home_country string Merchant country (required for reverse charge), can be
``null`` or empty string
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom

View File

@@ -14,7 +14,7 @@ Core
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display,
register_text_placeholders, register_mail_placeholders
register_text_placeholders, register_mail_placeholders, device_info_updated
Order events
""""""""""""

View File

@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
===================================== ========================== =======================================================
Endpoints
---------
Layout endpoints
----------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
@@ -268,5 +268,75 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
Ticket rendering endpoint
-----------------------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketpdfrenderer/render_batch/
With this API call, you can instruct the system to render a set of tickets into one combined PDF file. To specify
which tickets to render, you need to submit a list of "parts". For every part, the following fields are supported:
* ``orderposition`` (``integer``, required): The ID of the order position to render.
* ``override_channel`` (``string``, optional): The sales channel ID to be used for layout selection instead of the
original channel of the order.
* ``override_layout`` (``integer``, optional): The ticket layout ID to be used instead of the auto-selected one.
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
yield one of the following status codes:
* ``200 OK`` The export succeeded. The body will be your resulting file. Might be large!
* ``409 Conflict`` Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
* ``410 Gone`` Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
* ``404 Not Found`` The export does not exist / is expired.
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
.. note:: To avoid performance issues, a maximum number of 1000 parts is currently allowed.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/render_batch/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"parts": [
{
"orderposition": 55412
},
{
"orderposition": 55412,
"override_channel": "web"
},
{
"orderposition": 55412,
"override_layout": 56
}
]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 202: no error
:statuscode 400: Invalid input options
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json

View File

@@ -175,7 +175,7 @@ without any special behavior.
Connecting SSO providers (pretix as the SSO client)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To connect an external application as a SSO client, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
To connect an external application as a SSO provider, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
in your organizer account.
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png

View File

@@ -36,7 +36,7 @@ dependencies = [
"css-inline==0.14.*",
"defusedcsv>=1.1.0",
"Django[argon2]==4.2.*,>=4.2.15",
"django-bootstrap3==24.2",
"django-bootstrap3==24.3",
"django-compressor==4.5.1",
"django-countries==7.6.*",
"django-filter==24.3",
@@ -55,7 +55,7 @@ dependencies = [
"django-scopes==2.0.*",
"django-statici18n==2.5.*",
"djangorestframework==3.15.*",
"dnspython==2.6.*",
"dnspython==2.7.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
@@ -80,18 +80,18 @@ dependencies = [
"psycopg2-binary",
"pycountry",
"pycparser==2.22",
"pycryptodome==3.20.*",
"pypdf==4.3.*",
"pycryptodome==3.21.*",
"pypdf==5.0.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==7.4.*",
"redis==5.0.*",
"qrcode==8.0",
"redis==5.1.*",
"reportlab==4.2.*",
"requests==2.31.*",
"sentry-sdk==2.13.*",
"sentry-sdk==2.17.*",
"sepaxml==2.6.*",
"slimit",
"stripe==7.9.*",
@@ -111,7 +111,7 @@ dev = [
"aiohttp==3.10.*",
"coverage",
"coveralls",
"fakeredis==2.24.*",
"fakeredis==2.26.*",
"flake8==7.1.*",
"freezegun",
"isort==5.13.*",

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2024.9.0.dev0"
__version__ = "2024.10.0.dev0"

View File

@@ -88,16 +88,20 @@ class SalesChannelMigrationMixin:
}
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
raise ValidationError(
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the list of all sales channels."
)
raise ValidationError({
"limit_sales_channels": [
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the list of all sales channels."
]
})
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
raise ValidationError(
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the same list."
)
raise ValidationError({
"limit_sales_channels": [
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the same list."
]
})
if data["sales_channels"] == all_channels:
data["all_sales_channels"] = True
@@ -106,6 +110,10 @@ class SalesChannelMigrationMixin:
data["all_sales_channels"] = False
data["limit_sales_channels"] = data["sales_channels"]
del data["sales_channels"]
if data.get("all_sales_channels"):
data["limit_sales_channels"] = []
return super().to_internal_value(data)
def to_representation(self, value):

View File

@@ -772,6 +772,7 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_address_custom_field_helptext',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_show_payments',
@@ -916,6 +917,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_address_custom_field_helptext',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_address_from_name',

View File

@@ -369,7 +369,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
require_membership_types = validated_data.pop('require_membership_types', [])
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
item = Item.objects.create(**validated_data)
if limit_sales_channels:
if limit_sales_channels and not validated_data.get('all_sales_channels'):
item.limit_sales_channels.add(*limit_sales_channels)
if picture:
item.picture.save(os.path.basename(picture.name), picture)
@@ -441,7 +441,22 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
class Meta:
model = ItemCategory
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
fields = (
'id', 'name', 'internal_name', 'description', 'position',
'is_addon', 'cross_selling_mode',
'cross_selling_condition', 'cross_selling_match_products'
)
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)
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
return data
class QuestionOptionSerializer(I18nAwareModelSerializer):

View File

@@ -200,6 +200,11 @@ class UpdateView(APIView):
device.save()
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
from ...base.signals import device_info_updated
device_info_updated.send(
sender=Device, old_device=request.auth, new_device=device
)
serializer = DeviceSerializer(device)
return Response(serializer.data)

View File

@@ -297,7 +297,8 @@ class EventViewSet(viewsets.ModelViewSet):
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
if not new_event.all_sales_channels:
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
else:
serializer.instance.set_defaults()
@@ -370,7 +371,7 @@ with scopes_disabled():
class Meta:
model = SubEvent
fields = ['active', 'event__live']
fields = ['is_public', 'active', 'event__live']
def ends_after_qs(self, queryset, name, value):
expr = Q(

View File

@@ -32,13 +32,16 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import string
from collections import OrderedDict
from importlib import import_module
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.hashers import check_password, make_password
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _, ngettext
def get_auth_backends():
@@ -160,3 +163,62 @@ class NativeAuthBackend(BaseAuthBackend):
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
if u and u.auth_backend == self.identifier:
return u
class NumericAndAlphabeticPasswordValidator:
def validate(self, password, user=None):
has_numeric = any(c in string.digits for c in password)
has_alpha = any(c in string.ascii_letters for c in password)
if not has_numeric or not has_alpha:
raise ValidationError(
_(
"Your password must contain both numeric and alphabetic characters.",
),
code="password_numeric_and_alphabetic",
)
def get_help_text(self):
return _(
"Your password must contain both numeric and alphabetic characters.",
)
class HistoryPasswordValidator:
def __init__(self, history_length=4):
self.history_length = history_length
def validate(self, password, user=None):
from pretix.base.models import User
if not user or not user.pk or not isinstance(user, User):
return
for hp in user.historic_passwords.order_by("-created")[:self.history_length]:
if check_password(password, hp.password):
raise ValidationError(
ngettext(
"Your password may not be the same as your previous password.",
"Your password may not be the same as one of your %(history_length)s previous passwords.",
self.history_length,
),
code="password_history",
params={"history_length": self.history_length},
)
def get_help_text(self):
return ngettext(
"Your password may not be the same as your previous password.",
"Your password may not be the same as one of your %(history_length)s previous passwords.",
self.history_length,
) % {"history_length": self.history_length}
def password_changed(self, password, user=None):
if not user:
pass
user.historic_passwords.create(password=make_password(password))
user.historic_passwords.filter(
pk__in=user.historic_passwords.order_by("-created")[self.history_length:].values_list("pk", flat=True),
).delete()

View File

@@ -46,6 +46,8 @@ This module contains utilities for implementing OpenID Connect for customer auth
as well as an OpenID Provider (OP).
"""
pretix_token_endpoint_auth_methods = ['client_secret_basic', 'client_secret_post']
def _urljoin(base, path):
if not base.endswith("/"):
@@ -127,6 +129,16 @@ def oidc_validate_and_complete_config(config):
fields=", ".join(provider_config.get("claims_supported", []))
))
if "token_endpoint_auth_methods_supported" in provider_config:
token_endpoint_auth_methods_supported = provider_config.get("token_endpoint_auth_methods_supported",
["client_secret_basic"])
if not any(x in pretix_token_endpoint_auth_methods for x in token_endpoint_auth_methods_supported):
raise ValidationError(
_(f'No supported Token Endpoint Auth Methods supported: {token_endpoint_auth_methods_supported}').format(
token_endpoint_auth_methods_supported=", ".join(token_endpoint_auth_methods_supported)
)
)
config['provider_config'] = provider_config
return config
@@ -147,6 +159,18 @@ def oidc_authorize_url(provider, state, redirect_uri):
def oidc_validate_authorization(provider, code, redirect_uri):
endpoint = provider.configuration['provider_config']['token_endpoint']
# Wall of shame and RFC ignorant IDPs
if endpoint == 'https://www.linkedin.com/oauth/v2/accessToken':
token_endpoint_auth_method = 'client_secret_post'
else:
token_endpoint_auth_methods = provider.configuration['provider_config'].get(
'token_endpoint_auth_methods_supported', ['client_secret_basic']
)
token_endpoint_auth_method = [
x for x in pretix_token_endpoint_auth_methods if x in token_endpoint_auth_methods
][0]
params = {
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
# https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
@@ -154,6 +178,11 @@ def oidc_validate_authorization(provider, code, redirect_uri):
'code': code,
'redirect_uri': redirect_uri,
}
if token_endpoint_auth_method == 'client_secret_post':
params['client_id'] = provider.configuration['client_id']
params['client_secret'] = provider.configuration['client_secret']
try:
resp = requests.post(
endpoint,
@@ -161,7 +190,10 @@ def oidc_validate_authorization(provider, code, redirect_uri):
headers={
'Accept': 'application/json',
},
auth=(provider.configuration['client_id'], provider.configuration['client_secret']),
auth=(
provider.configuration['client_id'],
provider.configuration['client_secret']
) if token_endpoint_auth_method == 'client_secret_basic' else None,
)
resp.raise_for_status()
data = resp.json()

View File

@@ -1122,6 +1122,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if event.settings.invoice_address_custom_field:
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
self.fields['custom_field'].help_text = event.settings.invoice_address_custom_field_helptext
else:
del self.fields['custom_field']

View File

@@ -36,6 +36,7 @@ import time
import traceback
from django.conf import settings
from django.core.cache import cache
from django.core.management.base import BaseCommand
from django.dispatch.dispatcher import NO_RECEIVERS
@@ -50,17 +51,23 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
'(dotted path, comma separation)')
parser.add_argument('--list-tasks', action='store_true', help='Only list all tasks')
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
'(dotted path, comma separation)')
def handle(self, *args, **options):
verbosity = int(options['verbosity'])
cache.set("pretix_runperiodic_executed", True, 3600 * 12)
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
return
for receiver in periodic_task._live_receivers(self):
name = f'{receiver.__module__}.{receiver.__name__}'
if options['list_tasks']:
print(name)
continue
if options.get('tasks'):
if name not in options.get('tasks').split(','):
continue

View File

@@ -0,0 +1,36 @@
# Generated by Django 4.2.15 on 2024-09-16 15:10
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0269_order_api_meta"),
]
operations = [
migrations.CreateModel(
name="HistoricPassword",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("created", models.DateTimeField(auto_now_add=True)),
("password", models.CharField(max_length=128)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="historic_passwords",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 4.2.11 on 2024-05-27 13:19
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0270_historicpassword"),
]
operations = [
migrations.AddField(
model_name="itemcategory",
name="cross_selling_condition",
field=models.CharField(null=True, max_length=10),
),
migrations.AddField(
model_name="itemcategory",
name="cross_selling_mode",
field=models.CharField(null=True, max_length=5),
),
migrations.AddField(
model_name="itemcategory",
name="cross_selling_match_products",
field=models.ManyToManyField(
related_name="matched_by_cross_selling_categories", to="pretixbase.item"
),
),
]

View File

@@ -213,7 +213,13 @@ class DatetimeColumnMixin:
except (ValueError, TypeError):
pass
else:
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
try:
d = datetime.datetime.fromisoformat(value)
if not d.tzinfo:
d = d.replace(tzinfo=self.timezone)
return d
except (ValueError, TypeError):
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
class DecimalColumnMixin:

View File

@@ -40,8 +40,8 @@ from phonenumbers import SUPPORTED_REGIONS
from pretix.base.forms.questions import guess_country
from pretix.base.modelimport import (
DatetimeColumnMixin, DecimalColumnMixin, ImportColumn, SubeventColumnMixin,
i18n_flat,
BooleanColumnMixin, DatetimeColumnMixin, DecimalColumnMixin, ImportColumn,
SubeventColumnMixin, i18n_flat,
)
from pretix.base.models import (
Customer, ItemVariation, OrderPosition, Question, QuestionAnswer,
@@ -604,6 +604,22 @@ class Comment(ImportColumn):
order.comment = value or ''
class CheckinAttentionColumn(BooleanColumnMixin, ImportColumn):
identifier = 'checkin_attention'
verbose_name = gettext_lazy('Requires special attention')
def assign(self, value, order, position, invoice_address, **kwargs):
order.checkin_attention = value
class CheckinTextColumn(ImportColumn):
identifier = 'checkin_text'
verbose_name = gettext_lazy('Check-in text')
def assign(self, value, order, position, invoice_address, **kwargs):
order.checkin_text = value
class QuestionColumn(ImportColumn):
def __init__(self, event, q):
self.q = q
@@ -742,6 +758,8 @@ def get_order_import_columns(event):
ValidUntil(event),
Locale(event),
Saleschannel(event),
CheckinAttentionColumn(event),
CheckinTextColumn(event),
Expires(event),
Comment(event),
]

View File

@@ -571,13 +571,23 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
def get_session_auth_hash(self):
"""
Return an HMAC that needs to
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
logout after every password change.
"""
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
def get_session_auth_fallback_hash(self):
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
yield self._get_session_auth_hash(secret=fallback_secret)
def _get_session_auth_hash(self, secret):
"""
"""
key_salt = "pretix.base.models.User.get_session_auth_hash"
payload = self.password
payload += self.email
payload += self.session_token
return salted_hmac(key_salt, payload).hexdigest()
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
def update_session_token(self):
self.session_token = generate_session_token()
@@ -654,3 +664,9 @@ class WebAuthnDevice(Device):
@property
def webauthnpubkey(self):
return websafe_decode(self.pub_key)
class HistoricPassword(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="historic_passwords")
created = models.DateTimeField(auto_now_add=True)
password = models.CharField(verbose_name=_("Password"), max_length=128)

View File

@@ -219,13 +219,24 @@ class Customer(LoggedModel):
return is_password_usable(self.password)
def get_session_auth_hash(self):
"""
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
logout after every password change.
"""
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
def get_session_auth_fallback_hash(self):
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
yield self._get_session_auth_hash(secret=fallback_secret)
def _get_session_auth_hash(self, secret):
"""
Return an HMAC of the password field.
"""
key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash"
payload = self.password
payload += self.email
return salted_hmac(key_salt, payload).hexdigest()
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
def get_email_context(self):
from pretix.base.settings import get_name_parts_localized

View File

@@ -20,11 +20,11 @@
# <https://www.gnu.org/licenses/>.
#
from collections import defaultdict
from collections import defaultdict, namedtuple
from decimal import Decimal
from itertools import groupby
from math import ceil
from typing import Dict, Optional, Tuple
from math import ceil, inf
from typing import Dict
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
@@ -36,6 +36,8 @@ from django_scopes import ScopedManager
from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
PositionInfo = namedtuple('PositionInfo', ['item_id', 'subevent_id', 'line_price_gross', 'is_addon_to', 'voucher_discount'])
class Discount(LoggedModel):
SUBEVENT_MODE_MIXED = 'mixed'
@@ -245,22 +247,26 @@ class Discount(LoggedModel):
return False
return True
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result):
if self.condition_min_value and sum(positions[idx][2] for idx in condition_idx_group) < self.condition_min_value:
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
if self.condition_min_value and sum(positions[idx].line_price_gross for idx in condition_idx_group) < self.condition_min_value:
return
if self.condition_min_count or self.benefit_only_apply_to_cheapest_n_matches:
raise ValueError('Validation invariant violated.')
for idx in benefit_idx_group:
previous_price = positions[idx][2]
previous_price = positions[idx].line_price_gross
new_price = round_decimal(
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
self.event.currency,
)
result[idx] = new_price
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result):
if collect_potential_discounts is not None:
for idx in condition_idx_group:
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
if len(condition_idx_group) < self.condition_min_count:
return
@@ -268,23 +274,53 @@ class Discount(LoggedModel):
raise ValueError('Validation invariant violated.')
if self.benefit_only_apply_to_cheapest_n_matches:
if not self.condition_min_count:
raise ValueError('Validation invariant violated.')
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
# sort by line_price
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
# want to match multiples of 3
n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches))
# how many discount applications are allowed according to condition products in cart
possible_applications_cond = len(condition_idx_group) // self.condition_min_count
# how many discount applications are possible according to benefitting products in cart
possible_applications_benefit = ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches)
n_groups = min(possible_applications_cond, possible_applications_benefit)
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
if collect_potential_discounts is not None:
if n_groups * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
# partially used discount ("for each 1 ticket you buy, get 50% on 2 t-shirts", cart content: 1 ticket
# but only 1 t-shirt) -> 1 shirt definitiv potential discount
for idx in consume_idx:
collect_potential_discounts[idx] = [
(self, n_groups * self.benefit_only_apply_to_cheapest_n_matches - len(benefit_idx_group), -1, subevent_id)
]
if possible_applications_cond * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
# unused discount ("for each 1 ticket you buy, get 50% on 2 t-shirts", cart content: 1 ticket
# but 0 t-shirts) -> 2 shirt maybe potential discount (if the 1 ticket is not consumed by a later discount)
for i, idx in enumerate(condition_idx_group[
n_groups * self.condition_min_count:
possible_applications_cond * self.condition_min_count
]):
collect_potential_discounts[idx] += [
(self, self.benefit_only_apply_to_cheapest_n_matches, i // self.condition_min_count, subevent_id)
]
else:
consume_idx = condition_idx_group
benefit_idx = benefit_idx_group
if collect_potential_discounts is not None:
for idx in consume_idx:
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
for idx in benefit_idx:
previous_price = positions[idx][2]
previous_price = positions[idx].line_price_gross
new_price = round_decimal(
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
self.event.currency,
@@ -292,15 +328,16 @@ class Discount(LoggedModel):
result[idx] = new_price
for idx in consume_idx:
result.setdefault(idx, positions[idx][2])
result.setdefault(idx, positions[idx].line_price_gross)
def apply(self, positions: Dict[int, Tuple[int, Optional[int], Decimal, bool, Decimal]]) -> Dict[int, Decimal]:
def apply(self, positions: Dict[int, PositionInfo],
collect_potential_discounts=None) -> Dict[int, Decimal]:
"""
Tries to apply this discount to a cart
:param positions: Dictionary mapping IDs to tuples of the form
``(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)``.
:param positions: Dictionary mapping IDs to PositionInfo tuples.
Bundled positions may not be included.
:param collect_potential_discounts: For detailed description, see pretix.base.services.pricing.apply_discounts
:return: A dictionary mapping keys from the input dictionary to new prices. All positions
contained in this dictionary are considered "consumed" and should not be considered
@@ -342,13 +379,13 @@ class Discount(LoggedModel):
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
if self.condition_min_count:
self._apply_min_count(positions, condition_candidates, benefit_candidates, result)
self._apply_min_count(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
else:
self._apply_min_value(positions, condition_candidates, benefit_candidates, result)
self._apply_min_value(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
def key(idx):
return positions[idx][1] or 0 # subevent_id
return positions[idx].subevent_id or 0
# Build groups of candidates with the same subevent, then apply our regular algorithm
# to each group
@@ -357,11 +394,11 @@ class Discount(LoggedModel):
candidate_groups = [(k, list(g)) for k, g in _groups]
for subevent_id, g in candidate_groups:
benefit_g = [idx for idx in benefit_candidates if positions[idx][1] == subevent_id]
benefit_g = [idx for idx in benefit_candidates if positions[idx].subevent_id == subevent_id]
if self.condition_min_count:
self._apply_min_count(positions, g, benefit_g, result)
self._apply_min_count(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
else:
self._apply_min_value(positions, g, benefit_g, result)
self._apply_min_value(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
if self.condition_min_value or not self.benefit_same_products:
@@ -377,9 +414,9 @@ class Discount(LoggedModel):
# Build a list of subevent IDs in descending order of frequency
subevent_to_idx = defaultdict(list)
for idx, p in positions.items():
subevent_to_idx[p[1]].append(idx)
subevent_to_idx[p.subevent_id].append(idx)
for v in subevent_to_idx.values():
v.sort(key=lambda idx: positions[idx][2])
v.sort(key=lambda idx: positions[idx].line_price_gross)
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
# Build groups of exactly condition_min_count distinct subevents
@@ -394,7 +431,7 @@ class Discount(LoggedModel):
l = [ll for ll in l if ll in condition_candidates and ll not in current_group]
if cardinality and len(l) != cardinality:
continue
if se not in {positions[idx][1] for idx in current_group}:
if se not in {positions[idx].subevent_id for idx in current_group}:
candidates += l
cardinality = len(l)
@@ -403,7 +440,7 @@ class Discount(LoggedModel):
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
# and 2 from the end" scheme to optimize price distribution among groups
candidates = sorted(candidates, key=lambda idx: positions[idx][2])
candidates = sorted(candidates, key=lambda idx: positions[idx].line_price_gross)
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
candidate = candidates[0]
else:
@@ -415,14 +452,14 @@ class Discount(LoggedModel):
if len(current_group) >= max(self.condition_min_count, 1):
candidate_groups.append(current_group)
for c in current_group:
subevent_to_idx[positions[c][1]].remove(c)
subevent_to_idx[positions[c].subevent_id].remove(c)
current_group = []
# Distribute "leftovers"
for se in subevent_order:
if subevent_to_idx[se]:
for group in candidate_groups:
if se not in {positions[idx][1] for idx in group}:
if se not in {positions[idx].subevent_id for idx in group}:
group.append(subevent_to_idx[se].pop())
if not subevent_to_idx[se]:
break
@@ -432,6 +469,8 @@ class Discount(LoggedModel):
positions,
[idx for idx in g if idx in condition_candidates],
[idx for idx in g if idx in benefit_candidates],
result
result,
None,
None
)
return result

View File

@@ -870,10 +870,12 @@ class Event(EventMixin, LoggedModel):
for i in Item.objects.filter(event=other).prefetch_related(
'variations', 'limit_sales_channels', 'require_membership_types',
'variations__limit_sales_channels', 'variations__require_membership_types',
'matched_by_cross_selling_categories',
):
vars = list(i.variations.all())
require_membership_types = list(i.require_membership_types.all())
limit_sales_channels = list(i.limit_sales_channels.all())
matched_by_cross_selling_categories = list(i.matched_by_cross_selling_categories.all())
item_map[i.pk] = i
i.pk = None
i.event = self
@@ -911,6 +913,9 @@ class Event(EventMixin, LoggedModel):
if not v.all_sales_channels:
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
if matched_by_cross_selling_categories:
i.matched_by_cross_selling_categories.set([category_map[c.pk] for c in matched_by_cross_selling_categories])
for i in self.items.filter(hidden_if_item_available__isnull=False):
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
i.save()

View File

@@ -63,14 +63,13 @@ from django_countries.fields import Country
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Event, SubEvent
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from pretix.base.timemachine import time_machine_now
from ...helpers.images import ImageSizeValidator
from ..media import MEDIA_TYPES
from .event import Event, SubEvent
from pretix.helpers.images import ImageSizeValidator
class ItemCategory(LoggedModel):
@@ -111,6 +110,33 @@ class ItemCategory(LoggedModel):
'only be bought in combination with a product that has this category configured as a possible '
'source for add-ons.')
)
CROSS_SELLING_MODES = (
(None, _('Normal category')),
('both', _('Normal + cross-selling category')),
('only', _('Cross-selling category')),
)
cross_selling_mode = models.CharField(
choices=CROSS_SELLING_MODES,
null=True,
max_length=5
)
CROSS_SELLING_CONDITION = (
('always', _('Always show in cross-selling step')),
('discounts', _('Only show products that qualify for a discount according to discount rules')),
('products', _('Only show if the cart contains one of the following products')),
)
cross_selling_condition = models.CharField(
verbose_name=_("Cross-selling condition"),
choices=CROSS_SELLING_CONDITION,
null=True,
max_length=10,
)
cross_selling_match_products = models.ManyToManyField(
'pretixbase.Item',
blank=True,
verbose_name=_("Cross-selling condition products"),
related_name="matched_by_cross_selling_categories",
)
class Meta:
verbose_name = _("Product category")
@@ -119,19 +145,31 @@ class ItemCategory(LoggedModel):
def __str__(self):
name = self.internal_name or self.name
if self.is_addon:
return _('{category} (Add-On products)').format(category=str(name))
if self.category_type != 'normal':
return _('{category} ({category_type})').format(category=str(name),
category_type=self.get_category_type_display())
return str(name)
def get_category_type_display(self):
if self.is_addon:
return _('Add-On products')
return _('Add-on category')
elif self.cross_selling_mode:
return self.get_cross_selling_mode_display()
else:
return None
return _('Normal category')
@property
def category_type(self):
return 'addon' if self.is_addon else 'normal'
return 'addon' if self.is_addon else self.cross_selling_mode or 'normal'
@category_type.setter
def category_type(self, new_value):
if new_value == 'addon':
self.is_addon = True
self.cross_selling_mode = None
else:
self.is_addon = False
self.cross_selling_mode = None if new_value == 'normal' else new_value
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -270,7 +308,7 @@ class SubEventItemVariation(models.Model):
return True
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
# makes the query SIGNIFICANTLY faster
from .organizer import SalesChannel
@@ -291,6 +329,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
if not allow_addons:
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
if not allow_cross_sell:
q &= Q(Q(category__isnull=True) | ~Q(category__cross_selling_mode='only'))
if voucher:
if voucher.item_id:
@@ -304,8 +344,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
class ItemQuerySet(models.QuerySet):
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_available(self, channel, voucher, allow_addons)
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
return filter_available(self, channel, voucher, allow_addons, allow_cross_sell)
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
@@ -313,8 +353,8 @@ class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__)
super().__init__()
self._queryset_class = ItemQuerySet
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
return filter_available(self.get_queryset(), channel, voucher, allow_addons, allow_cross_sell)
class Item(LoggedModel):

View File

@@ -40,6 +40,7 @@ import json
import logging
import operator
import string
import warnings
from collections import Counter
from datetime import datetime, time, timedelta
from decimal import Decimal
@@ -381,8 +382,28 @@ class Order(LockModel, LoggedModel):
self.event.cache.delete('complain_testmode_orders')
self.delete()
def email_confirm_secret(self):
return self.tagged_secret("email_confirm", 9)
def email_confirm_hash(self):
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
warnings.warn('Use email_confirm_secret() instead of email_confirm_hash().',
DeprecationWarning)
return self.email_confirm_secret()
def check_email_confirm_secret(self, received_secret):
return (
hmac.compare_digest(
self.tagged_secret("email_confirm", 9),
received_secret[:9].lower()
) or any(
# TODO: remove this clause after a while (compatibility with old secrets currently in flight)
hmac.compare_digest(
hashlib.sha256(sk.encode() + self.secret.encode()).hexdigest()[:9],
received_secret
)
for sk in [settings.SECRET_KEY, *settings.SECRET_KEY_FALLBACKS]
)
)
def get_extended_status_display(self):
# Changes in this method should to be replicated in pretixcontrol/orders/fragment_order_status.html
@@ -2835,6 +2856,14 @@ class OrderPosition(AbstractPosition):
(self.order.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
)
@property
def code(self):
"""
A ticket code which is unique among all events of a single organizer,
built by the order code and the position number.
"""
return '{order_code}-{position}'.format(order_code=self.order.code, position=self.positionid)
class Transaction(models.Model):
"""

View File

@@ -29,6 +29,8 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.deconstruct import deconstructible
from django.utils.formats import localize
from django.utils.functional import lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.fields import I18nCharField
from i18nfield.strings import LazyI18nString
@@ -120,6 +122,8 @@ EU_CURRENCIES = {
}
VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH', 'NO'}
format_html_lazy = lazy(format_html, str)
def is_eu_country(cc):
cc = str(cc)
@@ -193,11 +197,17 @@ class TaxRule(LoggedModel):
eu_reverse_charge = models.BooleanField(
verbose_name=_("Use EU reverse charge taxation rules"),
default=False,
help_text=_("Not recommended. Most events will NOT be qualified for reverse charge since the place of "
"taxation is the location of the event. This option disables charging VAT for all customers "
"outside the EU and for business customers in different EU countries who entered a valid EU VAT "
"ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax "
"calculation. USE AT YOUR OWN RISK.")
help_text=format_html_lazy(
'<span class="label label-warning" data-toggle="tooltip" title="{}">{}</span> {}',
_('This feature will be removed in the future as it does not handle VAT for non-business customers in '
'other EU countries in a way that works for all organizers. Use custom rules instead.'),
_('DEPRECATED'),
_("Not recommended. Most events will NOT be qualified for reverse charge since the place of "
"taxation is the location of the event. This option disables charging VAT for all customers "
"outside the EU and for business customers in different EU countries who entered a valid EU VAT "
"ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax "
"calculation. USE AT YOUR OWN RISK.")
),
)
home_country = FastCountryField(
verbose_name=_('Merchant country'),
@@ -294,10 +304,24 @@ class TaxRule(LoggedModel):
subtract_from_gross = Decimal('0.00')
rate = adjust_rate
def _limit_subtract(base_price, subtract_from_gross):
if not subtract_from_gross:
return base_price
if base_price >= Decimal('0.00'):
# For positive prices, make sure they don't go negative because of bundles
return max(Decimal('0.00'), base_price - subtract_from_gross)
else:
# If the price is already negative, we don't really care any more
return base_price - subtract_from_gross
if rate == Decimal('0.00'):
gross = _limit_subtract(base_price, subtract_from_gross)
return TaxedPrice(
net=base_price - subtract_from_gross, gross=base_price - subtract_from_gross, tax=Decimal('0.00'),
rate=rate, name=self.name
net=gross,
gross=gross,
tax=Decimal('0.00'),
rate=rate,
name=self.name,
)
if base_price_is == 'auto':
@@ -307,19 +331,14 @@ class TaxRule(LoggedModel):
base_price_is = 'net'
if base_price_is == 'gross':
if base_price >= Decimal('0.00'):
# For positive prices, make sure they don't go negative because of bundles
gross = max(Decimal('0.00'), base_price - subtract_from_gross)
else:
# If the price is already negative, we don't really care any more
gross = base_price - subtract_from_gross
gross = _limit_subtract(base_price, subtract_from_gross)
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
elif base_price_is == 'net':
net = base_price
gross = round_decimal((net * (1 + rate / 100)), currency)
if subtract_from_gross:
gross -= subtract_from_gross
gross = _limit_subtract(gross, subtract_from_gross)
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
else:

View File

@@ -1542,10 +1542,9 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param voucher: A voucher code
:param session: Session ID of a guest
:param cart_id: The cart ID of the cart to modify
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
@@ -1566,10 +1565,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes a list of items from a user's cart.
Removes an item specified by its position ID from a user's cart.
:param event: The event ID in question
:param position: A cart position ID
:param session: Session ID of a guest
:param cart_id: The cart ID of the cart to modify
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
@@ -1590,9 +1589,9 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes a list of items from a user's cart.
Removes all items from a user's cart.
:param event: The event ID in question
:param session: Session ID of a guest
:param cart_id: The cart ID of the cart to modify
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
@@ -1611,13 +1610,15 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes a list of items from a user's cart.
Assigns addons to eligible products in a user's cart, adding and removing the addon products as necessary to
ensure the requested addon state.
:param event: The event ID in question
:param addons: A list of dicts with the keys addon_to, item, variation
:param session: Session ID of a guest
:param add_to_cart_items: A list of dicts with the keys item, variation, count, custom_price, voucher, seat ID
:param cart_id: The cart ID of the cart to modify
"""
with language(locale), time_machine_now_assigned(override_now_dt):
ia = False
@@ -1635,6 +1636,7 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
cm.set_addons(addons)
cm.add_new_items(add_to_cart_items)
cm.commit()
except LockTimeoutException:
self.retry()

View File

@@ -1182,10 +1182,11 @@ def process_exit_all(sender, **kwargs):
positions = cl.positions_inside_query(ignore_status=True, at_time=cl.exit_all_at)
for p in positions:
with scope(organizer=cl.event.organizer):
ci = Checkin.objects.create(
ci, created = Checkin.objects.get_or_create(
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
)
checkin_created.send(cl.event, checkin=ci)
if created:
checkin_created.send(cl.event, checkin=ci)
d = cl.exit_all_at.astimezone(cl.event.timezone)
if cl.event.settings.get(f'autocheckin_dst_hack_{cl.pk}'): # move time back if yesterday was DST switch
d -= timedelta(hours=1)

View File

@@ -0,0 +1,234 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from collections import defaultdict
from decimal import Decimal
from itertools import groupby
from math import inf
from typing import List
from django.utils.functional import cached_property
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
from pretix.presale.views.event import get_grouped_items
class DummyCategory:
"""
Used to create fake category objects for displaying the same cross-selling category multiple times,
once for each subevent
"""
def __init__(self, category: ItemCategory, subevent):
self.id = category.id
self.name = str(category.name)
self.subevent_name = str(subevent)
self.description = category.description
class CrossSellingService:
def __init__(self, event, sales_channel: SalesChannel, cartpositions: List[CartPosition], customer):
self.event = event
self.sales_channel = sales_channel
self.cartpositions = cartpositions
self.customer = customer
def get_data(self):
if self.event.has_subevents:
subevents = set(pos.subevent for pos in self.cartpositions)
result = (
(DummyCategory(category, subevent),
self._prepare_items(subevent, items_qs, discount_info),
f'subevent_{subevent.pk}_')
for subevent in subevents
for (category, items_qs, discount_info) in self._applicable_categories(subevent.pk)
)
else:
result = (
(category,
self._prepare_items(None, items_qs, discount_info),
'')
for (category, items_qs, discount_info) in self._applicable_categories(0)
)
result = [(category, items, form_prefix) for (category, items, form_prefix) in result if len(items) > 0]
for category, items, form_prefix in result:
category.category_has_discount = any(item.original_price or (
item.has_variations and any(var.original_price for var in item.available_variations)
) for item in items)
return result
def _applicable_categories(self, subevent_id):
return [
(c, products_qs, discount_info) for (c, products_qs, discount_info) in
(
(c, *self._get_visible_items_for_category(subevent_id, c))
for c in self.event.categories.filter(cross_selling_mode__isnull=False).prefetch_related('items')
)
if products_qs is not None
]
def _get_visible_items_for_category(self, filter_subevent_id, category: ItemCategory):
"""
If this category should be visible in the cross-selling step for a given cart and sales_channel, this method
returns a queryset of the items that should be displayed, as well as a dict giving additional information on them.
:returns: (QuerySet<Item>, dict<(subevent_id, item_pk): (max_count, discount_rule)>)
max_count is `inf` if the item should not be limited
discount_rule is None if the item will not be discounted
"""
if category.cross_selling_mode is None:
return None, {}
if category.cross_selling_condition == 'always':
return category.items.all(), {}
if category.cross_selling_condition == 'products':
match = set(match.pk for match in category.cross_selling_match_products.only('pk')) # TODO prefetch this
return (category.items.all(), {}) if any(pos.item.pk in match for pos in self.cartpositions) else (None, {})
if category.cross_selling_condition == 'discounts':
my_item_pks = [item.id for item in category.items.all()]
potential_discount_items = {
item.pk: (max_count, discount_rule)
for subevent_id, item, max_count, discount_rule in self._potential_discounts_by_subevent_and_item_for_current_cart
if max_count > 0 and item.pk in my_item_pks and item.is_available() and (subevent_id == filter_subevent_id or subevent_id is None)
}
return category.items.filter(pk__in=potential_discount_items), potential_discount_items
@cached_property
def _potential_discounts_by_subevent_and_item_for_current_cart(self):
potential_discounts_by_cartpos = defaultdict(list)
from ..services.pricing import apply_discounts
self._discounted_prices = apply_discounts(
self.event,
self.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled,
cp.listed_price - cp.price_after_voucher)
for cp in self.cartpositions
],
collect_potential_discounts=potential_discounts_by_cartpos
)
# flatten potential_discounts_by_cartpos (a dict of lists of potential discounts) into a set of potential discounts
# (which is technically stored as a dict, but we use it as an OrderedSet here)
potential_discount_set = dict.fromkeys(
info for lst in potential_discounts_by_cartpos.values() for info in lst)
# sum up the max_counts and pass them on (also pass on the discount_rules so we can calculate actual discounted prices later):
# group by benefit product
# - max_count for product: sum up max_counts
# - discount_rule for product: take first discount_rule
def discount_info(subevent_id, item, infos_for_item):
infos_for_item = list(infos_for_item)
return (
subevent_id,
item,
sum(max_count for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
next(discount_rule for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
)
return [
discount_info(subevent_id, item, infos_for_item) for (subevent_id, item), infos_for_item in
groupby(
sorted(
(
(subevent_id, item, discount_rule, max_count, i)
for (discount_rule, max_count, i, subevent_id) in potential_discount_set.keys()
for item in discount_rule.benefit_limit_products.all()
),
key=lambda tup: (tup[0], tup[1].pk)
),
lambda tup: (tup[0], tup[1]))
]
def _prepare_items(self, subevent, items_qs, discount_info):
items, _btn = get_grouped_items(
self.event,
subevent=subevent,
voucher=None,
channel=self.sales_channel,
base_qs=items_qs,
allow_addons=False,
allow_cross_sell=True,
memberships=(
self.customer.usable_memberships(
for_event=subevent or self.event,
testmode=self.event.testmode
)
if self.customer else None
),
)
new_items = list()
for item in items:
max_count = inf
if item.pk in discount_info:
(max_count, discount_rule) = discount_info[item.pk]
# only benefit_only_apply_to_cheapest_n_matches discounted items have a max_count, all others get 'inf'
if not max_count:
max_count = inf
# calculate discounted price
if discount_rule and discount_rule.benefit_discount_matching_percent > 0:
if not item.has_variations:
item.original_price = item.original_price or item.display_price
previous_price = item.display_price
new_price = (
previous_price * (
(Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00'))
)
item.display_price = new_price
else:
# discounts always match "whole" items, not specific variations -> we apply the discount to all
# available variations of the item
for var in item.available_variations:
var.original_price = var.original_price or var.display_price
previous_price = var.display_price
new_price = (
previous_price * (
(Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00'))
)
var.display_price = new_price
if not item.has_variations:
# reduce order_max by number of items already in cart (prevent recommending a product the user can't add anyway)
item.order_max = min(
item.order_max - sum(1 for pos in self.cartpositions if pos.item_id == item.pk),
max_count
)
if item.order_max > 0:
new_items.append(item)
else:
new_vars = list()
for var in item.available_variations:
# reduce order_max by number of items already in cart (prevent recommending a product the user can't add anyway)
var.order_max = min(
var.order_max - sum(1 for pos in self.cartpositions if pos.item_id == item.pk and pos.variation_id == var.pk),
max_count
)
if var.order_max > 0:
new_vars.append(var)
if len(new_vars):
item.available_variations = new_vars
new_items.append(item)
return new_items

View File

@@ -301,7 +301,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
'hash': order.email_confirm_secret()
}
)
)

View File

@@ -262,7 +262,7 @@ def base_placeholders(sender, **kwargs):
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
'hash': order.email_confirm_secret()
}
), lambda event: build_absolute_uri(
event,
@@ -443,7 +443,7 @@ def base_placeholders(sender, **kwargs):
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash(),
'hash': order.email_confirm_secret(),
}),
)
for order in orders

View File

@@ -20,8 +20,9 @@
# <https://www.gnu.org/licenses/>.
#
import re
from collections import defaultdict
from decimal import Decimal
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Union
from django import forms
from django.db.models import Q
@@ -31,6 +32,7 @@ from pretix.base.models import (
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
SalesChannel, Voucher,
)
from pretix.base.models.discount import Discount, PositionInfo
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.timemachine import time_machine_now
@@ -155,14 +157,22 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
return price
def apply_discounts(event: Event, sales_channel: str,
positions: List[Tuple[int, Optional[int], Decimal, bool, bool]]) -> List[Decimal]:
def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
positions: List[Tuple[int, Optional[int], Decimal, bool, bool, Decimal]],
collect_potential_discounts: Optional[defaultdict]=None) -> List[Tuple[Decimal, Optional[Discount]]]:
"""
Applies any dynamic discounts to a cart
:param event: Event the cart belongs to
:param sales_channel: Sales channel the cart was created with
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
of tuples describing the discounts that could be applied in the form `(discount, max_count, grouping_id)`.
`max_count` is either the maximum number of benefitting items that the discount would apply to, or `inf` if that number
is not limited. The `grouping_id` can be used to distinguish several occurrences of the same discount.
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
"""
if isinstance(sales_channel, SalesChannel):
@@ -177,10 +187,10 @@ def apply_discounts(event: Event, sales_channel: str,
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs:
result = discount.apply({
idx: (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
idx: PositionInfo(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount) in enumerate(positions)
if not is_bundled and idx not in new_prices
})
}, collect_potential_discounts)
for k in result.keys():
result[k] = (result[k], discount)
new_prices.update(result)

View File

@@ -53,7 +53,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
v.tag = r.get('tag')
if v.comment:
v.comment += '\n\n'
v.comment = gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
v.comment += gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
logs.append(v.log_action(
'pretix.voucher.sent',
user=user,

View File

@@ -571,7 +571,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Custom recipient field"),
label=_("Custom recipient field label"),
widget=I18nTextInput,
help_text=_("If you want to add a custom text field, e.g. for a country-specific registration number, to "
"your invoice address form, please fill in the label here. This label will both be used for "
@@ -580,6 +580,18 @@ DEFAULTS = {
"The field will not be required.")
)
},
'invoice_address_custom_field_helptext': {
'default': '',
'type': LazyI18nString,
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Custom recipient field help text"),
widget=I18nTextInput,
help_text=_("If you use the custom recipient field, you can specify a help text which will be displayed "
"underneath the field. It will not be displayed on the invoice.")
)
},
'invoice_address_vatid': {
'default': 'False',
'type': bool,

View File

@@ -838,3 +838,12 @@ is given as the first argument.
The ``sender`` keyword argument will contain the organizer.
"""
device_info_updated = django.dispatch.Signal()
"""
Arguments: ``old_device``, ``new_device``
This signal is sent out each time the information for a Device is modified.
Both the original and updated versions of the Device are included to allow
receivers to see what has been updated.
"""

View File

@@ -143,7 +143,7 @@
</tr>
</table>
<div class="order-button">
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}" class="button">
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_secret order=order.code secret=order.secret %}" class="button">
{% trans "View order details" %}
</a>
</div>

View File

@@ -103,7 +103,7 @@ def timeline_for_event(event, subevent=None):
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer modify their orders'),
description=pgettext_lazy('timeline', 'Customers can no longer modify their order information'),
edit_url=ev_edit_url
))
@@ -159,6 +159,18 @@ def timeline_for_event(event, subevent=None):
})
))
rd = event.settings.get('change_allow_user_until', as_type=RelativeDateWrapper)
if rd and event.settings.change_allow_user_until:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer make changes to their orders'),
edit_url=reverse('control:event.settings.cancel', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
rd = event.settings.get('waiting_list_auto_disable', as_type=RelativeDateWrapper)
if rd and event.settings.waiting_list_enabled:
tl.append(TimelineEvent(

View File

@@ -30,7 +30,9 @@ from celery import states
from celery.result import AsyncResult
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.exceptions import (
BadRequest, PermissionDenied, ValidationError,
)
from django.core.files.uploadedfile import UploadedFile
from django.db import transaction
from django.http import HttpResponse, JsonResponse, QueryDict
@@ -131,6 +133,8 @@ class AsyncMixin:
return data
def get_result(self, request):
if not request.GET.get('async_id'):
raise BadRequest("No async_id given")
res = AsyncResult(request.GET.get('async_id'))
if 'ajax' in self.request.GET:
return JsonResponse(self._return_ajax_result(res, timeout=0.25))
@@ -140,7 +144,12 @@ class AsyncMixin:
return self.success(res.info)
else:
return self.error(res.info)
return render(request, 'pretixpresale/waiting.html')
state, info = res.state, res.info
return render(request, 'pretixpresale/waiting.html', {
'started': state in ('PROGRESS', 'STARTED'),
'percentage': info.get('value', 0) if isinstance(info, dict) else 0,
'steps': info.get('steps', []) if isinstance(info, dict) else None,
})
def success(self, value):
smes = self.get_success_message(value)
@@ -208,6 +217,8 @@ class AsyncAction(AsyncMixin):
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
if not request.GET.get('async_id'):
raise BadRequest("No async_id given")
return self.get_result(request)
return self.http_method_not_allowed(request)

View File

@@ -36,6 +36,7 @@ import sys
from importlib import import_module
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q
from django.urls import Resolver404, get_script_prefix, resolve
from django.utils.translation import get_language
@@ -152,6 +153,8 @@ def _default_context(request):
ctx['warning_update_available'] = True
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
ctx['warning_update_check_active'] = True
if not cache.get('pretix_runperiodic_executed') and not settings.DEBUG:
ctx['warning_cronjob'] = True
ctx['ie_deprecation_warning'] = 'MSIE' in request.headers.get('User-Agent', '') or 'Trident/' in request.headers.get('User-Agent', '')

View File

@@ -844,6 +844,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_address_custom_field_helptext',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_include_free',
@@ -1143,12 +1144,12 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
widget=I18nTextInput,
)
mail_subject_order_incomplete_payment = I18nFormField(
label=_("Subject"),
label=_("Subject (if an incomplete payment was received)"),
required=False,
widget=I18nTextInput,
)
mail_text_order_incomplete_payment = I18nFormField(
label=_("Text"),
label=_("Text (if an incomplete payment was received)"),
required=False,
widget=I18nMarkdownTextarea,
help_text=_("This email only applies to payment methods that can receive incomplete payments, "

View File

@@ -40,7 +40,8 @@ from urllib.parse import urlencode
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Max
from django.db.models import Max, Q
from django.forms import ChoiceField, RadioSelect
from django.forms.formsets import DELETION_FIELD_NAME
from django.urls import reverse
from django.utils.functional import cached_property
@@ -79,11 +80,67 @@ class CategoryForm(I18nModelForm):
'name',
'internal_name',
'description',
'is_addon'
'cross_selling_condition',
'cross_selling_match_products',
]
widgets = {
'description': I18nMarkdownTextarea,
'cross_selling_condition': RadioSelect,
}
field_classes = {
'cross_selling_match_products': SafeModelMultipleChoiceField,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tpl = '{} &nbsp; <span class="text-muted">{}</span>'
self.fields['category_type'] = ChoiceField(widget=RadioSelect, choices=(
('normal', mark_safe(tpl.format(
_('Normal category'),
_('Products in this category are regular products displayed on the front page.')
)),),
('addon', mark_safe(tpl.format(
_('Add-on product category'),
_('Products in this category are add-on products and can only be bought as add-ons.')
)),),
('only', mark_safe(tpl.format(
_('Cross-selling category'),
_('Products in this category are regular products, but are only shown in the cross-selling step, '
'according to the configuration below.')
)),),
('both', mark_safe(tpl.format(
_('Normal + cross-selling category'),
_('Products in this category are regular products displayed on the front page, but are additionally '
'shown in the cross-selling step, according to the configuration below.')
)),),
))
self.fields['category_type'].initial = self.instance.category_type
self.fields['cross_selling_condition'].widget.attrs['data-display-dependency'] = '#id_category_type_2,#id_category_type_3'
self.fields['cross_selling_condition'].widget.attrs['data-disable-dependent'] = 'true'
self.fields['cross_selling_condition'].widget.choices = self.fields['cross_selling_condition'].widget.choices[1:]
self.fields['cross_selling_condition'].required = False
self.fields['cross_selling_match_products'].widget = forms.CheckboxSelectMultiple(
attrs={
'class': 'scrolling-multiple-choice',
'data-display-dependency': '#id_cross_selling_condition_2'
}
)
self.fields['cross_selling_match_products'].queryset = self.event.items.filter(
# don't show products which are only visible in addon/cross-sell step themselves
Q(category__isnull=True) | Q(
Q(category__is_addon=False) & Q(Q(category__cross_selling_mode='both') | Q(category__cross_selling_mode__isnull=True))
)
)
def clean(self):
d = super().clean()
if d.get('category_type') == 'only' or d.get('category_type') == 'both':
if not d.get('cross_selling_condition'):
raise ValidationError({'cross_selling_condition': [_('This field is required')]})
self.instance.category_type = d.get('category_type')
return d
class QuestionForm(I18nModelForm):

View File

@@ -239,11 +239,14 @@ class VoucherForm(I18nModelForm):
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
)
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
if 'seat' in self.fields and data.get('seat'):
self.instance.seat = Voucher.clean_seat_id(
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
)
self.instance.item = self.instance.seat.product
if 'seat' in self.fields:
if data.get('seat'):
self.instance.seat = Voucher.clean_seat_id(
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
)
self.instance.item = self.instance.seat.product
else:
self.instance.seat = None
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)

View File

@@ -421,6 +421,14 @@
</a>
</div>
{% endif %}
{% if warning_cronjob %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
The cronjob component of pretix was not executed in the last hours. Please check that
you have completed all installation steps and your cronjob is executed correctly.
{% endblocktrans %}
</div>
{% endif %}
{% if debug_warning %}
<div class="alert alert-danger">

View File

@@ -32,6 +32,7 @@
{% bootstrap_field form.invoice_address_beneficiary layout="control" %}
{% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
{% bootstrap_field form.invoice_address_custom_field layout="control" %}
{% bootstrap_field form.invoice_address_custom_field_helptext layout="control" %}
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
</fieldset>
<fieldset>

View File

@@ -41,7 +41,7 @@
{% bootstrap_field form.eu_reverse_charge layout="control" %}
{% bootstrap_field form.home_country layout="control" %}
{% bootstrap_field form.keep_gross_if_rate_changes layout="control" %}
<h3>{% trans "Custom taxation rules" %}</h3>
<h3>{% trans "Custom rules" %}</h3>
<div class="alert alert-warning">
{% blocktrans trimmed %}
These settings are intended for professional users with very specific taxation situations.

View File

@@ -31,6 +31,7 @@
<thead>
<tr>
<th>{% trans "Product categories" %}</th>
<th>{% trans "Category type" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
@@ -40,6 +41,9 @@
<td>
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
</td>
<td>
{{ c.get_category_type_display }}
</td>
<td class="text-right flip">
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>

View File

@@ -16,7 +16,9 @@
{% bootstrap_field form.internal_name layout="control" %}
</div>
{% bootstrap_field form.description layout="control" %}
{% bootstrap_field form.is_addon layout="control" %}
{% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-lg-9" %}
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-lg-9" %}
{% bootstrap_field form.cross_selling_match_products layout="control" %}
</fieldset>
</div>
{% if category %}

View File

@@ -133,7 +133,7 @@
{% endif %}
{{ i.default_price|money:request.event.currency }}
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
{% if i.tax_rule and i.default_price %}
{% if i.tax_rule %}
<br/>
<small class="text-muted">
{% if not i.tax_rule.price_includes_tax %}

View File

@@ -13,31 +13,52 @@
{% trans "Edit question" %}
</a>
</h1>
<form class="form-inline helper-display-inline" action="" method="get">
<p>
<select name="status" class="form-control">
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
</select>
<select name="item" class="form-control">
<option value="">{% trans "All products" %}</option>
{% for item in items %}
<option value="{{ item.id }}"
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
{{ item.name }}
</option>
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
</form>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Filter" %}</h3>
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-lg-2 col-sm-6 col-xs-6">
<select name="status" class="form-control">
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
</select>
</div>
<div class="col-lg-5 col-sm-6 col-xs-6">
<select name="item" class="form-control">
<option value="">{% trans "All products" %}</option>
{% for item in items %}
<option value="{{ item.id }}"
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
{{ item.name }}
</option>
{% endfor %}
</select>
</div>
{% if request.event.has_subevents %}
<div class="col-lg-5 col-sm-6 col-xs-6">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
</div>
{% endif %}
</div>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">
<span class="fa fa-filter"></span>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
<div class="row" id="question-stats">
{% if not stats %}
<div class="empty-collection col-md-10 col-xs-12">
@@ -75,7 +96,7 @@
{% for stat in stats %}
<tr>
<td>
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status={{ request.GET.status|default:"np" }}&item={{ request.GET.item }}&question={{ question.pk }}&answer={{ stat.alink|default:stat.answer|urlencode }}">
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status={{ request.GET.status|default:"np" }}&item={{ request.GET.item }}&subevent={{ request.GET.subevent }}&question={{ question.pk }}&answer={{ stat.alink|default:stat.answer|urlencode }}">
{{ stat.answer }}
</a>
</td>

View File

@@ -21,6 +21,11 @@
{% trans "The waiting list is no longer active for this event. The waiting list no longer affects quotas and no longer notifies waiting users." %}
</div>
{% endif %}
{% if request.event.settings.hide_sold_out %}
<div class="alert alert-warning">
{% trans "According to your event settings, sold out products are hidden from customers. This way, customers will not be able to discover the waiting list." %}
</div>
{% endif %}
<div class="row">
{% if 'can_change_orders' in request.eventpermset %}
<form method="post" class="col-md-6"

View File

@@ -94,7 +94,9 @@ def process_login(request, user, keep_logged_in):
pretix_successful_logins.inc(1)
handle_login_source(user, request)
auth_login(request, user)
request.session['pretix_auth_login_time'] = int(time.time())
t = int(time.time())
request.session['pretix_auth_login_time'] = t
request.session['pretix_auth_last_used'] = t
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
return redirect_to_url(next_url)
return redirect('control:index')

View File

@@ -39,7 +39,7 @@ from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Exists, Max, OuterRef, Prefetch, Q, Subquery
from django.http import Http404, HttpResponseRedirect
from django.http import Http404, HttpResponseNotAllowed, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.functional import cached_property
@@ -193,6 +193,9 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
class CheckInListBulkRevertConfirmView(CheckInListQueryMixin, EventPermissionRequiredMixin, TemplateView):
template_name = "pretixcontrol/checkin/bulk_revert_confirm.html"
def get(self, request, *args, **kwargs):
return HttpResponseNotAllowed(permitted_methods=["POST"])
def post(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
return super().get(request, *args, **kwargs)

View File

@@ -302,6 +302,8 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
i = modelcopy(self.copy_from)
i.pk = None
kwargs['instance'] = i
kwargs.setdefault('initial', {})
kwargs['initial']['cross_selling_match_products'] = [str(i.pk) for i in self.copy_from.cross_selling_match_products.all()]
else:
kwargs['instance'] = ItemCategory(event=self.request.event)
return kwargs
@@ -661,6 +663,10 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
question=self.object, orderposition__isnull=False,
orderposition__order__event=self.request.event
)
if self.request.GET.get("subevent", "") != "":
qs = qs.filter(orderposition__subevent=self.request.GET["subevent"])
s = self.request.GET.get("status", "np")
if s != "":
if s == 'o':

View File

@@ -156,7 +156,7 @@ def event_list(request):
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
).annotate(
order_from=Coalesce('min_from', 'date_from'),
).order_by('-order_from')
).order_by('-order_from', 'slug')
total = qs.count()
pagesize = 20
@@ -318,7 +318,7 @@ def nav_context_list(request):
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
).annotate(
order_from=Coalesce('min_from', 'date_from'),
).order_by('-order_from')
).order_by('-order_from', 'slug')
if request.user.has_active_staff_session(request.session.session_key):
qs_orga = Organizer.objects.all()

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -622,40 +622,40 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -656,40 +656,40 @@ msgid ""
"darker shade."
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr "البحث في الاستفسارات"
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "الكل"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr "قم باستخدم اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "اضغط لاغلاق الصفحة"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr "لم تقم بحفظ التعديلات!"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -623,40 +623,40 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
"Last-Translator: Michael <michael.happl@gmx.at>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -650,40 +650,40 @@ msgstr ""
"Tato barva je pro text na bílém pozadí špatně kontrastní, zvolte prosím "
"tmavší odstín."
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr "Hledaný výraz"
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Všechny"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr "Žádný"
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr "Pouze vybrané"
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr "Interně používat jiný název"
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "Kliknutím zavřete"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr "Máte neuložené změny!"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -623,40 +623,40 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2024-07-10 15:00+0000\n"
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -670,40 +670,40 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "Klik for at lukke"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr "Du har ændringer, der ikke er gemt!"

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-08-27 16:02+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
"de/>\n"
@@ -572,10 +572,8 @@ msgid "Group of objects"
msgstr "Gruppe von Objekten"
#: pretix/static/pretixcontrol/js/ui/editor.js:899
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)"
msgstr "Text-Objekt"
msgstr "Text-Objekt (veraltet)"
#: pretix/static/pretixcontrol/js/ui/editor.js:901
msgid "Text box"
@@ -646,40 +644,40 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wählen Sie eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr "Suchbegriff"
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein."
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr "Ungültige Seitenzahl."
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr "Sie haben ungespeicherte Änderungen!"

View File

@@ -4,6 +4,7 @@ AES
Absenderadresse
Absenderinformation
Absendername
ABGEKÜNDIGT
Admin
Adminbereich
Affirm
@@ -25,6 +26,7 @@ Ausgangsscans
ausgeklappt
ausgecheckt
auswahl
Auth
Authentication
Authenticator
Authentifizierungsmechanismus
@@ -36,6 +38,7 @@ Bancontact
BankID
Banking
barcodes
Baskisch
Bcc
BCC
Beispielevent
@@ -79,6 +82,7 @@ Connect
Consent
Copyleft
Cronjob
Cross
csv
Customer
CZK
@@ -277,6 +281,7 @@ Scopes
sechsstelligen
Secret
Security
Selling
SEPA
Shirts
Signaturverfahren

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-08-27 16:00+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/de_Informal/>\n"
@@ -571,10 +571,8 @@ msgid "Group of objects"
msgstr "Gruppe von Objekten"
#: pretix/static/pretixcontrol/js/ui/editor.js:899
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)"
msgstr "Text-Objekt"
msgstr "Text-Objekt (veraltet)"
#: pretix/static/pretixcontrol/js/ui/editor.js:901
msgid "Text box"
@@ -645,40 +643,40 @@ msgstr ""
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
"Hintergrund. Bitte wähle eine dunklere Farbe."
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr "Suchbegriff"
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein."
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr "Ungültige Seitenzahl."
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr "Du hast ungespeicherte Änderungen!"

View File

@@ -4,6 +4,7 @@ AES
Absenderadresse
Absenderinformation
Absendername
ABGEKÜNDIGT
Admin
Adminbereich
Affirm
@@ -25,6 +26,7 @@ Ausgangsscans
ausgeklappt
ausgecheckt
auswahl
Auth
Authentication
Authenticator
Authentifizierungsmechanismus
@@ -36,6 +38,7 @@ Bancontact
BankID
Banking
barcodes
Baskisch
Bcc
BCC
Beispielevent
@@ -79,6 +82,7 @@ Connect
Consent
Copyleft
Cronjob
Cross
csv
Customer
CZK
@@ -277,6 +281,7 @@ Scopes
sechsstelligen
Secret
Security
Selling
SEPA
Shirts
Signaturverfahren

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:14+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -622,40 +622,40 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -686,40 +686,40 @@ msgstr ""
"Το χρώμα σας έχει κακή αντίθεση για κείμενο σε λευκό φόντο, επιλέξτε μια πιο "
"σκούρα σκιά."
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Όλα"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr "Κανένας"
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "Κάντε κλικ για να κλείσετε"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -622,40 +622,40 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,17 +7,17 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-07-19 08:56+0000\n"
"Last-Translator: Reece Needham <nouveaureece@protonmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2024-10-22 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/"
"pretix-js/es/>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.6.2\n"
"X-Generator: Weblate 5.7.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -576,10 +576,8 @@ msgid "Text object (deprecated)"
msgstr "Objeto de texto"
#: pretix/static/pretixcontrol/js/ui/editor.js:901
#, fuzzy
#| msgid "Text object"
msgid "Text box"
msgstr "Objeto de texto"
msgstr "Campo de texto"
#: pretix/static/pretixcontrol/js/ui/editor.js:903
msgid "Barcode area"
@@ -646,40 +644,40 @@ msgstr ""
"Tu color tiene mal contraste para un texto con fondo blanco, por favor, "
"escoge un tono más oscuro."
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr "Consultar búsqueda"
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Todos"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr "Ninguno"
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr "Solamente seleccionados"
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr "Introduce un número de página entre 1 y %(max)s."
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr "Número de página inválido."
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr "Usar un nombre diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr "¡Tienes cambios sin guardar!"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-08-29 10:36+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2024-09-06 08:47+0000\n"
"Last-Translator: Albizuri <oier@puntu.eus>\n"
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix-js/"
"eu/>\n"
@@ -557,17 +557,14 @@ msgid "absent"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:171
#: pretix/static/pretixcontrol/js/ui/editor.js:72
msgid "Check-in QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:543
#: pretix/static/pretixcontrol/js/ui/editor.js:387
msgid "The PDF background file could not be loaded for the following reason:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:894
#: pretix/static/pretixcontrol/js/ui/editor.js:656
msgid "Group of objects"
msgstr ""
@@ -580,44 +577,35 @@ msgid "Text box"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:903
#: pretix/static/pretixcontrol/js/ui/editor.js:663
msgid "Barcode area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:905
#: pretix/static/pretixcontrol/js/ui/editor.js:665
msgid "Image area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:907
#: pretix/static/pretixcontrol/js/ui/editor.js:667
msgid "Powered by pretix"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:909
#: pretix/static/pretixcontrol/js/ui/editor.js:669
msgid "Object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:913
#: pretix/static/pretixcontrol/js/ui/editor.js:673
msgid "Ticket design"
msgstr "Sarrera diseinua"
#: pretix/static/pretixcontrol/js/ui/editor.js:1250
#: pretix/static/pretixcontrol/js/ui/editor.js:972
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1319
#: pretix/static/pretixcontrol/js/ui/editor.js:1370
#: pretix/static/pretixcontrol/js/ui/editor.js:1041
#: pretix/static/pretixcontrol/js/ui/editor.js:1091
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1353
#: pretix/static/pretixcontrol/js/ui/editor.js:1074
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
@@ -634,66 +622,53 @@ msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:322
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:326
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:493
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Guztiak"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:494
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:498
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:828
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:831
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:989
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1029
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1104
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr ""
@@ -720,7 +695,7 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Zure saskiko artikuluak ez daude zuretzat erreserbatuta. Oraindik ere zure "
"Zure saskiko produktuak ez daude zuretzat erreserbatuta. Oraindik ere zure "
"eskaera bete dezakezu, baldin eta eskuragarri badaude."
#: pretix/static/pretixpresale/js/ui/cart.js:45
@@ -730,8 +705,9 @@ msgstr "Saskia iraungita"
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[0] "Zure saskiko produktuak minutu -ez erreserbatuta daude zuretzat."
msgstr[1] ""
"Zure saskiko produktuak {num} minutuz erreserbatuta daude zuretzat."
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
@@ -1099,7 +1075,3 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:89
msgid "December"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:661
msgid "Text object"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -651,40 +651,40 @@ msgid ""
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Kaikki"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr "Käytä toista nimeä sisäisesti"
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "Sulje klikkaamalla"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr "Sinulla on tallentamattomia muutoksia!"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2024-06-12 03:00+0000\n"
"Last-Translator: simonD <simon.dalvy@gmail.com>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -656,41 +656,41 @@ msgstr ""
"Votre choix de couleur n'a pas un bon contraste avec du texte sur un fond "
"blanc, SVP choisissez un ton plus sombre."
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr "Recherche"
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Tous"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr "Aucun"
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr "Seuls les sélectionnés"
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
#, fuzzy
msgid "Invalid page number."
msgstr "Numéro de page invalide."
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr "Utiliser un nom différent en interne"
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "Cliquez pour fermer"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr "Vous avez des modifications non sauvegardées !"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
"PO-Revision-Date: 2022-02-22 22:00+0000\n"
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -658,40 +658,40 @@ msgstr ""
"A túa cor ten mal contraste para un texto con fondo branco. Por favor, "
"escolle un ton máis escuro."
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:495
#: pretix/static/pretixcontrol/js/ui/main.js:515
msgid "Search query"
msgstr "Consultar unha procura"
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:513
msgid "All"
msgstr "Todos"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:514
msgid "None"
msgstr "Ningún"
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:518
msgid "Selected only"
msgstr "Soamente seleccionados"
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:861
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:864
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:1022
msgid "Use a different name internally"
msgstr "Usar un nome diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1062
msgid "Click to close"
msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1137
msgid "You have unsaved changes!"
msgstr "Tes cambios sen gardar!"

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