Compare commits

...

422 Commits

Author SHA1 Message Date
Raphael Michel
423e64a348 Bump to 4.5.2 2022-02-28 16:13:57 +01:00
Raphael Michel
69a32e2274 Pin django-countries to 7.2 2022-02-28 16:13:32 +01:00
Raphael Michel
25eaf8d625 [SECURITY] Fix stored XSS in help texts 2022-02-28 16:13:25 +01:00
Raphael Michel
ae21ad02a3 [SECURITY] Fix stored XSS in question errors 2022-02-28 16:10:58 +01:00
Raphael Michel
10c76c6391 [SECURITY] Prevent untrusted values from creating Excel formulas 2022-02-28 16:10:58 +01:00
Raphael Michel
e116299311 Bump to 4.5.1 2022-01-26 13:42:32 +01:00
Raphael Michel
6f14fb176e [SECURITY] Make redirect view dependent on referer 2022-01-26 13:41:13 +01:00
Raphael Michel
b03daab452 [SECURITY] Fix (non-exploitable) XSS issue 2022-01-26 13:41:13 +01:00
Raphael Michel
860cfc3227 Bump version to 4.5.0 2021-11-29 15:46:42 +01:00
pretix translation bot
45859a07dd Translations update from Weblate (#2352)
Co-authored-by: Raphael Michel <michel@rami.io>
2021-11-29 10:35:24 +01:00
dependabot[bot]
04fb8efc0d Update flake8 requirement from ==3.7.* to >=3.7,<4.1 in /src
Updates the requirements on [flake8](https://github.com/pycqa/flake8) to permit the latest version.
- [Release notes](https://github.com/pycqa/flake8/releases)
- [Commits](https://github.com/pycqa/flake8/compare/3.7.0...4.0.1)

---
updated-dependencies:
- dependency-name: flake8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-29 09:53:14 +01:00
Raphael Michel
fdb8a3720b Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-11-29 09:28:15 +01:00
Raphael Michel
5638d68894 Raise some dependencies 2021-11-29 09:27:24 +01:00
Raphael Michel
f64042280a Tighten dependency ranges 2021-11-29 09:27:24 +01:00
Angel Saiz Velasco
50060cdc8d Translations: Update Spanish
Currently translated at 66.7% (2992 of 4483 strings)

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

powered by weblate
2021-11-29 09:19:44 +01:00
Ismael Menéndez Fernández
4499f58e3d Translated on translate.pretix.eu (Galician)
Currently translated at 100.0% (172 of 172 strings)

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

powered by weblate
2021-11-29 09:19:44 +01:00
Ismael Menéndez Fernández
918e4a5a89 Translations: Update Galician
Currently translated at 0.7% (33 of 4483 strings)

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

powered by weblate
2021-11-29 09:19:44 +01:00
Ismael Menéndez Fernández
15a86fd796 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (172 of 172 strings)

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

powered by weblate
2021-11-29 09:19:44 +01:00
Ismael Menéndez Fernández
4126d20f1c Translations: Update Spanish
Currently translated at 66.6% (2986 of 4483 strings)

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

powered by weblate
2021-11-29 09:19:44 +01:00
Eva-Maria Obermann
ea3edf83f8 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (4483 of 4483 strings)

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

powered by weblate
2021-11-29 09:19:44 +01:00
Eva-Maria Obermann
9a42819b56 Translations: Update German
Currently translated at 100.0% (4483 of 4483 strings)

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

powered by weblate
2021-11-29 09:19:44 +01:00
Marco Giacopuzzi
3e4ba28700 Translated on translate.pretix.eu (Italian)
Currently translated at 100.0% (172 of 172 strings)

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

powered by weblate
2021-11-29 09:19:44 +01:00
Marco Giacopuzzi
9014ffcc28 Translations: Update Italian
Currently translated at 17.1% (770 of 4483 strings)

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

powered by weblate
2021-11-29 09:19:44 +01:00
Raphael Michel
48f4bcf88c Fix breaking multi-event exporters 2021-11-23 17:07:39 +01:00
Raphael Michel
b7dfb3697e Widget: Fix price box not shown for free-price events with one product 2021-11-23 11:13:09 +01:00
Richard Schreiber
475a5be351 Day calendar: Fix missing current-time-bar back for all browsers (#2342) 2021-11-22 15:12:51 +01:00
Richard Schreiber
8254d8f5cc Day-Calendar: improve width of row-names (#2341) 2021-11-22 15:09:40 +01:00
Raphael Michel
6f0f4755ef Restrict day calendar JS to day calendar page 2021-11-19 19:02:46 +01:00
Richard Schreiber
910a35dedc Fix: calculate day calendar grid in JS as chrome does not support calc-division in CSS-grid (#2340)
Co-authored-by: Raphael Michel <michel@rami.io>
2021-11-19 17:42:16 +01:00
Raphael Michel
e694bd8c21 Fix next crash in day calendar if there is no start time 2021-11-19 17:08:05 +01:00
Raphael Michel
29cf384c28 Fix crash in day calendar if there is no start time 2021-11-19 16:32:07 +01:00
Raphael Michel
492288f437 Allow customers to change add-ons on existing orders (#2283) 2021-11-19 14:59:54 +01:00
Raphael Michel
34e4f7e0fc Add day calendar to organizer page (#2100)
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-11-19 14:59:35 +01:00
Rasmus Kock Grusgaard
f6f3bbcce6 Translations: Update Danish
Currently translated at 35.9% (1613 of 4483 strings)

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

powered by weblate
2021-11-19 14:59:06 +01:00
Raphael Michel
16054893ed Avoid creation of manual payments with zero amount (#2325) 2021-11-19 12:02:36 +01:00
dependabot[bot]
f6038d2c39 Update django-statici18n requirement from ==1.9.* to >=1.9,<2.2 in /src (#2332)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 17:41:37 +01:00
dependabot[bot]
8d13b51271 Bump pycparser from 2.13 to 2.21 in /src (#2334)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 17:40:59 +01:00
Raphael Michel
83e1f365c2 Sendmail rules: Add warnings and scheduling view (#2328) 2021-11-18 12:48:27 +01:00
Raphael Michel
146e1aeb67 Upgrade mt-940 to 4.* (#2331) 2021-11-18 12:24:54 +01:00
dependabot[bot]
f9b2920984 Update libsass requirement from ==0.20.* to >=0.20,<0.22 in /src (#2315)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 12:14:24 +01:00
dependabot[bot]
2c01b214a7 Update pyflakes requirement from ==2.1.* to >=2.1,<2.5 in /src (#2313)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 12:14:01 +01:00
dependabot[bot]
fdab45e5ce Update bleach requirement from ==3.3.* to >=3.3,<4.2 in /src (#2317)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 12:13:17 +01:00
pretix translation bot
9d2cf18543 Translations update from Weblate (#2327)
Co-authored-by: +se <sebastiano@endsummercamp.org>
2021-11-18 12:12:42 +01:00
Martin Gross
2206ab1d35 Validate Swiss VAT ID against PROD and not TEST-env 2021-11-17 14:07:14 +01:00
Raphael Michel
ecd2c80dce Downgrade 'markdown' package (#2329) 2021-11-17 11:21:59 +01:00
Raphael Michel
3387df491a Fix error handling in Swiss VAT ID validation 2021-11-17 10:30:52 +01:00
pretix translation bot
b6974e0c77 Translations update from Weblate (#2319)
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
Co-authored-by: +se <sebastiano@endsummercamp.org>
2021-11-16 16:58:21 +01:00
Raphael Michel
31751cbd79 Stripe: Fix storage of failed refunds 2021-11-16 12:18:33 +01:00
Raphael Michel
993da5a392 VAT validation: Move cache to data directory 2021-11-16 10:21:08 +01:00
Richard Schreiber
72455209bb CSP: Strip keys with empty values from header (#2322) 2021-11-16 09:24:19 +01:00
Richard Schreiber
803aa0b70d Setup: Allow django-hijack v2.2 (#2321) 2021-11-16 09:24:06 +01:00
Bentrex95
954d86337c Docs: Fix typo in dev-setup-command (#2316) 2021-11-12 12:42:07 +01:00
Raphael Michel
38a58d62f3 Change default settings for background color, invoice attachmentes and name scheme (#2288) 2021-11-11 12:20:34 +01:00
Raphael Michel
e67b39a57b Increase padding if background color is set (#2301) 2021-11-11 12:20:20 +01:00
dependabot[bot]
148b67ac3f Update django-filter requirement from ==2.4.* to >=2.4,<21.2 in /src (#2311)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-11 11:14:35 +01:00
dependabot[bot]
d261cb3b6b Bump django-libsass from 0.8 to 0.9 in /src (#2312)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-11 11:14:07 +01:00
ser8phin
169a6c51b4 Add check to force users to change password (#2284) 2021-11-11 11:10:33 +01:00
Raphael Michel
245ad644ff Subevent calendar: Improve heuristic on when to show names (#2308) 2021-11-11 10:02:45 +01:00
Jaakko Rinta-Filppula
4fdce0d126 Translated on translate.pretix.eu (Finnish)
Currently translated at 50.0% (86 of 172 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/fi/

powered by weblate
2021-11-11 10:02:32 +01:00
Jaakko Rinta-Filppula
a542bc7a5a Translated on translate.pretix.eu (Finnish)
Currently translated at 19.0% (856 of 4483 strings)

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

powered by weblate
2021-11-11 10:02:32 +01:00
dependabot[bot]
3164919923 Update pytest-rerunfailures requirement from ==9.* to >=9,<11 in /src (#2303)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@rami.io>
2021-11-09 19:22:43 +01:00
dependabot[bot]
8085311eb6 Update django-localflavor requirement from ==3.0.* to >=3.0,<3.2 in /src (#2305)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 19:21:53 +01:00
dependabot[bot]
3887a65961 Update pytest-mock requirement from ==2.0.* to >=2.0,<3.7 in /src (#2302)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 19:20:47 +01:00
dependabot[bot]
b229c6156a Update chardet requirement from <3.1.0,>=3.0.2 to >=3.0.2,<4.1.0 in /src (#2304)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 19:20:40 +01:00
Raphael Michel
c45298544e Fix incorrect settings propagagion 2021-11-09 18:45:45 +01:00
Maarten van den Berg
7bb9d3fc3d Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (4482 of 4483 strings)

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

powered by weblate
2021-11-09 17:25:38 +01:00
Ismael Menéndez Fernández
8607df5a9c Translated on translate.pretix.eu (Galician)
Currently translated at 31.3% (54 of 172 strings)

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

powered by weblate
2021-11-09 17:25:38 +01:00
Ismael Menéndez Fernández
c4150473fc Translated on translate.pretix.eu (Galician)
Currently translated at 0.4% (20 of 4483 strings)

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

powered by weblate
2021-11-09 17:25:38 +01:00
Martin Gross
172b2f74e0 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4483 of 4483 strings)

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

powered by weblate
2021-11-09 17:25:38 +01:00
Svyatoslav
9586f71dc2 Translated on translate.pretix.eu (Latvian)
Currently translated at 24.0% (1077 of 4483 strings)

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

powered by weblate
2021-11-09 17:25:38 +01:00
Raphael Michel
25692d180f Make weblate script more robust 2021-11-09 16:34:57 +01:00
Raphael Michel
ae047037dc Docs: Add style guide for commit messages (#2281)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-11-09 16:30:32 +01:00
dependabot[bot]
265106034b Update django-otp requirement from ==0.7.*,>=0.7.5 to >=0.7,<1.2 in /src (#2290)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 12:00:15 +01:00
Raphael Michel
dd0a4df914 Fix error 500 on non-ASCII attachment file names 2021-11-09 11:55:03 +01:00
dependabot[bot]
b0ae40c264 Bump rollup from 1.32.1 to 2.59.0 in /src/pretix/static/npm_dir (#2298)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 11:54:28 +01:00
dependabot[bot]
ad95815043 Update redis requirement from ==3.4.* to >=3.4,<3.6 in /src (#2293)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 11:54:07 +01:00
dependabot[bot]
f68522ec0d Bump @babel/core from 7.13.14 to 7.16.0 in /src/pretix/static/npm_dir (#2297)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:51:38 +01:00
dependabot[bot]
b831e57351 Bump @rollup/plugin-node-resolve from 11.2.0 to 11.2.1 in /src/pretix/static/npm_dir (#2299)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:50:08 +01:00
dependabot[bot]
51166786ee Update phonenumberslite requirement from ==8.11.* to >=8.11,<8.13 in /src (#2291)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:49:52 +01:00
dependabot[bot]
909e7906ff Update sentry-sdk requirement from ==1.1.* to >=1.1,<1.5 in /src (#2292)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:47:55 +01:00
dependabot[bot]
e185d5f0e7 Bump @babel/preset-env from 7.13.12 to 7.16.0 in /src/pretix/static/npm_dir (#2295)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:46:21 +01:00
dependabot[bot]
ce8edf621b Bump vue and vue-template-compiler in /src/pretix/static/npm_dir (#2296)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-09 09:45:24 +01:00
Raphael Michel
e58b512876 Fix ordering of questions in backend if all system questions are 0 2021-11-09 09:44:44 +01:00
Raphael Michel
d1754f6d1b GitHub: Enable dependabot (#2289) 2021-11-09 09:43:52 +01:00
Raphael Michel
ff2f1b7424 Fix incorrect check for enabled fields in QuestionList 2021-11-09 09:32:52 +01:00
Raphael Michel
fb1838a2f0 Fix incorrect help text 2021-11-09 09:32:52 +01:00
Raphael Michel
d7b05063a4 Allow to print event location on invoices (#2278)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-11-05 09:47:41 +01:00
Raphael Michel
f64a42d61a Stripe: Fix handling of charges without source 2021-11-04 18:21:29 +01:00
Raphael Michel
c1994e89a5 Stripe: Fix MultipleObjectsReturned in webhook 2021-11-04 17:58:24 +01:00
Raphael Michel
f37de1ad2f Invoice renderer: Do not show end date if same as start date 2021-11-04 17:34:44 +01:00
Raphael Michel
e1ff6f8590 Stripe: Look up charges by their source ID as well 2021-11-04 17:20:45 +01:00
Raphael Michel
a5dd22eb4d Reduce number of global locks needed for confirming payments 2021-11-04 17:18:48 +01:00
Raphael Michel
19cde63505 Fix incorrect setting if Invoice.full_invoice_no 2021-11-04 13:48:39 +01:00
Raphael Michel
754d4f4f62 Sendmail: Fix subevent-less rules in event series 2021-11-04 10:21:03 +01:00
Bentrex95
e433230573 Docs: Update dependencies for dev setup (#2282)
Co-authored-by: Raphael Michel <michel@rami.io>
2021-11-03 12:35:26 +01:00
Julia Luna
f8927396d3 API: Add endpoints for automated email rules (#2178)
Co-authored-by: Raphael Michel <michel@rami.io>
2021-11-03 11:49:01 +01:00
Raphael Michel
60be99fbb2 Another attempt at correct sanitization of HTML in invoice content (#2279) 2021-11-03 11:13:43 +01:00
Raphael Michel
0c508c5ba4 Fix remaining DST error in auto check-out 2021-11-03 09:34:50 +01:00
Richard Schreiber
ea6067ab3f Fix Outlook >= 2010 trimming header image (#2277)
* fix image cutoff with mso-line-height: at-least
* align text to the left; fully centered text is hard to read
* remove mso cellpadding-tables as they double up the spacing
* additionally add background-color to a table with width=100% for broader support (e.g. Yahoo and AOL)
2021-11-02 12:59:09 +01:00
Raphael Michel
9d0fa84277 Add nodejs to update notes 2021-10-31 18:32:16 +01:00
Raphael Michel
a6835d3b14 Fix bug in 03de0d5d2 2021-10-31 18:26:45 +01:00
Raphael Michel
9ff565f772 Fix unreadable active tab 2021-10-31 17:28:35 +01:00
Raphael Michel
5d41b20bae Fix crash in waiting list 2021-10-31 17:28:29 +01:00
Raphael Michel
03de0d5d2e Do not ask authenticated customers to re-type their email address 2021-10-29 17:23:26 +02:00
Raphael Michel
2937acdc66 Bump to 4.5.0.dev0 2021-10-29 15:38:52 +02:00
Raphael Michel
6fd09e99e2 Bump version to 4.4.0 2021-10-29 15:38:52 +02:00
Raphael Michel
290e14689d Fix check_order_transactions on SQLite 2021-10-29 15:38:52 +02:00
Raphael Michel
89c937089b Translated on translate.pretix.eu (Galician)
Currently translated at 0.0% (0 of 4483 strings)

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

powered by weblate
2021-10-29 14:17:23 +02:00
Raphael Michel
0e02febe76 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4483 of 4483 strings)

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

powered by weblate
2021-10-29 14:17:23 +02:00
Raphael Michel
771f822e5f Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4483 of 4483 strings)

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

powered by weblate
2021-10-29 14:17:23 +02:00
Raphael Michel
e8936551c0 Extend spellcheck word list 2021-10-29 13:58:29 +02:00
Raphael Michel
ea0f6dfc54 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-10-29 12:10:08 +02:00
Raphael Michel
abeddd360e Invoices: Change expected behaviour for switches in numbering scheme 2021-10-29 12:09:09 +02:00
Maarten van den Berg
c209d195bf Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (172 of 172 strings)

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

powered by weblate
2021-10-29 10:24:22 +02:00
Maarten van den Berg
35c46d320c Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (4470 of 4474 strings)

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

powered by weblate
2021-10-29 10:24:22 +02:00
Raphael Michel
30621568ab Added translation on translate.pretix.eu (Galician) 2021-10-29 10:24:22 +02:00
Raphael Michel
403c4f4499 Add Galician as an incubating language 2021-10-29 10:23:57 +02:00
Raphael Michel
884bba0088 Fix transaction creation during split order creation 2021-10-29 10:21:37 +02:00
Raphael Michel
2b52edd5b7 Remove wrong optimization 2021-10-28 11:12:16 +02:00
Richard Schreiber
a4aed96784 Fix: add support for rtl-languages to checkout-step-bars 2021-10-27 16:16:04 +02:00
Raphael Michel
4bdfd56264 E-mail layout with logo: Make image display:block for outlook 2021-10-27 11:31:33 +02:00
Raphael Michel
31f0b07325 FIx typo causing test failure 2021-10-27 11:09:00 +02:00
pretix translation bot
3f08f3a7f4 Translations update from Weblate (#2266)
Co-authored-by: Tony Pavlik <kontakt@playton.cz>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2021-10-27 09:22:02 +02:00
Raphael Michel
93263e7567 money template filter: coerce None to 0.00 2021-10-26 18:07:37 +02:00
Raphael Michel
69cf62d2ca Fix missing or wrong create_transactions calls 2021-10-26 18:07:23 +02:00
Raphael Michel
bb353e5fde Improve detection of missing transactions 2021-10-26 18:06:49 +02:00
Raphael Michel
2dceff1218 Fix transaction creation issues and improve debugging 2021-10-26 11:33:44 +02:00
Raphael Michel
5ea8a8ef82 Ask and validate VAT IDs for Switzerland (#2259)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-10-26 11:20:45 +02:00
pretix translation bot
03a7a3303c Translations update from Weblate (#2264)
Co-authored-by: Tony Pavlik <kontakt@playton.cz>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Jacek Wielemborek <github@d33.pl>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
2021-10-26 11:19:45 +02:00
Raphael Michel
2beb0b20ca Check-in API: Work around libpretixsync issue with space encoding 2021-10-26 10:46:28 +02:00
Richard Schreiber
24eea02e0d API: sort ordered items’ answers by questions’ position (#2182) 2021-10-26 09:42:01 +02:00
Raphael Michel
15ab9c72d3 Invoice renderer: Reduce a few spacings 2021-10-22 13:10:48 +02:00
Raphael Michel
c957d77fe0 Fix linter issues 2021-10-22 12:58:45 +02:00
Raphael Michel
7697018ca4 Order JSON export: Add a lot more fields 2021-10-22 12:43:41 +02:00
Raphael Michel
3980a7b2a7 Docs: Fix missing files 2021-10-22 11:06:23 +02:00
pretix translation bot
035bb56386 Translations update from Weblate (#2254)
Co-authored-by: Tony Pavlik <kontakt@playton.cz>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Jacek Wielemborek <github@d33.pl>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2021-10-22 11:04:42 +02:00
Raphael Michel
837b03fff3 Add ugprade note to docs 2021-10-22 11:01:35 +02:00
Raphael Michel
d3dec72831 Add missing import 2021-10-22 10:26:10 +02:00
Raphael Michel
3d78f68d94 Docs: Add page on errors 2021-10-22 10:25:58 +02:00
Raphael Michel
faa43d4df8 Remove duplicate form field 2021-10-21 13:25:52 +02:00
Raphael Michel
78917afa1a Event settings API: Expose mail_days_order_expire_warning 2021-10-19 17:12:13 +02:00
Raphael Michel
4b53d39e3e Add debug command check_order_transactions 2021-10-19 17:10:08 +02:00
Raphael Michel
02db07cd25 Work around potential caching issue 2021-10-19 17:04:28 +02:00
Raphael Michel
19fb6c8c34 create_order_transactions: Make suitable for large datasets 2021-10-19 15:25:34 +02:00
Raphael Michel
0c25b2df92 Docs: Fix typo in index name 2021-10-19 15:25:15 +02:00
Raphael Michel
6a543e4557 Fix missing log message 2021-10-18 18:50:53 +02:00
Raphael Michel
846527546a Improve visual transaction table 2021-10-18 18:35:02 +02:00
Raphael Michel
c8cdb2b311 Log silent DirtyTransactionsForOrderException to sentry 2021-10-18 17:57:36 +02:00
Raphael Michel
96ff3d532d Fix logic error 2021-10-18 17:55:32 +02:00
Raphael Michel
8ebba9de86 Data model for transactional history (#2147) 2021-10-18 17:28:58 +02:00
Raphael Michel
c4e71011ee Update English wordlist 2021-10-18 13:24:38 +02:00
Raphael Michel
e71ad4bfba CSS: Always clear floats before drawing footer 2021-10-18 10:37:23 +02:00
Raphael Michel
05a5a69128 Lightbox: Remove .min.js and make dependency on gettext optional 2021-10-18 09:23:12 +02:00
Raphael Michel
bb83cd2f39 Fix duplicate margin 2021-10-17 19:16:19 +02:00
Raphael Michel
df26171ff1 Fix/Improve responsiveness of calendar pages 2021-10-17 18:55:01 +02:00
Raphael Michel
da937dc4e3 [a11y] Small fixes and improvements 2021-10-17 18:35:55 +02:00
Raphael Michel
bb9508ad96 Fix typo 2021-10-17 17:38:34 +02:00
Raphael Michel
41fed7d6a2 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 98.8% (170 of 172 strings)

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

powered by weblate
2021-10-17 17:37:17 +02:00
Raphael Michel
f441e9984d Translated on translate.pretix.eu (German)
Currently translated at 98.8% (170 of 172 strings)

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

powered by weblate
2021-10-17 17:37:17 +02:00
Raphael Michel
05c6155f37 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4474 of 4474 strings)

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

powered by weblate
2021-10-17 17:37:17 +02:00
Raphael Michel
3c096325bd Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4474 of 4474 strings)

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

powered by weblate
2021-10-17 17:37:17 +02:00
Raphael Michel
d06a352df5 Update wordlist 2021-10-17 17:36:58 +02:00
Raphael Michel
ba7b1bb89e Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-10-17 16:57:30 +02:00
Richard Schreiber
3dcfa57b70 A11y improvements (#2081)
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2021-10-17 16:56:16 +02:00
Raphael Michel
cc13ca1c3f Fix #2165 -- Idempotency key errors from Stripe 2021-10-15 12:01:58 +02:00
Raphael Michel
aac67ebf83 Refs #2165 -- Lock payment object while processing Stripe response 2021-10-15 11:57:40 +02:00
Raphael Michel
b51e1cfc6f Fix #2241 -- Display timezone for sale start 2021-10-15 11:46:45 +02:00
Raphael Michel
f0508cdcc3 Fix #2228 -- Date filter behavior in order data export 2021-10-15 11:46:45 +02:00
Raphael Michel
9ed2dc7b46 Add exporter for gift card transactions 2021-10-15 11:46:45 +02:00
Raphael Michel
0e568a3fca Translated on translate.pretix.eu (Spanish)
Currently translated at 67.8% (2995 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
ityd
7f3606ee81 Translated on translate.pretix.eu (Spanish)
Currently translated at 67.8% (2996 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
DJG Bayern
b22d43860a Translated on translate.pretix.eu (Japanese)
Currently translated at 2.9% (5 of 171 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
DJG Bayern
0f9b339f01 Translated on translate.pretix.eu (Japanese)
Currently translated at 0.1% (4 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
DJG Bayern
cd1e9c1740 Translated on translate.pretix.eu (Japanese)
Currently translated at 0.1% (2 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
Raphael Michel
aec1ce53fc Added translation on translate.pretix.eu (Japanese) 2021-10-15 11:18:45 +02:00
Raphael Michel
aae129be6a Added translation on translate.pretix.eu (Japanese) 2021-10-15 11:18:45 +02:00
Tony Pavlik
b906fe0fc3 Translated on translate.pretix.eu (Czech)
Currently translated at 8.8% (390 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
Adri
f0f1537e9c Translated on translate.pretix.eu (French)
Currently translated at 50.7% (2238 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
Raphael Michel
7b7e77d497 Subevent editor: Fix Quota.ignore_for_event_availability not being copied 2021-10-15 11:12:36 +02:00
Raphael Michel
9ac705cd88 Web check-in: Show subevent with check result 2021-10-14 18:48:19 +02:00
Richard Schreiber
01d9574ddf Fix #2244 -- Show products without category first on product-list (#2249) 2021-10-13 09:33:43 +02:00
Richard Schreiber
8121167d5e Control: Add drag and drop to sort categories and products (#2242)
* add drag and drop to categories

* add drag and drop to products

* add light grey background to dragged element

* add missing th, add sr-only desc of columns

* group up/down/move elements

* improve visualizing drag-area by dimming others

* change up/down-links to buttons in form-post

* limit sorting to POST requests

Co-authored-by: Raphael Michel <michel@rami.io>
2021-10-12 14:46:56 +02:00
Raphael Michel
dde4e12ce1 Fix bug in 6cd32400a 2021-10-11 17:36:57 +02:00
Raphael Michel
6cd32400ae Mails: Add elaborate retry logic for MS Exchange 2021-10-11 12:41:26 +02:00
Raphael Michel
8fa71ccad4 Show remaining quota on voucher redemption page 2021-10-08 18:08:28 +02:00
Raphael Michel
0f47bff5cd Allow to hide products that require membership (#2240)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-10-07 10:11:31 +02:00
Raphael Michel
f459f1f12d Fix logging error for automated emails 2021-10-07 10:08:30 +02:00
Richard Schreiber
65167cc290 Add new alert icons (#2226) 2021-10-06 12:31:08 +02:00
Raphael Michel
bc7300c393 Track if invoices have been sent via email (#2231) 2021-10-05 13:47:55 +02:00
Jaakko Rinta-Filppula
d8450202fe Translated on translate.pretix.eu (Finnish)
Currently translated at 50.2% (86 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/fi/

powered by weblate
2021-10-05 12:47:08 +02:00
Jaakko Rinta-Filppula
41d2bcc34f Translated on translate.pretix.eu (Finnish)
Currently translated at 19.3% (852 of 4413 strings)

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

powered by weblate
2021-10-05 12:47:08 +02:00
Fabian Rodriguez
0e1589013a Translated on translate.pretix.eu (French)
Currently translated at 63.7% (109 of 171 strings)

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

powered by weblate
2021-10-04 17:34:05 +02:00
cpoisnel
39f81617e1 Translated on translate.pretix.eu (French)
Currently translated at 63.7% (109 of 171 strings)

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

powered by weblate
2021-10-04 17:34:05 +02:00
cpoisnel
b394ef6de1 Translated on translate.pretix.eu (French)
Currently translated at 50.5% (2231 of 4413 strings)

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

powered by weblate
2021-10-04 17:34:05 +02:00
Raphael Michel
177906e2ac Custom order emails: Allow to attach tickets and invoices 2021-09-30 12:15:55 +02:00
Raphael Michel
59f6b20129 Add email placeholder {voucher_url_list} 2021-09-30 11:54:41 +02:00
Raphael Michel
51998e820d Orders API: Add item and variation filters 2021-09-30 11:48:23 +02:00
Raphael Michel
e803b56716 Bump to 4.4.0.dev0 2021-09-29 11:17:50 +02:00
Raphael Michel
fa8b1c176b Bump to 4.3.0 2021-09-29 11:17:12 +02:00
Richard Schreiber
2598787602 Customer profiles: add minor improvements around disabled fields and margins (#2195) 2021-09-29 10:34:45 +02:00
Raphael Michel
003fa62996 Fix help text 2021-09-28 18:07:34 +02:00
Raphael Michel
798c21955e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4413 of 4413 strings)

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

powered by weblate
2021-09-27 21:52:58 +02:00
Raphael Michel
fe6185af4b Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4413 of 4413 strings)

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

powered by weblate
2021-09-27 21:52:58 +02:00
Raphael Michel
7bacefa442 Update spelling wordlist 2021-09-27 21:47:05 +02:00
Raphael Michel
04e187c297 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-09-27 20:49:18 +02:00
Raphael Michel
9f2ffc3276 Improvements around the waiting list (#2219)
* Waiting list: Support for seated events, pre-fill customer email address

* Allow people to remove themselves

* Update src/pretix/base/settings.py

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

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

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

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

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

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

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

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

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

* Resolve a review note

* Review notes

* Fix linter issues

* Fix import

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-09-27 20:48:02 +02:00
Diego Rodrigo
a9a4cf6fca Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 14.5% (641 of 4403 strings)

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

powered by weblate
2021-09-27 20:26:20 +02:00
ofirtro
a563316e22 Translated on translate.pretix.eu (Hebrew)
Currently translated at 20.4% (35 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/he/

powered by weblate
2021-09-27 20:26:20 +02:00
ofirtro
4b6f55c31d Added translation on translate.pretix.eu (Hebrew) 2021-09-27 20:26:20 +02:00
Klevagruva
21a8fad17a Translated on translate.pretix.eu (Swedish)
Currently translated at 17.0% (749 of 4403 strings)

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

powered by weblate
2021-09-27 20:26:20 +02:00
Raphael Michel
7586df9d3f Docs: Fix note on widget on mobile 2021-09-27 11:34:55 +02:00
Raphael Michel
1d4afa5d27 Sendmail: Fix invalid state if attachment is adde then removed 2021-09-23 17:36:33 +02:00
Richard Schreiber
720d9b924e [Fix] on rtl-languages float productpicture to the right (#2224) 2021-09-23 17:22:50 +02:00
Raphael Michel
9f56669f2a Truelink filter: Allow dots with spaces 2021-09-23 09:50:39 +02:00
Richard Schreiber
fc541016c6 fix typo in logo-image-settings 2021-09-23 08:28:39 +02:00
Raphael Michel
5eefe9ad1e Fix linter issues 2021-09-20 16:51:48 +02:00
Raphael Michel
1d065a7672 Add setting organizer_logo_image_inherit 2021-09-17 13:33:34 +02:00
Raphael Michel
101f5f7781 Rewrite default ticket PDF to make sure caches affected by previous bug are cleaned 2021-09-17 11:55:53 +02:00
Raphael Michel
af7c6d360f Partially revert migration command monkeypatching 2021-09-17 11:07:46 +02:00
Raphael Michel
8751e6e5ba Product list: Show "sold out" before expanding variations 2021-09-17 10:20:43 +02:00
Raphael Michel
93004a8125 Customer detailv iew: Do not show names as "None" 2021-09-17 10:20:43 +02:00
Raphael Michel
adf40e1d56 Refactor our migrate command monkeypatching 2021-09-17 10:20:43 +02:00
Raphael Michel
364cfe0131 Update nginx config for static files 2021-09-17 10:20:43 +02:00
Raphael Michel
1514527ef3 Consistent naming 2021-09-17 10:20:43 +02:00
Tim Neumann
680024234d Dockerfile: Move nginx client_max_body_size to seperate file (#2207) 2021-09-16 12:36:25 +02:00
Richard Schreiber
2a3660f2d1 Fix -- copy answers even when matching customer profiles exist (#2209) 2021-09-16 10:07:43 +02:00
Richard Schreiber
2041d1213a fix address expand button submit-bug 2021-09-16 09:10:20 +02:00
Raphael Michel
42a1fe9bd1 Event settings API: Fix setting confirm_texts 2021-09-15 16:28:57 +02:00
Raphael Michel
002469d523 Fix .po file 2021-09-15 14:47:58 +02:00
pretix translation bot
5be4af1305 Translations update from Weblate (#2205)
* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (4403 of 4403 strings)

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

powered by weblate

* Translated on translate.pretix.eu (German (informal) (de_Informal))

Currently translated at 100.0% (4403 of 4403 strings)

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

powered by weblate

Co-authored-by: Raphael Michel <michel@rami.io>
2021-09-15 13:54:39 +02:00
Raphael Michel
0b241438e1 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-09-15 13:44:42 +02:00
Raphael Michel
61649ab2b8 Self-service cancellation: Allow to disable auto-refunds 2021-09-15 13:43:55 +02:00
Mohamed Tawfiq
848ea999c5 Translated on translate.pretix.eu (Arabic)
Currently translated at 96.4% (165 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ar/

powered by weblate
2021-09-15 13:43:52 +02:00
Mohamed Tawfiq
dfa82870fb Translated on translate.pretix.eu (Arabic)
Currently translated at 88.6% (3891 of 4391 strings)

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

powered by weblate
2021-09-15 13:43:52 +02:00
Mohamed Tawfiq
e05ac7ef34 Translated on translate.pretix.eu (Arabic)
Currently translated at 88.6% (3891 of 4391 strings)

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

powered by weblate
2021-09-15 13:43:52 +02:00
Raphael Michel
ad2334bffc WebHookCall: Increase max URL size 2021-09-15 13:06:25 +02:00
Raphael Michel
17adde99fa Allow to restrict availability of variations by date, sales channel, and voucher (#2202) 2021-09-15 12:04:17 +02:00
Raphael Michel
4789d82c4e Shredder: Fix crash in AttendeeInfoShredder 2021-09-14 15:28:39 +02:00
Raphael Michel
0567e2d22b API: Fix crash on missing require_membership_types property 2021-09-14 15:28:02 +02:00
Raphael Michel
2e0592b0a6 API: Fix crash on invalid input (PRETIXEU-5A9) 2021-09-14 15:02:06 +02:00
Mie Frydensbjerg
7f6d234b4c Translated on translate.pretix.eu (Danish)
Currently translated at 59.0% (101 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/da/

powered by weblate
2021-09-13 12:00:03 +02:00
Mie Frydensbjerg
0436de316b Translated on translate.pretix.eu (Danish)
Currently translated at 36.4% (1600 of 4391 strings)

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

powered by weblate
2021-09-13 12:00:03 +02:00
Klevagruva
e16d643d2a Translated on translate.pretix.eu (Swedish)
Currently translated at 16.6% (730 of 4391 strings)

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

powered by weblate
2021-09-13 12:00:03 +02:00
Raphael Michel
bdec22cf3b Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4391 of 4391 strings)

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

powered by weblate
2021-09-13 12:00:03 +02:00
Raphael Michel
b38df27dce Order import: Fix handling of seat IDs 2021-09-09 18:14:43 +02:00
Tim Neumann
b95f556d8f Add config options for max file upload sizes (#2199)
* feat(config): Add config options for max file upload sizes

Closes #2198

* Apply suggestions from code review

Fix docs and comment in settings.py

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

* Fix import order using isort

Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
2021-09-09 15:55:06 +02:00
Raphael Michel
851a4c977c Fix inconsistent handling of all_optional 2021-09-08 20:43:56 +02:00
Raphael Michel
7bffd461d1 Allow sales channels to opt out of customer accounts 2021-09-08 20:33:18 +02:00
Richard Schreiber
9a3b4f7863 Subevent: fix overflow for long lines in location 2021-09-08 13:22:57 +02:00
Raphael Michel
673a38ddc8 Cart: Display subevent location and end time in cart (#2191) 2021-09-08 11:24:39 +02:00
Richard Schreiber
a27b8bf213 Subevent: add missing verbose_name for seating plan (#2194) 2021-09-07 09:09:16 +02:00
Raphael Michel
36e6f10b37 Check-in list rule visualization: Fix broken height calculation 2021-09-06 22:35:14 +02:00
Raphael Michel
fde10d7f55 Fix missing license header 2021-09-06 21:14:25 +02:00
Raphael Michel
6b44b2f429 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4391 of 4391 strings)

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

powered by weblate
2021-09-06 21:14:11 +02:00
Raphael Michel
5e9018e0fd Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4391 of 4391 strings)

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

powered by weblate
2021-09-06 21:14:11 +02:00
Raphael Michel
185f8066ae Fix incorrect part of previous commit 2021-09-06 20:58:40 +02:00
Raphael Michel
6388f7b29c Fix #2192 -- Invoice address name-field always gets overwritten with customer profile 2021-09-06 20:57:45 +02:00
Raphael Michel
4aa2c9d51d Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-09-06 20:51:34 +02:00
Raphael Michel
ef9256f0b0 Fix typo in cache key 2021-09-06 20:50:47 +02:00
Raphael Michel
28d78e40f9 Allow to save invoice addresses and attendee profiles to customer account (#2084)
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-09-06 20:50:25 +02:00
Klevagruva
89554a82eb Translated on translate.pretix.eu (Swedish)
Currently translated at 16.2% (708 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Klevagruva
ae99e82ad1 Translated on translate.pretix.eu (Swedish)
Currently translated at 15.1% (663 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
rauxenz
5ea3d01b8d Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (171 of 171 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
rauxenz
aa2bd79b99 Translated on translate.pretix.eu (Spanish)
Currently translated at 68.2% (2977 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Niklas Forsström
44ee35b885 Translated on translate.pretix.eu (Swedish)
Currently translated at 14.9% (654 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Niklas Forsström
22b79a8c22 Translated on translate.pretix.eu (Swedish)
Currently translated at 14.7% (643 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Klevagruva
65bbd537e6 Translated on translate.pretix.eu (Swedish)
Currently translated at 14.7% (643 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Klevagruva
34387d7bc0 Translated on translate.pretix.eu (Swedish)
Currently translated at 11.1% (486 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
ityd
ca38204313 Translated on translate.pretix.eu (Spanish)
Currently translated at 68.0% (2969 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Niklas Forsström
b7083eca2e Translated on translate.pretix.eu (Swedish)
Currently translated at 10.3% (452 of 4364 strings)

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

powered by weblate
2021-09-06 20:18:55 +02:00
Raphael Michel
6bb8b428dc Stripe: API keys consistently are prefered over connect keys 2021-09-06 20:18:15 +02:00
Raphael Michel
677142d0c9 API: Fix storage of Item.picture 2021-09-06 19:56:57 +02:00
Raphael Michel
d1b66e365a API: Add test case for unsetting settings 2021-09-06 19:33:17 +02:00
Raphael Michel
50154c02ce Voucher: Add error message to form_invalid 2021-09-06 19:32:40 +02:00
Raphael Michel
04375d4fcf Fix voucher form validation (Z#2384192) 2021-09-06 19:32:15 +02:00
Raphael Michel
9c1ff296bb Add missing template 2021-09-06 16:33:41 +02:00
Raphael Michel
0b3acb06b5 Invoice: Fix incorrect reference to original invoice number 2021-09-06 16:33:27 +02:00
Raphael Michel
b2cdccedd6 Docker: Specify distribution of base image, upgrade to Python 3.9 2021-09-05 12:40:21 +02:00
Raphael Michel
7ebefa7b85 Allow to manually bump carts blocking a voucher 2021-08-30 15:57:28 +02:00
Raphael Michel
c7b5baa185 Widget: Only show new tab button on connection error 2021-08-30 15:49:22 +02:00
Raphael Michel
6d08e7a8b0 Docs: libmariadbclient-dev has been replaced by libmariadb-dev 2021-08-30 12:58:15 +02:00
Raphael Michel
0da2b12646 Check-in log exporter: Expose upload time 2021-08-30 12:44:25 +02:00
Raphael Michel
a0693483dc Bump to 4.3.0.dev0 2021-08-27 16:44:36 +02:00
Raphael Michel
29826a9f08 Bump to 4.2.0 2021-08-27 16:44:03 +02:00
Raphael Michel
36a045020f Fix typo check 2021-08-27 16:05:40 +02:00
Raphael Michel
40c2b774aa Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4364 of 4364 strings)

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

powered by weblate
2021-08-27 16:04:05 +02:00
Raphael Michel
8422b2b4aa Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4364 of 4364 strings)

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

powered by weblate
2021-08-27 16:04:05 +02:00
Raphael Michel
ae334c4860 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-08-27 15:05:38 +02:00
Raphael Michel
722f36121d Docs: Fix typo 2021-08-27 15:04:37 +02:00
Richard Schreiber
529092a4ed Order import: warn when char-replacement happens due to unknown encoding (#2184)
Co-authored-by: Raphael Michel <michel@rami.io>
2021-08-27 15:03:35 +02:00
Bruno
e7068020d5 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 14.1% (616 of 4357 strings)

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

powered by weblate
2021-08-27 15:03:19 +02:00
Raphael Michel
08acecf37b Docs: Add algorithm diagrams for checkin 2021-08-27 12:29:21 +02:00
Raphael Michel
b200ca5ad5 Set verbose name of Event.seating_plan 2021-08-24 12:25:42 +02:00
Raphael Michel
e564952148 Web check-in: Show ticket secret on result card 2021-08-24 12:05:45 +02:00
Maarten van den Berg
f4ad2a2293 Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))
Currently translated at 93.6% (4079 of 4357 strings)

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

powered by weblate
2021-08-24 09:00:42 +02:00
Maarten van den Berg
854bbf26c2 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4357 of 4357 strings)

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

powered by weblate
2021-08-24 09:00:42 +02:00
Raphael Michel
ec5a670ea6 PDF: Fix inheritance of answers to add-on products 2021-08-23 18:42:21 +02:00
Raphael Michel
276add9163 Refactor bulk-generation of voucher codes into utility function 2021-08-23 12:50:02 +02:00
Raphael Michel
de977f4818 event_copy_data Signal: Add quota_map argument 2021-08-23 12:03:03 +02:00
Raphael Michel
a4827fc992 Fix sorting of devices by descending name 2021-08-20 14:23:01 +02:00
Fabian
9a002bf172 Docs: no diff in mysql / pstgres in upgrade (#2180) 2021-08-19 12:37:48 +02:00
Raphael Michel
9a7f3e2d8a Stripe: Fix crash in shredder 2021-08-19 12:09:15 +02:00
Raphael Michel
e7546a7575 Manual refunds: Do not attempt to cancel if already canceled 2021-08-19 11:03:45 +02:00
Raphael Michel
434719285b Widget: Make "voucher required" clickable 2021-08-19 10:39:36 +02:00
Raphael Michel
5bc9ba4641 Deleting products: Catch ProtectedError 2021-08-19 10:20:01 +02:00
Raphael Michel
74dd13abd5 Fix unlimited quota issues 2021-08-18 18:40:33 +02:00
Raphael Michel
ead755aa86 Report canceled orders as underpaid if necessary 2021-08-18 17:36:36 +02:00
Raphael Michel
1f46a8b91b Remove print statement 2021-08-18 16:50:03 +02:00
Raphael Michel
eb77c2f6f6 API: Allow bulk-cration of cart positions 2021-08-18 15:28:41 +02:00
Raphael Michel
c5fe615be5 Sendmail: Force preview and show number of orders (#2099) 2021-08-18 15:28:33 +02:00
Raphael Michel
f5504e11ac Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (171 of 171 strings)

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

powered by weblate
2021-08-18 15:25:38 +02:00
Raphael Michel
e88a1a52f9 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (171 of 171 strings)

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

powered by weblate
2021-08-18 15:25:38 +02:00
Raphael Michel
b86d54ea9f Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4357 of 4357 strings)

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

powered by weblate
2021-08-18 15:25:38 +02:00
Raphael Michel
b6e2ed14db Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4357 of 4357 strings)

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

powered by weblate
2021-08-18 15:25:38 +02:00
nd
c513868afa Fix TypeError in Order.ticket_download_available on "empty" orders (#2176)
Co-authored-by: alice <tickets-dev@kulturkosmos.de>
Co-authored-by: nd <git@notandy.de>

Co-authored-by: alice <tickets-dev@kulturkosmos.de>
2021-08-17 23:42:36 +02:00
Raphael Michel
3f7664f743 Subevent bulk-creation: Fix flaky test (#2177) 2021-08-17 08:11:11 +02:00
Raphael Michel
e654b951ed Fix typo in docs 2021-08-16 17:55:33 +02:00
Raphael Michel
b5c7556abe Add FEE_TYPE_INSURANCE 2021-08-16 17:03:57 +02:00
Raphael Michel
53e3619140 Self-service cancellation: Never suggest cancellation fee higher than order total 2021-08-16 14:43:08 +02:00
Raphael Michel
e191988b81 Self-service cancellation: Do not allow to adjust fee on free orders 2021-08-16 14:40:47 +02:00
Raphael Michel
bb7fd9423b Invoice: Auto-resize font of meta data line 2021-08-16 13:07:07 +02:00
Raphael Michel
c10c6ee28d Subevent bulk creation: Resolve AmbiguousTimeError by preferring non-dst time 2021-08-16 13:07:07 +02:00
Raphael Michel
3c64733e93 Fix crash trying to thumbnail invalid image 2021-08-16 13:07:07 +02:00
dependabot[bot]
08cb045f2e Bump path-parse from 1.0.6 to 1.0.7 in /src/pretix/static/npm_dir
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-16 11:35:45 +02:00
Martin Gross
7bf854fe0b Add uninstall-handler for plugins 2021-08-16 11:35:27 +02:00
René Georgi
f2a1e11b85 Translated on translate.pretix.eu (German)
Currently translated at 99.8% (4349 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
amandajurno
9b07912b7f Translated on translate.pretix.eu (Portuguese)
Currently translated at 4.2% (186 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
amandajurno
e1cec9882a Translated on translate.pretix.eu (Portuguese)
Currently translated at 4.2% (185 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Alberto Leoncio
1ff9c1a84b Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 12.7% (556 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Michael
0035825f33 Translated on translate.pretix.eu (Czech)
Currently translated at 99.4% (170 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/cs/

powered by weblate
2021-08-15 19:11:03 +02:00
Michael
1ec73b1b33 Translated on translate.pretix.eu (Czech)
Currently translated at 4.0% (177 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Svyatoslav
ed83f4558e Translated on translate.pretix.eu (Russian)
Currently translated at 58.4% (100 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ru/

powered by weblate
2021-08-15 19:11:03 +02:00
Svyatoslav
b18ec7605a Translated on translate.pretix.eu (Russian)
Currently translated at 25.2% (1102 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Maarten van den Berg
f96bc0776d Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))
Currently translated at 100.0% (171 of 171 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl_Informal/

powered by weblate
2021-08-15 19:11:03 +02:00
Maarten van den Berg
629bdcd55d Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))
Currently translated at 91.5% (3989 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Maarten van den Berg
829fd907a1 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (171 of 171 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Abdullah
d7fe321f36 Translated on translate.pretix.eu (Arabic)
Currently translated at 89.3% (3891 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Maarten van den Berg
517432319e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4357 of 4357 strings)

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

powered by weblate
2021-08-15 19:11:03 +02:00
Richard Schreiber
edef9f1b23 Control: fix typo to correctly initialize basket’s reservation_time input 2021-08-12 12:39:41 +02:00
Martin Gross
617730ab76 Update Smallscale Docker Docs: bind exposed ports to 127.0.0.1 explicitly 2021-08-10 10:47:14 -04:00
Martin Gross
7c17d041f4 Fix wrong pretix.event.deleted webhook-label 2021-08-10 09:29:58 -04:00
Richard Schreiber
9295abb80e Fix item’s panel-title in checkout questions: move variation to name (#2172) 2021-08-09 15:13:19 +02:00
Richard Schreiber
4c3192f116 Payment availability-check: ignore country if no invoice address given (#2171) 2021-08-09 15:11:49 +02:00
Richard Schreiber
9c6a2eb85a Subevent bulk edit: check if list formset has changed only if not None (#2173) 2021-08-09 15:10:55 +02:00
Martin Gross
bcbc8a542f Update po files
[CI skip]

Signed-off-by: Martin Gross <gross@rami.io>
2021-08-04 13:18:45 -04:00
pretix translation bot
a915442efc Translations update from Weblate (#2167)
* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (4351 of 4351 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch)

Currently translated at 100.0% (170 of 170 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))

Currently translated at 91.7% (3991 of 4351 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Dutch (informal) (nl_Informal))

Currently translated at 78.8% (134 of 170 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/nl_Informal/

powered by weblate

Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2021-08-04 13:17:07 -04:00
Richard Schreiber
103631a14b Bundled items: improve label for quantity (#2166)
The label was „Number“, which is rather ambiguous. Changed it to „Quantity“.
2021-08-03 11:24:38 +02:00
Maarten van den Berg
e9d7a24cbf Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4351 of 4351 strings)

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

powered by weblate
2021-08-02 19:00:31 +02:00
Maarten van den Berg
cc977e441a Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (4347 of 4351 strings)

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

powered by weblate
2021-08-02 19:00:31 +02:00
Björn Out
add9bae018 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (4347 of 4351 strings)

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

powered by weblate
2021-08-02 19:00:31 +02:00
Hans Fraiponts
77d157ab8e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (170 of 170 strings)

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

powered by weblate
2021-08-02 19:00:31 +02:00
Raphael Michel
e42bc94329 Remove print statement 2021-08-01 17:39:17 +02:00
Raphael Michel
b4bf5f998e Check-in API: Recover from IntegrityError when saving questions 2021-07-31 11:14:36 +02:00
Raphael Michel
dc785e9dac Fix failing tests 2021-07-30 17:34:07 +02:00
Raphael Michel
8f5f95b04e nginx: Do not attempt to parse and rewrite our URLs, please 2021-07-30 16:46:03 +02:00
Raphael Michel
c86839ed41 Translated on translate.pretix.eu (Russian)
Currently translated at 25.3% (1102 of 4351 strings)

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

powered by weblate
2021-07-26 17:23:38 +02:00
Björn Out
8b6e0f0de7 Translated on translate.pretix.eu (Dutch)
Currently translated at 98.2% (167 of 170 strings)

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

powered by weblate
2021-07-26 17:23:38 +02:00
Björn Out
a65243e4bb Translated on translate.pretix.eu (Dutch)
Currently translated at 99.8% (4344 of 4351 strings)

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

powered by weblate
2021-07-26 17:23:38 +02:00
Svyatoslav
ac028be84e Translated on translate.pretix.eu (Russian)
Currently translated at 25.3% (1103 of 4351 strings)

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

powered by weblate
2021-07-26 17:23:38 +02:00
Raphael Michel
efd5b5b1da Thumbnailer: Do not screw with image modes 2021-07-26 17:08:26 +02:00
Raphael Michel
4be618bc93 Thumbnailing: Redefine min-size as exact-size 2021-07-26 15:53:21 +02:00
Raphael Michel
7b6d5a0cc9 Exporters: Allow extended XSLX formatting 2021-07-24 17:35:33 +02:00
Raphael Michel
f367d5e675 Multi-event exports: Pass organizer, fix in staff mode 2021-07-24 16:46:37 +02:00
Raphael Michel
f9b7894c4d Add tab-content stylesheet to frontend 2021-07-24 12:51:21 +02:00
Raphael Michel
354bbb485b Pagination: Add intcomma to number of results 2021-07-24 12:51:21 +02:00
Raphael Michel
8dc5dbd547 PDF: Add placehodler for order code + positionid 2021-07-23 19:29:27 +02:00
Raphael Michel
e04793d2eb pretixSCAN: Add new security profile PretixScanNoSyncSecurityProfile 2021-07-23 18:48:40 +02:00
Raphael Michel
db65c14733 Fix style check failures 2021-07-23 18:14:52 +02:00
Raphael Michel
f10c8b229f Check-in: Ticket should not count as redeemed if only exits have been scanned so far 2021-07-23 11:15:35 +02:00
Raphael Michel
4655d8237f Check-in API: Improve handling of unknown ticket codes 2021-07-23 10:49:33 +02:00
Raphael Michel
78f4f35ca3 Add documentation on certificates plugin 2021-07-21 18:00:33 +02:00
Raphael Michel
3a01a05a08 Check-in history: Fix incorrectly linked product 2021-07-21 10:17:18 +02:00
dedecosta
1738c710cb Translated on translate.pretix.eu (Italian)
Currently translated at 16.5% (720 of 4351 strings)

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

powered by weblate
2021-07-21 09:47:27 +02:00
dedecosta
d07783a453 Translated on translate.pretix.eu (Italian)
Currently translated at 100.0% (170 of 170 strings)

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

powered by weblate
2021-07-21 09:47:27 +02:00
dedecosta
1ce331f163 Translated on translate.pretix.eu (Italian)
Currently translated at 16.5% (720 of 4351 strings)

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

powered by weblate
2021-07-21 09:47:27 +02:00
Abdullah
586f95bc6d Translated on translate.pretix.eu (Arabic)
Currently translated at 89.4% (3892 of 4351 strings)

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

powered by weblate
2021-07-21 09:47:27 +02:00
Frank
5620aec5f2 Translated on translate.pretix.eu (Italian)
Currently translated at 14.4% (629 of 4351 strings)

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

powered by weblate
2021-07-21 09:47:27 +02:00
Martin Gross
c1dfec20f6 Always register ApplePay domains in livemode (#2154) 2021-07-21 09:41:57 +02:00
Raphael Michel
7fef81bdef Add fix and text for 0f2e90567 2021-07-20 18:14:59 +02:00
Raphael Michel
0f2e905672 Allow to remove seats that have canceled tickets 2021-07-20 17:17:26 +02:00
Raphael Michel
a57a4e7350 Subevent filter: Filter by start time 2021-07-20 09:28:15 +02:00
Raphael Michel
b57a6e982a Order import: Ignore completely empty lines 2021-07-20 08:55:07 +02:00
Raphael Michel
39736ef0d4 Check-in list: Allow to filter by date 2021-07-19 14:27:23 +02:00
Raphael Michel
f7e5f0b567 Order overview: Adjust to new filter style 2021-07-19 13:58:45 +02:00
Raphael Michel
b6078d5272 Allow to filter and sort the list of devices 2021-07-19 13:57:17 +02:00
Raphael Michel
1ed1cd33e8 Consistent styling for filter forms 2021-07-19 13:40:44 +02:00
Raphael Michel
a4a2500725 Subevent list: Allow to filter by time of day or multiple week days 2021-07-19 13:10:21 +02:00
Raphael Michel
3fb44ec9dd Widget: Fix crash if availability is unknown 2021-07-16 13:49:53 +02:00
Raphael Michel
2a96575b4d Event-level domains: Fix customer logout 2021-07-16 13:30:21 +02:00
Raphael Michel
dcf29ec63e Add workaround for issue with "back" button in Safari 2021-07-15 18:38:16 +02:00
Raphael Michel
a743605bd3 Stats: Allow queryset of subevents 2021-07-15 13:21:36 +02:00
Raphael Michel
75dc80eb09 API: Allow to set a custom pagination size 2021-07-13 18:20:12 +02:00
Richard Schreiber
ac16d9d900 Event calendar: Fix duplicate hidden inputs in week select form (#2152) 2021-07-12 16:12:07 +02:00
Raphael Michel
736d26c232 Order import: Fix redirect after import 2021-07-12 16:03:12 +02:00
Richard Schreiber
8985dfc5eb Fix #2150 -- Do not expose internal product names in modify view (#2151) 2021-07-12 15:55:32 +02:00
Martin Gross
bb80ef067a Fix nodejs doku link 2021-07-12 15:09:16 +02:00
Richard Schreiber
bdd9751f0e Widget: remove limit of 50 events in list-view by adding a load-more-button (#2144) 2021-07-10 21:37:27 +02:00
Raphael Michel
965aac6ad5 Add postfix to spellcheck list 2021-07-09 12:01:18 +02:00
Raphael Michel
e3858373d1 Fix missing Event.save() call after setting default plugins 2021-07-09 12:01:18 +02:00
Raphael Michel
fcdfae88d7 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4351 of 4351 strings)

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

powered by weblate
2021-07-09 11:54:40 +02:00
Raphael Michel
7d5a85e26f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4351 of 4351 strings)

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

powered by weblate
2021-07-09 11:54:40 +02:00
Raphael Michel
b8b2c2eba3 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-07-09 11:37:19 +02:00
Raphael Michel
c6a3280d69 Improve order confirmation message for free orders requiring approval 2021-07-09 11:34:36 +02:00
Raphael Michel
7f9368c415 QuestionStep: Correctly apply order of "initial" values 2021-07-08 09:28:36 +02:00
Raphael Michel
add764e3f0 Update django-i18nfield to 1.9.3 2021-07-06 11:28:45 +02:00
Raphael Michel
a3431cd51e Update django-i18nfield to 1.9.2 2021-07-06 10:31:04 +02:00
Martin Gross
9772d43235 Move PayPal postfix to end of settings list 2021-07-05 15:33:33 +02:00
Richard Schreiber
2e29e369f5 Fix celery progress: assign state/info once to avoid race-conditions (#2142) 2021-07-05 15:09:47 +02:00
Raphael Michel
9f6ce81229 Order import: Support phone number 2021-07-05 10:12:06 +02:00
Martin Gross
d67954de3f Extend WebHook URL to 255 chars (was: 200) 2021-07-05 09:56:50 +02:00
Martin Gross
d04f93d45c Revert the revertion + Fix "PayPal: Add postfix-field and pass information to custom-field" 2021-07-02 13:58:03 +02:00
Raphael Michel
ef70209ba8 Revert "PayPal: Add postfix-field and pass information to custom-field (#2137)"
This reverts commit 32f690e9d0.
2021-07-02 13:50:01 +02:00
Raphael Michel
f127cfc46a Dockerfile: Fix pretix version not known to pip 2021-07-02 10:07:49 +02:00
Raphael Michel
ec444e5bf3 Invoice: Show preview in browser during rebugging 2021-07-02 10:04:30 +02:00
Martin Gross
32f690e9d0 PayPal: Add postfix-field and pass information to custom-field (#2137) 2021-07-02 09:44:38 +02:00
Raphael Michel
9089b630ed Add new settings invoice_regenerate_allowed (#2071) 2021-07-01 14:51:08 +02:00
Richard Schreiber
0c6971ff5f Email: make responsive and show header image in MS Outlook (#2138) 2021-07-01 11:49:30 +02:00
Raphael Michel
59e92245de Merge branch 'release/4.1.x' 2021-06-30 16:39:42 +02:00
Raphael Michel
9894954233 Add static files to check-manifest ignore list 2021-06-30 16:14:27 +02:00
Raphael Michel
6e7505abd5 Add tests/plugins/sendmail to check-manifest ignore 2021-06-30 15:38:31 +02:00
Raphael Michel
9df381ec4c Do not require requirements.txt in GitLab CI 2021-06-30 15:26:53 +02:00
Raphael Michel
be726183cb Bump to 4.2.0.dev0 2021-06-30 15:15:27 +02:00
409 changed files with 236971 additions and 105966 deletions

15
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip"
directory: "/src"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/src/pretix/static/npm_dir"
schedule:
interval: "monthly"

View File

@@ -5,8 +5,8 @@ tests:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
- cd src
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- python manage.py check
- make all compress
- py.test --reruns 3 -n 3 tests
@@ -21,8 +21,8 @@ pypi:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools check-manifest twine
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
- cd src
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- python setup.py sdist
- pip install dist/pretix-*.tar.gz
- python -m pretix migrate

View File

@@ -1,9 +1,9 @@
FROM python:3.8
FROM python:3.9-bullseye
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
default-libmysqlclient-dev \
libmariadb-dev \
gettext \
git \
libffi-dev \
@@ -15,8 +15,7 @@ RUN apt-get update && \
libxslt1-dev \
locales \
nginx \
python-dev \
python-virtualenv \
python3-virtualenv \
python3-dev \
sudo \
supervisor \
@@ -57,10 +56,11 @@ COPY deployment/docker/supervisord /etc/supervisord
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
COPY deployment/docker/supervisord.web.conf /etc/supervisord.web.conf
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
COPY deployment/docker/nginx-max-body-size.conf /etc/nginx/conf.d/nginx-max-body-size.conf
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY src /pretix/src
RUN cd /pretix/src && pip3 install .
RUN cd /pretix/src && python setup.py install
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \

View File

@@ -0,0 +1 @@
client_max_body_size 100M;

View File

@@ -16,7 +16,6 @@ http {
charset utf-8;
tcp_nopush on;
tcp_nodelay on;
client_max_body_size 100M;
log_format private '[$time_local] $host "$request" $status $body_bytes_sent';
@@ -66,9 +65,18 @@ http {
access_log off;
expires 365d;
add_header Cache-Control "public";
add_header Access-Control-Allow-Origin "*";
gzip on;
}
location / {
proxy_pass http://unix:/tmp/pretix.sock:/;
# Very important:
# proxy_pass http://unix:/tmp/pretix.sock:;
# is not the same as
# proxy_pass http://unix:/tmp/pretix.sock:/;
# In the latter case, nginx will apply its URL parsing, in the former it doesn't.
# There are situations in which pretix' API will deal with "file names" containing %2F%2F, which
# nginx will normalize to %2F, which can break ticket validation.
proxy_pass http://unix:/tmp/pretix.sock:;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}

View File

@@ -434,3 +434,19 @@ pretix can make use of some external tools if they are installed. Currently, the
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html
Maximum upload file sizes
-------------------------
You can configure the maximum file size for uploading various files::
[pretix_file_upload]
; Max upload size for images in MiB, defaults to 10 MiB
max_size_image = 12
; Max upload size for favicons in MiB, defaults to 1 MiB
max_size_favicon = 2
; Max upload size for email attachments in MiB, defaults to 10 MiB
max_size_email_attachment = 15
; Max upload size for other files in MiB, defaults to 10 MiB
; This includes all file upload type order questions
max_size_other = 100

40
doc/admin/errors.rst Normal file
View File

@@ -0,0 +1,40 @@
.. _`admin-errors`:
Dealing with errors
===================
If you encounter an error in pretix, please follow the following steps to debug it:
* If the error message is shown on a **white page** and the last line of the error includes "nginx", the error is not with pretix
directly but with your nginx webserver. This might mean that pretix is not running, but it could also be something else.
Please first check your nginx error log. The default location is ``/var/log/nginx/error.log``.
* If it turns out pretix is not running, check the output of ``docker logs pretix`` for a docker installation and
``journalctl -u pretix-web.service`` for a manual installation.
* If the error message is an "**Internal Server Error**" in purple pretix design, please check pretix' log file which by default is at
``/var/pretix-data/logs/pretix.log`` if you installed with docker and ``/var/pretix/data/logs/pretix.log`` otherwise. If you don't
know how to interpret it, open a discussion on GitHub with the relevant parts of the log file.
* If the error message includes ``/usr/bin/env: node: No such file or directory``, you forgot to install ``node.js``
* If the error message includes ``OfflineGenerationError``, you might have forgot to run the ``rebuild`` step after a pretix update
or plugin installation.
* If the error message mentions your database server or redis server, make sure these are running and accessible.
* If pretix loads fine but certain actions (creating carts, orders, or exports, downloading tickets, sending emails) **take forever**,
``pretix-worker`` is not running. Check the output of ``docker logs pretix`` for a docker installation and
``journalctl -u pretix-worker.service`` for a manual installation.
* If the page loads but all **styles are missing**, you probably forgot to update your nginx configuration file after an upgrade of your
operating system's python version.
If you are unable to debug the issue any further, please open a **discussion** on GitHub in our `Q&A Forum`_. Do **not** open an issue
right away, since most things turn out not to be a bug in pretix but a mistake in your server configuration. Make sure to include
relevant log excerpts in your question.
If you're a pretix Enterprise customer, you can also reach out to support@pretix.eu with your issue right away.
.. _Q&A Forum: https://github.com/pretix/pretix/discussions/categories/q-a

View File

@@ -9,7 +9,9 @@ This documentation is for everyone who wants to install pretix on a server.
:maxdepth: 2
installation/index
updates
config
maintainance
scaling
errors
indexes

View File

@@ -50,7 +50,7 @@ Here is the currently recommended set of commands::
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_name
ON pretixbase_orderposition
USING gin (upper("attendee_name_cached") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_scret
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_secret
ON pretixbase_orderposition
USING gin (upper("secret") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email

View File

@@ -39,6 +39,10 @@ Linux and firewalls, we recommend that you start with `ufw`_.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
.. warning:: By default, using `ufw` in conjunction will not have any effect. Please make sure to either bind the exposed
ports of your docker container explicitly to 127.0.0.1 or configure docker to respect any set up firewall
rules.
On this guide
-------------
@@ -183,7 +187,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStart=/usr/bin/docker run --name %n -p 8345:80 \
ExecStart=/usr/bin/docker run --name %n -p 127.0.0.1:8345:80 \
-v /var/pretix-data:/data \
-v /etc/pretix:/etc/pretix \
-v /var/run/redis:/var/run/redis \
@@ -233,7 +237,7 @@ The following snippet is an example on how to configure a nginx proxy for pretix
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8345/;
proxy_pass http://localhost:8345;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
@@ -252,6 +256,8 @@ create an event and start selling tickets!
You should probably read :ref:`maintainance` next.
.. _`docker_updates`:
Updates
-------
@@ -267,6 +273,8 @@ Restarting the service can take a few seconds, especially if the update requires
Replace ``stable`` above with a specific version number like ``1.0`` or with ``latest`` for the development
version, if you want to.
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
.. _`docker_plugininstall`:
Install a plugin

View File

@@ -25,7 +25,7 @@ installation guides):
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 9.6+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
* A `redis`_ server
* A `nodejs_` installation
* A `nodejs`_ installation
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
Linux and firewalls, we recommend that you start with `ufw`_.
@@ -72,7 +72,7 @@ To build and run pretix, you will need the following debian packages::
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
gettext libpq-dev libmariadbclient-dev libjpeg-dev libopenjp2-7-dev
gettext libpq-dev libmariadb-dev libjpeg-dev libopenjp2-7-dev
Config file
-----------
@@ -237,7 +237,7 @@ The following snippet is an example on how to configure a nginx proxy for pretix
add_header X-Content-Type-Options nosniff;
location / {
proxy_pass http://localhost:8345/;
proxy_pass http://localhost:8345;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
@@ -280,13 +280,14 @@ create an event and start selling tickets!
You should probably read :ref:`maintainance` next.
.. _`manual_updates`:
Updates
-------
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
To upgrade to a new pretix release, pull the latest code changes and run the following commands (again, replace
``postgres`` with ``mysql`` if necessary)::
To upgrade to a new pretix release, pull the latest code changes and run the following commands::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
@@ -295,6 +296,7 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
.. _`manual_plugininstall`:

View File

@@ -9,6 +9,8 @@ If you host your own pretix instance, you also need to care about the availabili
of your service and the safety of your data yourself. This page gives you some
information that you might need to do so properly.
.. _`backups`:
Backups
-------

51
doc/admin/updates.rst Normal file
View File

@@ -0,0 +1,51 @@
.. _`update_notes`:
Update notes
============
pretix receives regular feature and bugfix updates and we highly encourage you to always update to
the latest version for maximum quality and security. Updates are announces on our `blog`_. There are
usually 10 feature updates in a year, so you can expect a new release almost every month.
Pure bugfix releases are only issued in case of very critical bugs or security vulnerabilities. In these
case, we'll publish bugfix releases for the last three stable release branches.
Compatibility to plugins and in very rare cases API clients may break. For in-depth details on the
API changes of every version, please refer to the release notes published on our blog.
Upgrade steps
-------------
For the actual upgrade, you can usually just follow the steps from the installation guide for :ref:`manual installations <manual_updates>`
or :ref:`docker installations <docker_updates>` respectively.
Generally, it is always strongly recommended to perform a :ref:`backup <backups>` first.
It is possible to skip versions during updates, although we recommend not skipping over major version numbers
(i.e. if you want to go from 2.4 to 4.4, first upgrade to 3.0, then upgrade to 4.0, then to 4.4).
In addition to these standard update steps, the following list issues steps that should be taken when you upgrade
to specific versions for pretix. If you're skipping versions, please read the instructions for every version in
between as well.
Upgrade to 3.17.0 or newer
""""""""""""""""""""""""""
pretix 3.17 introduces a dependency on ``nodejs``, so you should install it on your system::
# apt install nodejs npm
Upgrade to 4.4.0 or newer
"""""""""""""""""""""""""
pretix 4.4 introduces a new data structure to store historical financial data. If you already have existing
data in your database, you will need to back-fill this data or you might get incorrect reports! This is not
done automatically as part of the usual update steps since it can take a while on large databases and you might
want to do it in parallel while the system is already running again. Please execute the following command::
(venv)$ python -m pretix create_order_transactions
Or, with a docker installation::
$ docker exec -it pretix.service pretix create_order_transactions
.. _blog: https://pretix.eu/about/en/blog/

View File

@@ -87,7 +87,8 @@ respectively, or ``null`` if there is no such page. You can use those URLs to re
respective page.
The field ``results`` contains a list of objects representing the first results. For most
objects, every page contains 50 results.
objects, every page contains 50 results. You can specify a lower pagination size using the
``page_size`` query parameter, but no more than 50.
Conditional fetching
--------------------

View File

@@ -243,6 +243,99 @@ Cart position endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
order.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/bulk_create/
Creates multiple new cart position. This operation is deliberately not atomic, so each cart position can succeed
or fail individually, so the response code of the response is not the only thing to look at!
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
.. warning:: The same limitations as with the regular creation endpoint apply.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/bulk_create/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
[
{
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name_parts": {
"given_name": "Peter",
"family_name": "Miller"
},
"attendee_email": null,
"answers": [
{
"question": 1,
"answer": "23",
"options": []
}
],
"subevent": null
},
{
"item": 1,
"variation": null,
"price": "23.00",
"attendee_name_parts": {
"given_name": "Maria",
"family_name": "Miller"
},
"attendee_email": null,
"answers": [
{
"question": 1,
"answer": "23",
"options": []
}
],
"subevent": null
}
]
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"results": [
{
"success": true,
"errors": null,
"data": {
"id": 1,
...
},
},
{
"success": "false",
"errors": {
"non_field_errors": ["There is not enough quota available on quota \"Tickets\" to perform the operation."]
},
"data": null
}
]
}
:param organizer: The ``slug`` field of the organizer of the event to create positions for
:param event: The ``slug`` field of the event to create positions for
:statuscode 200: See response for success
:statuscode 400: Your input could not be parsed
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
order.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/(id)/
Deletes a cart position, identified by its internal ID.

View File

@@ -604,6 +604,8 @@ Order position endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position or check-in list does not exist.
.. _`rest-checkin-redeem`:
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)/redeem/
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
@@ -618,8 +620,9 @@ Order position endpoints
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
returned. Otherwise, canceled orders will return ``unpaid``.
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
questions that have not been filled. Defaults to ``false``.
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
questions that have not been filled. This is usually used to upload offline scans that already happened,
because there's no point in validating them since they happened whether they are valid or not. Defaults to ``false``.
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in

View File

@@ -31,5 +31,6 @@ Resources and endpoints
webhooks
seatingplans
exporters
sendmail_rules
billing_invoices
billing_var

View File

@@ -78,6 +78,12 @@ lines list of objects The actual invo
an event series not created by a product (e.g. shipping or
cancellation fees) as well as whenever the respective (sub)event
has no end date set.
├ event_location string Location of the (sub)event this line was created for as it
was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in
an event series not created by a product (e.g. shipping or
cancellation fees) as well as whenever the respective (sub)event
has no location set.
├ attendee_name string Attendee name at time of invoice creation. Can be ``null`` if no
name was set or if names are configured to not be added to invoices.
├ gross_value money (string) Price including taxes
@@ -110,6 +116,10 @@ internal_reference string Customer's refe
The attributes ``fee_type`` and ``fee_internal_type`` have been added.
.. versionchanged:: 4.1
The attribute ``lines.event_location`` has been added.
Endpoints
---------
@@ -179,6 +189,7 @@ Endpoints
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"event_location": "Heidelberg",
"attendee_name": null,
"gross_value": "23.00",
"tax_value": "0.00",
@@ -267,6 +278,7 @@ Endpoints
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"event_location": "Heidelberg",
"attendee_name": null,
"gross_value": "23.00",
"tax_value": "0.00",

View File

@@ -25,7 +25,21 @@ description multi-lingual string A public descri
Markdown syntax or can be ``null``.
position integer An integer, used for sorting
require_membership boolean If ``true``, booking this variation requires an active membership.
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
be hidden from users without a valid membership.
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
sales_channels list of strings Sales channels this variation is available on, such as
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
The item-level list takes precedence, i.e. a sales
channel needs to be on both lists for the item to be
available.
available_from datetime The first date time at which this variation can be bought
(or ``null``).
available_until datetime The last date time at which this variation can be bought
(or ``null``).
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
===================================== ========================== =======================================================
Endpoints
@@ -63,7 +77,12 @@ Endpoints
},
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": {
"en": "Test2"
},
@@ -79,6 +98,7 @@ Endpoints
},
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"description": {},
"position": 1,
@@ -128,7 +148,12 @@ Endpoints
"original_price": null,
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
}
@@ -159,7 +184,12 @@ Endpoints
"default_price": "10.00",
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
}
@@ -180,7 +210,12 @@ Endpoints
"original_price": null,
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
}
@@ -232,7 +267,12 @@ Endpoints
"original_price": null,
"active": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}

View File

@@ -70,6 +70,8 @@ require_approval boolean If ``true``, or
paid.
require_bundling boolean If ``true``, this item is only available as part of bundles.
require_membership boolean If ``true``, booking this item requires an active membership.
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this product will
be hidden from users without a valid membership.
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
grant_membership_type integer If set to the internal ID of a membership type, purchasing this item will
create a membership of the given type.
@@ -105,8 +107,22 @@ variations list of objects A list with one
├ active boolean If ``false``, this variation will not be sold or shown.
├ description multi-lingual string A public description of the variation. May contain
├ require_membership boolean If ``true``, booking this variation requires an active membership.
├ require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
be hidden from users without a valid membership.
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
Markdown syntax or can be ``null``.
├ sales_channels list of strings Sales channels this variation is available on, such as
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
The item-level list takes precedence, i.e. a sales
channel needs to be on both lists for the item to be
available.
├ available_from datetime The first date time at which this variation can be bought
(or ``null``).
├ available_until datetime The last date time at which this variation can be bought
(or ``null``).
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
└ position integer An integer, used for sorting
addons list of objects Definition of add-ons that can be chosen for this item.
Only writable during creation,
@@ -143,6 +159,10 @@ meta_data object Values set for
The attributes ``require_membership``, ``require_membership_types``, ``grant_membership_type``, ``grant_membership_duration_like_event``,
``grant_membership_duration_days`` and ``grant_membership_duration_months`` have been added.
.. versionchanged:: 4.4
The attributes ``require_membership_hidden`` attribute has been added.
Notes
-----
@@ -230,6 +250,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
},
@@ -241,6 +265,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}
@@ -337,6 +365,10 @@ Endpoints
"require_membership": false,
"require_membership_types": [],
"description": null,
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"position": 0
},
{
@@ -347,6 +379,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}
@@ -422,6 +458,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
},
@@ -433,6 +473,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}
@@ -497,6 +541,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
},
@@ -508,6 +556,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}
@@ -603,6 +655,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 0
},
@@ -614,6 +670,10 @@ Endpoints
"active": true,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": null,
"position": 1
}

View File

@@ -128,6 +128,10 @@ last_modified datetime Last modificati
The ``custom_followup_at`` attribute has been added.
.. versionchanged:: 4.4
The ``item`` and ``variation`` query parameters have been added.
.. _order-position-resource:
@@ -415,6 +419,8 @@ List of all orders
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query string search: Only return orders matching a given search query
:query integer item: Only return orders with a position that contains this item ID. *Warning:* Result will also include orders if they contain mixed items, and it will even return orders where the item is only contained in a canceled position.
:query integer variation: Only return orders with a position that contains this variation ID. *Warning:* Result will also include orders if they contain mixed items and variations, and it will even return orders where the variation is only contained in a canceled position.
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
``require_approval`` will be returned.

View File

@@ -0,0 +1,281 @@
Automated email rules
=====================
Resource description
--------------------
Automated email rules that specify emails that the system will send automatically at a specific point in time, e.g.
the day of the event.
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the rule
enabled boolean If ``false``, the rule is ignored
subject multi-lingual string The subject of the email
template multi-lingual string The body of the email
all_products boolean If ``true``, the email is sent to buyers of all products
limit_products list of integers List of product IDs, if ``all_products`` is not set
include_pending boolean If ``true``, the email is sent to pending orders. If ``false``,
only paid orders are considered.
date_is_absolute boolean If ``true``, the email is set at a specific point in time.
send_date datetime If ``date_is_absolute`` is set: Date and time to send the email.
send_offset_days integer If ``date_is_absolute`` is not set, this is the number of days
before/after the email is sent.
send_offset_time time If ``date_is_absolute`` is not set, this is the time of day the
email is sent on the day specified by ``send_offset_days``.
offset_to_event_end boolean If ``true``, ``send_offset_days`` is relative to the event end
date. Otherwise it is relative to the event start date.
offset_is_after boolean If ``true``, ``send_offset_days`` is the number of days **after**
the event start or end date. Otherwise it is the number of days
**before**.
send_to string Can be ``"orders"`` if the email should be sent to customers
(one email per order),
``"attendees"`` if the email should be sent to every attendee,
or ``"both"``.
date. Otherwise it is relative to the event start date.
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/
Returns a list of all rules configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"enabled": true,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/(id)/
Returns information on one rule, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"enabled": true,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the rule to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/
Create a new rule.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 166
{
"enabled": true,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"enabled": true,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
:param organizer: The ``slug`` field of the organizer to create a rule for
:param event: The ``slug`` field of the event to create a rule for
:statuscode 201: no error
:statuscode 400: The rule could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create rules.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/(id)/
Update a rule. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 34
{
"enabled": false,
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"enabled": false,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the rule to modify
:statuscode 200: no error
:statuscode 400: The rule could not be modified due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/(id)/
Delete a rule.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the rule to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this rule cannot be deleted since it is currently in use.

View File

@@ -0,0 +1,28 @@
.. spelling: libpretixsync
Check-in algorithms
===================
When a ticket is scanned at the entrance or exit of an event, we follow a series of steps to determine whether
the check-in is allowed or not. To understand some of the terms in the following diagrams, you should also check
out the documentation of the :ref:`ticket redemption API endpoint <rest-checkin-redeem>`.
Server-side
-----------
The following diagram shows the series of checks executed on the server when a ticket is redeemed through the API.
Some simplifications have been made, for example the deduplication mechanism based on the ``nonce`` parameter
to prevent re-uploads of the same scan is not shown.
.. image:: /images/checkin_online.png
Client-side
-----------
The process of verifying tickets offline is a little different. There are two different approaches,
depending on whether we have information about all tickets in the local database. The following diagram shows
the algorithm as currently implemented in recent versions of `libpretixsync`_.
.. image:: /images/checkin_offline.png
.. _libpretixsync: https://github.com/pretix/libpretixsync

View File

@@ -0,0 +1,13 @@
Algorithms
==========
The business logic inside pretix is full of complex algorithms making decisions based on all the hundreds of settings
and input parameters available. Some of them are documented here as graphs, either because fully understanding them is very
when working on features close to them, or because they also need to be re-implemented by client-side components like our
ticket scanning apps and we want to ensure the implementations are as similar as possible to avoid confusion.
.. toctree::
:maxdepth: 2
checkin
layouts

View File

@@ -0,0 +1,15 @@
.. spelling: pretixPOS
Ticket layout
=============
When a ticket is exported to PDF, the system needs to decide which of multiple PDF layouts to use. The
following diagram shows the steps of the decision, showing both the implementation in pretix itself as
well as the implementation in `pretixPOS`_.
The process can be influenced by plugins, which is demonstrated with the example of the shipping plugin.
.. image:: /images/ticket_layouts.png
.. _pretixPOS: https://pretix.eu/about/en/pos

View File

@@ -1,6 +1,11 @@
.. spelling:: Rebase rebasing
Coding style and quality
========================
Code
----
* Basically, we want all python code to follow the `PEP 8`_ standard. There are a few exceptions where
we see things differently or just aren't that strict. The ``setup.cfg`` file in the project's source
folder contains definitions that allow `flake8`_ to check for violations automatically. See :ref:`checksandtests`
@@ -20,8 +25,62 @@ Coding style and quality
test suite are in the style of Python's unit test module. If you extend those files, you might continue in this style,
but please use ``pytest`` style for any new test files.
* Please keep the first line of your commit messages short. When referencing an issue, please phrase it like
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.
Commits and Pull Requests
-------------------------
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the
individual commits, we use "Rebase and merge" instead. Merge commits should be avoided.
* The commit message should start with a single subject line and can optionally be followed by a commit message body.
* The subject line should be the shortest possible representation of what the commit changes. Someone who reviewed
the commit should able to immediately remember the commit in a couple of weeks based on the subject line and tell
it apart from other commits.
* If there's additional useful information that we should keep, such as reasoning behind the commit, you can
add a longer body, separated from the first line by a blank line.
* The body should explain **what** you changed and more importantly **why** you changed it. There's no need to iterate
**how** you changed something.
* The subject line should be capitalized ("Add new feature" instead of "add new feature") and should not end with a period
("Add new feature" instead of "Add new feature.")
* The subject line should be written in imperative mood, as if you were giving a command what the computer should do if the
commit is applied. This is how generated commit messages by git itself are already written ("Merge branch …", "Revert …")
and makes for short and consistent messages.
* Good: "Fix typo in template"
* Good: "Add Chinese translation"
* Good: "Remove deprecated method"
* Good: "Bump version to 4.4.0"
* Bad: "Fixed bug with …"
* Bad: "Fixes bug with …"
* Bad: "Fixing bug …"
* If all changes in your commit are in context of a single feature or e.g. a bundled plugin, it makes sense to prefix the
subject line with the name of that feature. Examples:
* "API: Add support for PATCH on customers"
* "Docs: Add chapter on alpaca feeding"
* "Stripe: Fix duplicate payments"
* "Order change form: Fix incorrect validation"
* If your commit references a GitHub issue that is fully resolved by your commit, start your subject line with the issue
ID in the form of "Fix #1234 -- Crash in order list". In this case, you can omit the verb "Fix" at the beginning of the
second part of the message to avoid repetition of the word "fix". If your commit only partially resolves the issue, use
"Refs #1234 -- Crash in order list" instead.
* Applies to pretix employees only: If your commit references a sentry issue, please put it in parentheses at the end
of the subject line or inside the body ("Fix crash in order list (PRETIXEU-ABC)"). If your commit references a support
ticket, please put it in parentheses at the end of the subject line with a "Z#" prefix ("Fix crash in order list (Z#12345)").
* If your PR was open for a while and might cause conflicts on merge, please prefer rebasing it (``git rebase -i master``)
over merging ``master`` into your branch unless it is prohibitively complicated.
.. _PEP 8: https://legacy.python.org/dev/peps/pep-0008/

View File

@@ -92,6 +92,9 @@ Carts and Orders
.. autoclass:: pretix.base.models.OrderRefund
:members:
.. autoclass:: pretix.base.models.Transaction
:members:
.. autoclass:: pretix.base.models.CartPosition
:members:

View File

@@ -8,6 +8,7 @@ Developer documentation
setup
contribution/index
implementation/index
translation/index
algorithms/index
api/index
structure
translation/index

View File

@@ -26,7 +26,7 @@ Your should install the following on your system:
* ``libssl`` (Debian package: ``libssl-dev``)
* ``libxml2`` (Debian package ``libxml2-dev``)
* ``libxslt`` (Debian package ``libxslt1-dev``)
* ``libenchant1c2a`` (Debian package ``libenchant1c2a``)
* ``libenchant-2-2`` (Debian package ``libenchant-2-2``)
* ``msgfmt`` (Debian package ``gettext``)
* ``git``
@@ -51,7 +51,12 @@ the dependencies might fail::
Working with the code
---------------------
The first thing you need are all the main application's dependencies::
If you do not have a recent installation of ``nodejs``, install it now::
curl -sL https://deb.nodesource.com/setup_17.x | sudo -E bash -
sudo apt install nodejs
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
cd src/
pip3 install -e ".[dev]"

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -0,0 +1,146 @@
@startuml
partition "data-based check" {
"Check based on local database" --> "Is the order in status PAID or PENDING\nand is the position not canceled?"
--> if "" then
-right->[no] "Return error CANCELED"
else
-down->[yes] "Is the product part of the check-in list?"
--> if "" then
-right->[no] "Return error PRODUCT"
else
-down->[yes] "Is the subevent part of the check-in list?"
--> if "" then
-right->[no] "Return error INVALID"
note bottom: TODO\ninconsistent\nwith online\ncheck
else
-down->[yes] "Is the order in status PAID?"
--> if "" then
-right->[no] "Does the check-in list include pending orders?"
--> if "" then
-right->[no] "Return error UNPAID "
else
-down->[yes] "Is ignore_unpaid set?\n(Has the operator confirmed\nthe checkin?)"
--> if "" then
-right->[no] "Return error UNPAID "
else
-down->[yes] "Is this an entry or exit?"
endif
endif
else
-down->[yes] "Is this an entry or exit?"
endif
endif
endif
endif
"Is this an entry or exit?" --> if "" then
-right->[entry] Evaluate custom logic (rules)
--> if "" then
-right->[error] "Return error RULES"
else
-down->[ok] "Are all required questions answered?"
--> if "" then
-right->[no] "Return error INCOMPLETE"
else
-down->[yes] "Does the check-in list allow multi-entry?"
endif
endif
else
-->[exit] "Return OK "
endif
"Does the check-in list allow multi-entry?" --> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Is this the first checkin\nfor this ticket on this list?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Are all previous checkins\nfor this ticket on this list exits?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Does the check-in list\n allow entry after exit\nand is the last checkin\nan exit?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Return error ALREADY_REDEEMED"
endif
endif
endif
endif
}
partition "dataless check" {
"Check based on secret content" --> "Does the secret decode with\nany supported scheme\nand has a valid signature?"
--> if "" then
-down->[yes] "Is the ticket secret on the revocation list?"
--> if "" then
-right->[yes] "Return error REVOKED"
else
-down->[no] "Is the product part of the check-in list? "
--> if "" then
-right->[no] "Return error PRODUCT "
else
-down->[yes] "Is the subevent part of the check-in list? "
--> if "" then
-right->[no] "Return error INVALID "
note bottom: TODO\ninconsistent\nwith online\ncheck
else
--> "Is this an entry or exit? "
endif
endif
endif
else
-right>[no] "Return error INVALID "
endif
"Is this an entry or exit? " --> if "" then
-right->[entry] "Evaluate custom logic (rules) "
--> if "" then
-right->[error] "Return error RULES "
else
-down->[ok] "Are all required questions answered? "
--> if "" then
-right->[no] "Return error INCOMPLETE "
else
-down->[yes] "Does the check-in list allow multi-entry? "
endif
endif
else
-->[exit] " Return OK "
endif
"Does the check-in list allow multi-entry? " --> if "" then
-right->[yes] " Return OK "
else
-down->[no] "Are any locally queued checkins for\nthis ticket of this list known?"
--> if "" then
-right->[no] " Return OK "
else
-down->[yes] "Are all locally queued checkins\nfor this ticket on this list exits? "
--> if "" then
-right->[yes] " Return OK "
else
-down->[no] "Does the check-in list\n allow entry after exit\nand is the last locally\nqueued checkin\nan exit? "
--> if "" then
-right->[yes] " Return OK "
else
-down->[no] "Return error ALREADY_REDEEMED "
endif
endif
endif
endif
}
(*) --> "Check if order position with\nscanned ticket secret exists"
--> if "" then
-down->[yes] "Check based on local database"
else
-->[no] "Check based on secret content"
endif
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -0,0 +1,92 @@
@startuml
(*) --> "Check if order position with\nscanned ticket secret exists"
--> if "" then
-down->[yes] ===CHECK===
else
-->[no] "Check if secret exists\nin revocation list"
--> if "" then
--> "Is this a forced upload?"
--> if "" then
-->[yes] ===CHECK===
else
-right->[no] "Return error REVOKED"
endif
else
-right->[no] "Return error INVALID"
endif
endif
===CHECK=== -down-> "Is the order in status PAID or PENDING\nand is the position not canceled?"
--> if "" then
-right->[no] "Return error CANCELED"
else
-down->[yes] "Is the product part of the check-in list?"
--> if "" then
-right->[no] "Return error PRODUCT"
else
-down->[yes] "Is the subevent part of the check-in list?"
--> if "" then
-right->[no] "Return error PRODUCT "
else
-down->[yes] "Is the order in status PAID\nor is this a forced upload?"
--> if "" then
-right->[no] "Does the check-in list include pending orders?"
--> if "" then
-right->[no] "Return error UNPAID "
else
-down->[yes] "Is ignore_unpaid set?\n(Has the operator confirmed\nthe checkin?)"
--> if "" then
-right->[no] "Return error UNPAID "
else
-down->[yes] "Is this an entry or exit?\nIs the upload forced?"
endif
endif
else
-down->[yes] "Is this an entry or exit?\nIs the upload forced?"
endif
endif
endif
endif
"Is this an entry or exit?\nIs the upload forced?" --> if "" then
-right->[entry && not force] Evaluate custom logic (rules)
--> if "" then
-right->[error] "Return error RULES"
else
-down->[ok] "Are all required questions answered?"
--> if "" then
-right->[no && questions_supported] "Return error INCOMPLETE"
else
-down->[yes || not questions_supported] "Does the check-in list allow multi-entry?"
endif
endif
else
-->[exit || force=true] "Return OK "
endif
"Does the check-in list allow multi-entry?" --> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Is this the first checkin\nfor this ticket on this list?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Are all previous checkins\nfor this ticket on this list exits?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Does the check-in list\n allow entry after exit\nand is the last checkin\nan exit?"
--> if "" then
-right->[yes] "Return OK"
else
-down->[no] "Return error ALREADY_REDEEMED"
endif
endif
endif
endif
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -0,0 +1,52 @@
@startuml
(*) --> "Which implementation?"
--> if "" then
-down->[pretixPOS] "Check for TicketLayoutItem with\nsales_channel=pretixpos [libpretixsync]"
--> if "" then
--> (*)
else
-->[not found] "Check for TicketLayoutItem with\nsales_channel=web [libpretixsync]"
--> if "" then
--> (*)
else
-->[not found] "Use event default [libpretixsync]"
--> (*)
endif
endif
else
-right->[pretix] "Check for TicketLayoutItem with\nsales_channel=order.sales_channel"
--> if "" then
-right-> "Run override_layout plugin signal on result"
else
-down->[not found] "Check for TicketLayoutItem with\nsales_channel=web"
--> if "" then
--> "Run override_layout plugin signal on result"
else
-->[not found] "Use event default"
--> "Run override_layout plugin signal on result"
endif
endif
endif
"Run override_layout plugin signal on result" -> (*)
partition pretix_shipping {
"Run override_layout plugin signal on result" --> "Check for ShippingLayoutItem with\nmethod=order.shipping_method"
--> if "" then
--> (*)
else
-down->[not found] "Check for ShippingMethod.layout"
--> if "" then
--> (*)
else
-down->[not found] "Keep original layout"
--> (*)
endif
endif
}
@enduml

View File

@@ -0,0 +1,64 @@
Certificates of attendance
==========================
The certificates plugin provides a HTTP API that allows you to download the certificate for a specific attendee.
Certificate download
--------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/certificate/
Downloads the certificate for one order position, identified by its internal ID. Download is a two-step
process. You will always get a :http:statuscode:`303` response with a ``Location`` header to a different
URL. In the background, our server starts preparing the PDF file.
If you then do a ``GET`` to the URL you were given, you will either receive a :http:statuscode:`409` response
indicating to retry after a few seconds, or a :http:statuscode:`200` response with the PDF file.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 303 See Other
Location: /api/v1/organizers/democon/events/3vjrh/orderpositions/426/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5 HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/pdf
...
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the order position to fetch
:statuscode 200: File ready for download
:statuscode 303: Processing started
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
**or** downloads are not available for this order position at this time. The response content will
contain more details.
:statuscode 404: The requested order position or download provider does not exist.
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.

View File

@@ -15,5 +15,6 @@ If you want to **create** a plugin, please go to the
ticketoutputpdf
badges
campaigns
certificates
digital
webinar

View File

@@ -17,6 +17,7 @@ bic
BIC
boolean
booleans
bugfix
cancelled
casted
Ceph
@@ -77,6 +78,7 @@ mixin
mixins
multi
multidomain
multiplicator
namespace
namespaced
namespaces

View File

@@ -4,8 +4,7 @@ Embeddable Widget
=================
If you want to show your ticket shop on your event website or blog, you can use our JavaScript widget. This way,
users will not need to leave your site to buy their ticket in most cases. The widget will still open a new tab
for the checkout if the user is on a mobile device.
users will not need to leave your site to buy their ticket in most cases.
To obtain the correct HTML code for embedding your event into your website, we recommend that you go to the "Widget"
tab of your event's settings. You can specify some optional settings there (for example the language of the widget)

View File

@@ -34,5 +34,7 @@ git push
# Unlock Weblate
for c in $COMPONENTS; do
wlc unlock $c;
done
for c in $COMPONENTS; do
wlc pull $c;
done

View File

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

View File

@@ -70,9 +70,9 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
)
class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixscan_online_kiosk'
verbose_name = _('pretixSCAN (kiosk mode, online only)')
verbose_name = _('pretixSCAN (kiosk mode, no order sync, no search)')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
@@ -99,6 +99,36 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
)
class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixscan_online_noorders'
verbose_name = _('pretixSCAN (online only, no order sync)')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
('GET', 'api-v1:event-list'),
('GET', 'api-v1:event-detail'),
('GET', 'api-v1:subevent-list'),
('GET', 'api-v1:subevent-detail'),
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:checkinlist-list'),
('GET', 'api-v1:checkinlist-status'),
('POST', 'api-v1:checkinlist-failed_checkins'),
('GET', 'api-v1:checkinlistpos-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
)
class PretixPosSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixpos'
verbose_name = _('pretixPOS')
@@ -133,6 +163,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:orderrefund-list'),
('POST', 'api-v1:orderrefund-done'),
('POST', 'api-v1:cartposition-list'),
('POST', 'api-v1:cartposition-bulk-create'),
('GET', 'api-v1:checkinlist-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
@@ -160,6 +191,7 @@ DEVICE_SECURITY_PROFILES = {
FullAccessSecurityProfile,
PretixScanSecurityProfile,
PretixScanNoSyncSecurityProfile,
PretixScanNoSyncNoSearchSecurityProfile,
PretixPosSecurityProfile,
)
}

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.2 on 2021-07-05 07:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixapi', '0005_auto_20191028_1541'),
]
operations = [
migrations.AlterField(
model_name='webhook',
name='target_url',
field=models.URLField(max_length=255),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-09-15 11:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixapi', '0006_alter_webhook_target_url'),
]
operations = [
migrations.AlterField(
model_name='webhookcall',
name='target_url',
field=models.URLField(max_length=255),
),
]

View File

@@ -95,7 +95,7 @@ class OAuthRefreshToken(AbstractRefreshToken):
class WebHook(models.Model):
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
target_url = models.URLField(verbose_name=_("Target URL"))
target_url = models.URLField(verbose_name=_("Target URL"), max_length=255)
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
@@ -120,7 +120,7 @@ class WebHookEventListener(models.Model):
class WebHookCall(models.Model):
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
datetime = models.DateTimeField(auto_now_add=True)
target_url = models.URLField()
target_url = models.URLField(max_length=255)
action_type = models.CharField(max_length=255)
is_retry = models.BooleanField(default=False)
execution_time = models.FloatField(null=True)

View File

@@ -0,0 +1,27 @@
#
# 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 rest_framework.pagination import PageNumberPagination
class Pagination(PageNumberPagination):
page_size_query_param = 'page_size'
max_page_size = 50

View File

@@ -73,53 +73,61 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
)
with self.context['event'].lock():
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
if validated_data.get('variation')
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
if len(new_quotas) == 0:
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
if validated_data.get('variation')
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
if len(new_quotas) == 0:
raise ValidationError(
gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(validated_data.get('item'))
)
)
for quota in new_quotas:
avail = quota.availability(_cache=self.context['quota_cache'])
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
raise ValidationError(
gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(validated_data.get('item'))
gettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
quota.name
)
)
for quota in new_quotas:
avail = quota.availability()
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
raise ValidationError(
gettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
quota.name
)
)
attendee_name = validated_data.pop('attendee_name', '')
if attendee_name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
if validated_data.get('seat'):
if not seated:
raise ValidationError('The specified product does not allow to choose a seat.')
try:
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
except Seat.DoesNotExist:
raise ValidationError('The specified seat does not exist.')
except Seat.MultipleObjectsReturned:
raise ValidationError('The specified seat ID is not unique.')
else:
validated_data['seat'] = seat
if not seat.is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
):
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated:
raise ValidationError('The specified product requires to choose a seat.')
for quota in new_quotas:
oldsize = self.context['quota_cache'][quota.pk][1]
newsize = oldsize - 1 if oldsize is not None else None
self.context['quota_cache'][quota.pk] = (
Quota.AVAILABILITY_OK if newsize is None or newsize > 0 else Quota.AVAILABILITY_GONE,
newsize
)
validated_data.pop('sales_channel')
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
attendee_name = validated_data.pop('attendee_name', '')
if attendee_name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
if validated_data.get('seat'):
if not seated:
raise ValidationError('The specified product does not allow to choose a seat.')
try:
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
except Seat.DoesNotExist:
raise ValidationError('The specified seat does not exist.')
except Seat.MultipleObjectsReturned:
raise ValidationError('The specified seat ID is not unique.')
else:
validated_data['seat'] = seat
if not seat.is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
):
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated:
raise ValidationError('The specified product requires to choose a seat.')
validated_data.pop('sales_channel')
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data:
options = answ_data.pop('options')

View File

@@ -54,7 +54,7 @@ from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
from pretix.base.settings import validate_event_settings
from pretix.base.settings import LazyI18nStringList, validate_event_settings
from pretix.base.signals import api_event_settings_fields
logger = logging.getLogger(__name__)
@@ -704,6 +704,7 @@ class EventSettingsSerializer(SettingsSerializer):
'payment_term_accept_late',
'payment_explanation',
'payment_pending_hidden',
'mail_days_order_expire_warning',
'ticket_download',
'ticket_download_date',
'ticket_download_addons',
@@ -733,6 +734,7 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_numbers_prefix_cancellations',
'invoice_numbers_counter_length',
'invoice_attendee_name',
'invoice_event_location',
'invoice_include_expire_date',
'invoice_address_explanation_text',
'invoice_email_attachment',
@@ -762,6 +764,7 @@ class EventSettingsSerializer(SettingsSerializer):
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'change_allow_user_variation',
'change_allow_user_addons',
'change_allow_user_until',
'change_allow_user_price',
'primary_color',
@@ -789,6 +792,10 @@ class EventSettingsSerializer(SettingsSerializer):
data = super().validate(data)
settings_dict = self.instance.freeze()
settings_dict.update(data)
if data.get('confirm_texts') is not None:
data['confirm_texts'] = LazyI18nStringList(data['confirm_texts'])
validate_event_settings(self.event, settings_dict)
return data

View File

@@ -31,9 +31,10 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# 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 os.path
from decimal import Decimal
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import QuerySet
@@ -58,7 +59,8 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types',)
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -73,7 +75,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types',)
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -161,7 +164,7 @@ class ItemSerializer(I18nAwareModelSerializer):
meta_data = MetaDataField(required=False, source='*')
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
'image/png', 'image/jpeg', 'image/gif'
), max_size=10 * 1024 * 1024)
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
class Meta:
model = Item
@@ -172,7 +175,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'grant_membership_type',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
'grant_membership_duration_like_event', 'grant_membership_duration_days',
'grant_membership_duration_months')
read_only_fields = ('has_variations',)
@@ -245,10 +248,13 @@ class ItemSerializer(I18nAwareModelSerializer):
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
picture = validated_data.pop('picture', None)
item = Item.objects.create(**validated_data)
if picture:
item.picture.save(os.path.basename(picture.name), picture)
for variation_data in variations_data:
require_membership_types = variation_data.pop('require_membership_types')
require_membership_types = variation_data.pop('require_membership_types', [])
v = ItemVariation.objects.create(item=item, **variation_data)
if require_membership_types:
v.require_membership_types.add(*require_membership_types)
@@ -269,7 +275,10 @@ class ItemSerializer(I18nAwareModelSerializer):
def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None)
picture = validated_data.pop('picture', None)
item = super().update(instance, validated_data)
if picture:
item.picture.save(os.path.basename(picture.name), picture)
# Meta data
if meta_data is not None:

View File

@@ -26,6 +26,7 @@ from collections import Counter, defaultdict
from decimal import Decimal
import pycountry
from django.conf import settings
from django.core.files import File
from django.db.models import F, Q
from django.utils.timezone import now
@@ -191,7 +192,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
)
if cf.type not in allowed_types:
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
if cf.file.size > 10 * 1024 * 1024:
if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
data['options'] = []
@@ -1403,6 +1404,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
state=OrderPayment.PAYMENT_STATE_CREATED
)
order.create_transactions(is_new=True, fees=fees, positions=pos_map.values())
return order
@@ -1426,7 +1428,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
model = InvoiceLine
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name', 'fee_type',
'fee_internal_type')
'fee_internal_type', 'event_location')
class InvoiceSerializer(I18nAwareModelSerializer):

View File

@@ -275,6 +275,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
default_fields = [
'customer_accounts',
'customer_accounts_link_by_email',
'invoice_regenerate_allowed',
'contact_mail',
'imprint_url',
'organizer_info_text',
@@ -294,6 +295,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'theme_color_background',
'theme_round_borders',
'primary_font',
'organizer_logo_image_inherit',
'organizer_logo_image'
]

View File

@@ -21,14 +21,18 @@
#
from django.db import transaction
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.response import Response
from rest_framework.settings import api_settings
from pretix.api.serializers.cart import (
CartPositionCreateSerializer, CartPositionSerializer,
)
from pretix.base.models import CartPosition
from pretix.base.services.locking import NoLockManager
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
@@ -50,18 +54,61 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['quota_cache'] = {}
return ctx
def create(self, request, *args, **kwargs):
serializer = CartPositionCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():
with transaction.atomic(), self.request.event.lock():
self.perform_create(serializer)
cp = serializer.instance
serializer = CartPositionSerializer(cp, context=serializer.context)
cp = serializer.instance
serializer = CartPositionSerializer(cp, context=serializer.context)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
@action(detail=False, methods=['POST'])
def bulk_create(self, request, *args, **kwargs):
if not isinstance(request.data, list): # noqa
return Response({"error": "Please supply a list"}, status=status.HTTP_400_BAD_REQUEST)
ctx = self.get_serializer_context()
with transaction.atomic():
serializers = [
CartPositionCreateSerializer(data=d, context=ctx)
for d in request.data
]
lockfn = self.request.event.lock
if not any(s.is_valid(raise_exception=False) for s in serializers):
lockfn = NoLockManager
results = []
with lockfn():
for s in serializers:
if s.is_valid(raise_exception=False):
try:
cp = s.save()
except ValidationError as e:
results.append({
'success': False,
'data': None,
'errors': {api_settings.NON_FIELD_ERRORS_KEY: e.detail},
})
else:
results.append({
'success': True,
'data': CartPositionSerializer(cp, context=ctx).data,
'errors': None,
})
else:
results.append({
'success': False,
'data': None,
'errors': s.errors,
})
return Response({'results': results}, status=status.HTTP_200_OK)
def perform_create(self, serializer):
serializer.save()

View File

@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import django_filters
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import (
@@ -32,6 +33,7 @@ from django.utils.functional import cached_property
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from packaging.version import parse
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.fields import DateTimeField
@@ -421,13 +423,20 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
nonce=nonce,
forced=force,
)
raw_barcode_for_checkin = None
try:
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
if self.kwargs['pk'].isnumeric():
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:
op = queryset.get(secret=self.kwargs['pk'])
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
# `id`, however, is part of a path where this technically is not allowed. Old versions of our
# scan apps still do it, so we try work around it!
try:
op = queryset.get(secret=self.kwargs['pk'])
except OrderPosition.DoesNotExist:
op = queryset.get(secret=self.kwargs['pk'].replace('+', ' '))
except OrderPosition.DoesNotExist:
revoked_matches = list(self.request.event.revoked_secrets.filter(secret=self.kwargs['pk']))
if len(revoked_matches) == 0:
@@ -455,7 +464,41 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
raise Http404()
if force and isinstance(self.request.auth, Device):
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
# valid at the time but no longer exists at time of upload, the device would retry to
# upload the same scan over and over again. Since we can't update all devices quickly,
# here's a dirty workaround to make it stop.
try:
brand = self.request.auth.software_brand
ver = parse(self.request.auth.software_version)
legacy_mode = (
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
)
if legacy_mode:
return Response({
'status': 'error',
'reason': Checkin.REASON_ALREADY_REDEEMED,
'reason_explanation': None,
'require_attention': False,
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
}, status=400)
except: # we don't care e.g. about invalid version numbers
pass
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
}, status=404)
elif revoked_matches and force:
op = revoked_matches[0].position
raw_barcode_for_checkin = self.kwargs['pk']
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
@@ -506,7 +549,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
user=self.request.user,
auth=self.request.auth,
type=type,
raw_barcode=None,
raw_barcode=raw_barcode_for_checkin,
from_revoked_secret=True,
)
except RequiredQuestionsError as e:
return Response({
@@ -566,7 +610,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
)
if cf.type not in allowed_types:
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
if cf.file.size > 10 * 1024 * 1024:
if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
return cf.file

View File

@@ -69,7 +69,7 @@ class ExportersMixin:
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
if cf.file:
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
return resp
elif not settings.HAS_CELERY:
return Response(
@@ -132,7 +132,7 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
def exporters(self):
exporters = []
responses = register_data_exporters.send(self.request.event)
for ex in sorted([response(self.request.event) for r, response in responses], key=lambda ex: str(ex.verbose_name)):
for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses], key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
return exporters
@@ -151,7 +151,7 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
organizer=self.request.organizer
)
responses = register_multievent_data_exporters.send(self.request.organizer)
for ex in sorted([response(events) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
for ex in sorted([response(events, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex, events=events)
exporters.append(ex)
return exporters

View File

@@ -92,6 +92,8 @@ with scopes_disabled():
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
search = django_filters.CharFilter(method='search_qs')
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id')
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id')
class Meta:
model = Order
@@ -214,7 +216,9 @@ class OrderViewSet(viewsets.ModelViewSet):
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
'item', 'variation',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
'seat',
)
)
)
@@ -1451,8 +1455,14 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
inv = self.get_object()
if inv.canceled:
raise ValidationError('The invoice has already been canceled.')
if not inv.event.settings.invoice_regenerate_allowed:
raise PermissionDenied('Invoices may not be changed after they are created.')
elif inv.shredded:
raise PermissionDenied('The invoice file is no longer stored on the server.')
elif inv.sent_to_organizer:
raise PermissionDenied('The invoice file has already been exported.')
elif now().astimezone(self.request.event.timezone).date() - inv.date > datetime.timedelta(days=1):
raise PermissionDenied('The invoice file is too old to be regenerated.')
else:
inv = regenerate_invoice(inv)
inv.order.log_action(

View File

@@ -261,7 +261,7 @@ def register_default_webhook_events(sender, **kwargs):
),
ParametrizedEventWebhookEvent(
'pretix.event.deleted',
_('Event details changed'),
_('Event deleted'),
),
ParametrizedSubEventWebhookEvent(
'pretix.subevent.added',

View File

@@ -47,6 +47,7 @@ class PretixBaseConfig(AppConfig):
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .models import _transactions # NOQA
from django.conf import settings
try:

View File

@@ -82,6 +82,13 @@ class SalesChannel:
"""
return False
@property
def customer_accounts_supported(self) -> bool:
"""
If this property is ``True``, checkout will show the customer login step.
"""
return True
def get_all_sales_channels():
global _ALL_CHANNELS

View File

@@ -462,6 +462,16 @@ def base_placeholders(sender, **kwargs):
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
),
SimpleFunctionalMailTextPlaceholder(
'url_remove', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(
@@ -529,6 +539,22 @@ def base_placeholders(sender, **kwargs):
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalMailTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in voucher_list
]),
lambda event: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
]),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,

View File

@@ -33,7 +33,6 @@
# License for the specific language governing permissions and limitations under the License.
import io
import re
import tempfile
from collections import OrderedDict, namedtuple
from decimal import Decimal
@@ -46,23 +45,13 @@ from django.conf import settings
from django.db.models import QuerySet
from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES
from pretix.base.models import Event
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
)
def excel_safe(val):
if not isinstance(val, KNOWN_TYPES):
val = str(val)
if isinstance(val, bytes):
val = val.decode("utf-8", errors="ignore")
if isinstance(val, str):
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
return val
__ = excel_safe # just so the compatbility import above is "used" and doesn't get removed by linter
class BaseExporter:
@@ -70,8 +59,9 @@ class BaseExporter:
This is the base class for all data exporters
"""
def __init__(self, event, progress_callback=lambda v: None):
def __init__(self, event, organizer, progress_callback=lambda v: None):
self.event = event
self.organizer = organizer
self.progress_callback = progress_callback
self.is_multievent = isinstance(event, QuerySet)
if isinstance(event, QuerySet):
@@ -220,9 +210,13 @@ class ListExporter(BaseExporter):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def prepare_xlsx_sheet(self, ws):
pass
def _render_xlsx(self, form_data, output_file=None):
wb = Workbook(write_only=True)
wb = SafeWorkbook(write_only=True)
ws = wb.create_sheet()
self.prepare_xlsx_sheet(ws)
try:
ws.title = str(self.verbose_name)
except:
@@ -234,7 +228,7 @@ class ListExporter(BaseExporter):
total = line.total
continue
ws.append([
excel_safe(val) for val in line
val for val in line
])
if total:
counter += 1
@@ -339,7 +333,7 @@ class MultiSheetListExporter(ListExporter):
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data, output_file=None):
wb = Workbook(write_only=True)
wb = SafeWorkbook(write_only=True)
n_sheets = len(self.sheets)
for i_sheet, (s, l) in enumerate(self.sheets):
ws = wb.create_sheet(str(l))
@@ -353,8 +347,7 @@ class MultiSheetListExporter(ListExporter):
total = line.total
continue
ws.append([
excel_safe(val)
for val in line
val for val in line
])
if total:
counter += 1

View File

@@ -324,7 +324,6 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Tax rate'),
_('Tax name'),
_('Event start date'),
_('Date'),
_('Order code'),
_('E-mail address'),
@@ -348,6 +347,8 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
_('Payment providers'),
_('Event end date'),
_('Location'),
]
p_providers = OrderPayment.objects.filter(
@@ -406,7 +407,9 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
', '.join([
str(self.providers.get(p, p)) for p in sorted(set((l.payment_providers or '').split(',')))
if p and p != 'free'
])
]),
date_format(l.event_date_to, "SHORT_DATE_FORMAT") if l.event_date_to else "",
l.event_location or "",
]
@cached_property

View File

@@ -55,16 +55,20 @@ class JSONExporter(BaseExporter):
'name': str(self.event.organizer.name),
'slug': self.event.organizer.slug
},
'meta_data': self.event.meta_data,
'categories': [
{
'id': category.id,
'name': str(category.name),
'description': str(category.description),
'position': category.position,
'internal_name': category.internal_name
} for category in self.event.categories.all()
],
'items': [
{
'id': item.id,
'position': item.position,
'name': str(item.name),
'internal_name': str(item.internal_name),
'category': item.category_id,
@@ -73,13 +77,35 @@ class JSONExporter(BaseExporter):
'tax_name': str(item.tax_rule.name) if item.tax_rule else None,
'admission': item.admission,
'active': item.active,
'sales_channels': item.sales_channels,
'description': str(item.description),
'available_from': item.available_from,
'available_until': item.available_until,
'require_voucher': item.require_voucher,
'hide_without_voucher': item.hide_without_voucher,
'allow_cancel': item.allow_cancel,
'require_bundling': item.require_bundling,
'min_per_order': item.min_per_order,
'max_per_order': item.max_per_order,
'checkin_attention': item.checkin_attention,
'original_price': item.original_price,
'issue_giftcard': item.issue_giftcard,
'meta_data': item.meta_data,
'require_membership': item.require_membership,
'variations': [
{
'id': variation.id,
'active': variation.active,
'price': variation.default_price if variation.default_price is not None else
item.default_price,
'name': str(variation)
'name': str(variation),
'description': str(variation.description),
'position': variation.position,
'require_membership': variation.require_membership,
'sales_channels': variation.sales_channels,
'available_from': variation.available_from,
'available_until': variation.available_until,
'hide_without_voucher': variation.hide_without_voucher,
} for variation in item.variations.all()
]
} for item in self.event.items.select_related('tax_rule').prefetch_related('variations')
@@ -87,7 +113,13 @@ class JSONExporter(BaseExporter):
'questions': [
{
'id': question.id,
'identifier': question.identifier,
'required': question.required,
'question': str(question.question),
'position': question.position,
'hidden': question.hidden,
'ask_during_checkin': question.ask_during_checkin,
'help_text': str(question.help_text),
'type': question.type
} for question in self.event.questions.all()
],
@@ -95,7 +127,18 @@ class JSONExporter(BaseExporter):
{
'code': order.code,
'status': order.status,
'customer': order.customer.identifier if order.customer else None,
'testmode': order.testmode,
'user': order.email,
'email': order.email,
'phone': str(order.phone),
'locale': order.locale,
'comment': order.comment,
'custom_followup_at': order.custom_followup_at,
'require_approval': order.require_approval,
'checkin_attention': order.checkin_attention,
'sales_channel': order.sales_channel,
'expires': order.expires,
'datetime': order.datetime,
'fees': [
{
@@ -108,11 +151,21 @@ class JSONExporter(BaseExporter):
'positions': [
{
'id': position.id,
'positionid': position.positionid,
'item': position.item_id,
'variation': position.variation_id,
'subevent': position.subevent_id,
'seat': position.seat.seat_guid if position.seat else None,
'price': position.price,
'tax_rate': position.tax_rate,
'tax_value': position.tax_value,
'attendee_name': position.attendee_name,
'attendee_email': position.attendee_email,
'company': position.company,
'street': position.street,
'zipcode': position.zipcode,
'country': str(position.country) if position.country else None,
'state': position.state,
'secret': position.secret,
'addon_to': position.addon_to_id,
'answers': [
@@ -124,15 +177,30 @@ class JSONExporter(BaseExporter):
} for position in order.positions.all()
]
} for order in
self.event.orders.all().prefetch_related('positions', 'positions__answers', 'fees')
self.event.orders.all().prefetch_related('positions', 'positions__answers', 'positions__seat', 'customer', 'fees')
],
'quotas': [
{
'id': quota.id,
'size': quota.size,
'subevent': quota.subevent_id,
'items': [item.id for item in quota.items.all()],
'variations': [variation.id for variation in quota.variations.all()],
} for quota in self.event.quotas.all().prefetch_related('items', 'variations')
],
'subevents': [
{
'id': se.id,
'name': str(se.name),
'location': str(se.location),
'date_from': se.date_from,
'date_to': se.date_to,
'date_admission': se.date_admission,
'geo_lat': se.geo_lat,
'geo_lon': se.geo_lon,
'is_public': se.is_public,
'meta_data': se.meta_data,
} for se in self.event.subevents.all()
]
}
}

View File

@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
from collections import OrderedDict
from datetime import date, datetime, time
from decimal import Decimal
import dateutil
@@ -42,10 +43,10 @@ from django.db.models import (
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
Q, Subquery, Sum, When,
)
from django.db.models.functions import Coalesce, TruncDate
from django.db.models.functions import Coalesce
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, now
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.models import (
@@ -129,7 +130,7 @@ class OrderListExporter(MultiSheetListExporter):
label=_('End event date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include orders including at least one ticket for a date on or after this date. '
help_text=_('Only include orders including at least one ticket for a date on or before this date. '
'Will also include other dates in case of mixed orders!')
)),
]
@@ -181,41 +182,43 @@ class OrderListExporter(MultiSheetListExporter):
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
annotations['date'] = TruncDate(f'{rel}datetime')
filters['date__gte'] = date_value
filters[f'{rel}datetime__gte'] = datetime_value
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
annotations['date'] = TruncDate(f'{rel}datetime')
filters['date__lte'] = date_value
filters[f'{rel}datetime__lte'] = datetime_value
if form_data.get('event_date_from'):
date_value = form_data.get('event_date_from')
if isinstance(date_value, str):
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
annotations['event_date_max'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_max__gte'] = date_value
filters['event_date_max__gte'] = datetime_value
if form_data.get('event_date_to'):
date_value = form_data.get('event_date_to')
if isinstance(date_value, str):
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
annotations['event_date_min'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Min(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_min__lte'] = date_value
filters['event_date_min__lte'] = datetime_value
if filters:
return qs.annotate(**annotations).filter(**filters)
@@ -870,6 +873,78 @@ class QuotaListExporter(ListExporter):
return '{}_quotas'.format(self.event.slug)
def generate_GiftCardTransactionListExporter(organizer): # hackhack
class GiftcardTransactionListExporter(ListExporter):
identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions')
@property
def additional_form_fields(self):
d = [
('date_from',
forms.DateField(
label=_('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
]
d = OrderedDict(d)
return d
def iterate_list(self, form_data):
qs = GiftCardTransaction.objects.filter(
card__issuer=organizer,
).order_by('datetime').select_related('card', 'order', 'order__event')
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__gte=make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__lte=make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
)
headers = [
_('Gift card code'),
_('Test mode'),
_('Date'),
_('Amount'),
_('Currency'),
_('Order'),
]
yield headers
for obj in qs:
row = [
obj.card.secret,
_('TEST MODE') if obj.card.testmode else '',
obj.datetime.astimezone(self.timezone).strftime('%Y-%m-%d %H:%M:%S'),
obj.value,
obj.card.currency,
obj.order.full_code if obj.order else None,
]
yield row
def get_filename(self):
return '{}_giftcardtransactions'.format(organizer.slug)
return GiftcardTransactionListExporter
class GiftcardRedemptionListExporter(ListExporter):
identifier = 'giftcardredemptionlist'
verbose_name = gettext_lazy('Gift card redemptions')
@@ -1062,3 +1137,8 @@ def register_multievent_i_giftcardredemptionlist_exporter(sender, **kwargs):
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardlist")
def register_multievent_i_giftcardlist_exporter(sender, **kwargs):
return generate_GiftCardListExporter(sender)
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardtransactionlist")
def register_multievent_i_giftcardtransactionlist_exporter(sender, **kwargs):
return generate_GiftCardTransactionListExporter(sender)

View File

@@ -37,21 +37,19 @@ import json
import logging
from decimal import Decimal
from io import BytesIO
from urllib.error import HTTPError
import dateutil.parser
import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
from babel import Locale
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import QuerySet
from django.forms import Select
from django.forms import Select, widgets
from django.utils import translation
from django.utils.formats import date_format
from django.utils.html import escape
@@ -75,8 +73,9 @@ from pretix.base.i18n import (
get_babel_locale, get_language_without_region, language,
)
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import (
EU_COUNTRIES, cc_to_vat_prefix, is_eu_country,
from pretix.base.models.tax import VAT_ID_COUNTRIES, ask_for_vat_id
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
)
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS,
@@ -153,8 +152,9 @@ class NamePartsWidget(forms.MultiWidget):
final_attrs,
id='%s_%s' % (id_, i),
title=self.scheme['fields'][i][1],
placeholder=self.scheme['fields'][i][1],
)
if not isinstance(widget, widgets.Select):
these_attrs['placeholder'] = self.scheme['fields'][i][1]
if self.scheme['fields'][i][0] in REQUIRED_NAME_PARTS:
if self.field.required:
these_attrs['required'] = 'required'
@@ -507,7 +507,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
def __init__(self, *args, **kwargs):
kwargs.setdefault('ext_whitelist', (".png", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff", ".bmp"))
kwargs.setdefault('max_size', 10 * 1024 * 1024)
kwargs.setdefault('max_size', settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
super().__init__(*args, **kwargs)
@@ -674,7 +674,7 @@ class BaseQuestionsForm(forms.Form):
label=label, required=required,
min_value=q.valid_number_min or Decimal('0.00'),
max_value=q.valid_number_max,
help_text=q.help_text,
help_text=help_text,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_STRING:
@@ -739,7 +739,7 @@ class BaseQuestionsForm(forms.Form):
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
".bmp", ".tif", ".tiff"
),
max_size=10 * 1024 * 1024,
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
)
elif q.type == Question.TYPE_DATE:
attrs = {}
@@ -900,7 +900,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-in-eu': ','.join(EU_COUNTRIES)}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-with-vat-id': ','.join(VAT_ID_COUNTRIES)}),
'internal_reference': forms.TextInput,
}
labels = {
@@ -920,6 +920,18 @@ class BaseInvoiceAddressForm(forms.ModelForm):
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
elif self.validate_vat_id:
self.fields['vat_id'].help_text = '<br/>'.join([
str(_('Optional, but depending on the country you reside in we might need to charge you '
'additional taxes if you do not enter it.')),
str(_('If you are registered in Switzerland, you can enter your UID instead.')),
])
else:
self.fields['vat_id'].help_text = '<br/>'.join([
str(_('Optional, but it might be required for you to claim tax benefits on your invoice '
'depending on your and the sellers country of residence.')),
str(_('If you are registered in Switzerland, you can enter your UID instead.')),
])
self.fields['country'].choices = CachedCountries()
@@ -951,7 +963,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['state'].widget.is_required = True
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
if cc and not is_eu_country(cc) and fprefix + 'vat_id' in self.data:
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'vat_id']
@@ -976,7 +988,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
scheme=event.settings.name_scheme,
titles=event.settings.name_scheme_titles,
label=_('Name'),
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
initial=self.instance.name_parts,
)
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
if not event.settings.invoice_name_required:
@@ -1001,7 +1013,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not data.get('is_business'):
data['company'] = ''
data['vat_id'] = ''
if data.get('is_business') and not is_eu_country(data.get('country')):
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
data['vat_id'] = ''
if self.event.settings.invoice_address_required:
if data.get('is_business') and not data.get('company'):
@@ -1024,36 +1036,19 @@ class BaseInvoiceAddressForm(forms.ModelForm):
# Do not save the country if it is the only field set -- we don't know the user even checked it!
self.cleaned_data['country'] = ''
if data.get('vat_id') and is_eu_country(data.get('country')) and data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
raise ValidationError(_('Your VAT ID does not match the selected country.'))
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and is_eu_country(data.get('country')) and data.get('vat_id'):
elif self.validate_vat_id and data.get('is_business') and ask_for_vat_id(data.get('country')) and data.get('vat_id'):
try:
result = vat_moss.id.validate(data.get('vat_id'))
if result:
country_code, normalized_id, company_name = result
self.instance.vat_id_validated = True
self.instance.vat_id = normalized_id
except (vat_moss.errors.InvalidError, ValueError):
raise ValidationError(_('This VAT ID is not valid. Please re-check your input.'))
except vat_moss.errors.WebServiceUnavailableError:
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = True
self.instance.vat_id = normalized_id
except VATIDFinalError as e:
raise ValidationError(e.message)
except VATIDTemporaryError as e:
self.instance.vat_id_validated = False
if self.request and self.vat_warning:
messages.warning(self.request, _('Your VAT ID could not be checked, as the VAT checking service of '
'your country is currently not available. We will therefore '
'need to charge VAT on your invoice. You can get the tax amount '
'back via the VAT reimbursement process.'))
except (vat_moss.errors.WebServiceError, HTTPError):
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))
self.instance.vat_id_validated = False
if self.request and self.vat_warning:
messages.warning(self.request, _('Your VAT ID could not be checked, as the VAT checking service of '
'your country returned an incorrect result. We will therefore '
'need to charge VAT on your invoice. Please contact support to '
'resolve this manually.'))
messages.warning(self.request, e.message)
else:
self.instance.vat_id_validated = False

View File

@@ -55,6 +55,7 @@ class UserSettingsForm(forms.ModelForm):
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
'pw_equal': _("Please choose a password different to your current one.")
}
old_pw = forms.CharField(max_length=255,
@@ -158,6 +159,12 @@ class UserSettingsForm(forms.ModelForm):
code='pw_current'
)
if password1 and password1 == old_pw:
raise forms.ValidationError(
self.error_messages['pw_equal'],
code='pw_equal'
)
if password1:
self.instance.set_password(password1)

View File

@@ -184,7 +184,7 @@ class BusinessBooleanRadio(forms.RadioSelect):
self.require_business = require_business
if self.require_business:
choices = (
('business', _('Business customer')),
('business', _('Business or institutional customer')),
)
else:
choices = (

View File

@@ -395,7 +395,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return txt
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
tz = self.invoice.event.timezone
show_end_date = (
self.invoice.event.settings.show_date_to and
self.invoice.event.date_to and
self.invoice.event.date_to.astimezone(tz).date() != self.invoice.event.date_from.astimezone(tz).date()
)
if show_end_date:
p_str = (
shorten(self.invoice.event.name) + '\n' +
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
@@ -550,7 +556,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
for line in self.invoice.lines.all():
if has_taxes:
tdata.append((
Paragraph(line.description, self.stylesheet['Normal']),
Paragraph(
bleach.clean(line.description, tags=['br']).strip().replace('<br>', '<br/>').replace('\n', '<br />\n'),
self.stylesheet['Normal']
),
"1",
localize(line.tax_rate) + " %",
money_filter(line.net_value, self.invoice.event.currency),
@@ -558,7 +567,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
))
else:
tdata.append((
Paragraph(line.description, self.stylesheet['Normal']),
Paragraph(
bleach.clean(line.description, tags=['br']).strip().replace('<br>', '<br/>').replace('\n', '<br />\n'),
self.stylesheet['Normal']
),
"1",
money_filter(line.gross_value, self.invoice.event.currency),
))
@@ -595,7 +607,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata))
story.append(table)
story.append(Spacer(1, 15 * mm))
story.append(Spacer(1, 10 * mm))
if self.invoice.payment_provider_text:
story.append(Paragraph(
@@ -611,12 +623,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
self.invoice.additional_text,
self.stylesheet['Normal']
))
story.append(Spacer(1, 15 * mm))
story.append(Spacer(1, 5 * mm))
tstyledata = [
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 1),
('BOTTOMPADDING', (0, 0), (-1, -1), 1),
('FONTSIZE', (0, 0), (-1, -1), 8),
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]
@@ -769,44 +783,55 @@ class Modern1Renderer(ClassicInvoiceRenderer):
]
def _draw_metadata(self, canvas):
# Draws the "invoice number -- date" line. This has gotten a little more complicated since we
# encountered some events with very long invoice numbers. In this case, we automatically reduce
# the font size until it fits.
begin_top = 100 * mm
textobject = canvas.beginText(self.left_margin, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Order code'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.order.full_code)
canvas.drawText(textobject)
if self.invoice.is_cancellation:
textobject = canvas.beginText(self.left_margin + 50 * mm, self.pagesize[1] - begin_top)
def _draw(label, value, value_size, x, width):
if canvas.stringWidth(value, self.font_regular, value_size) > width and value_size > 6:
return False
textobject = canvas.beginText(x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Cancellation number'))
textobject.textLine(label)
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
canvas.drawText(textobject)
textobject.setFont(self.font_regular, value_size)
textobject.textLine(value)
return textobject
textobject = canvas.beginText(self.left_margin + 100 * mm, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Original invoice'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.refers.number)
canvas.drawText(textobject)
else:
textobject = canvas.beginText(self.left_margin + 70 * mm, self.pagesize[1] - begin_top)
textobject.textLine(pgettext('invoice', 'Invoice number'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
canvas.drawText(textobject)
value_size = 10
while value_size >= 5:
objects = [
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm)
]
p = Paragraph(
date_format(self.invoice.date, "DATE_FORMAT"),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
)
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
p.wrapOn(canvas, w, 15 * mm)
date_x = self.pagesize[0] - w - self.right_margin
if self.invoice.is_cancellation:
objects += [
_draw(pgettext('invoice', 'Cancellation number'), self.invoice.number,
value_size, self.left_margin + 50 * mm, 45 * mm),
_draw(pgettext('invoice', 'Original invoice'), self.invoice.refers.number,
value_size, self.left_margin + 100 * mm, date_x - self.left_margin - 100 * mm - 5 * mm),
]
else:
objects += [
_draw(pgettext('invoice', 'Invoice number'), self.invoice.number,
value_size, self.left_margin + 70 * mm, date_x - self.left_margin - 70 * mm - 5 * mm),
]
if all(objects):
for o in objects:
canvas.drawText(o)
break
value_size -= 1
p = Paragraph(date_format(self.invoice.date, "DATE_FORMAT"), style=self.stylesheet['Normal'])
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
p.wrapOn(canvas, w, 15 * mm)
date_x = self.pagesize[0] - w - self.right_margin
p.drawOn(canvas, date_x, self.pagesize[1] - begin_top - 10 - 6)
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)

View File

@@ -0,0 +1,67 @@
#
# 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/>.
#
"""
Django, for theoretically very valid reasons, creates migrations for *every single thing*
we change on a model. Even the `help_text`! This makes sense, as we don't know if any
database backend unknown to us might actually use this information for its database schema.
However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
certain that some changes to models will never require a change to the database. In this case,
not creating a migration for certain changes will save us some performance while applying them
*and* allow for a cleaner git history. Win-win!
Only caveat is that we need to do some dirty monkeypatching to achieve it...
"""
from django.db import models
from django.db.migrations.operations import models as modelops
from django_countries.fields import CountryField
def monkeypatch_migrations():
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name_plural")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("ordering")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("get_latest_by")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
IGNORED_ATTRS = [
# (field type, attribute name, banlist of field sub-types)
(models.Field, 'verbose_name', []),
(models.Field, 'help_text', []),
(models.Field, 'validators', []),
(models.Field, 'editable', [models.DateField, models.DateTimeField, models.DateField, models.BinaryField]),
(models.Field, 'blank', [models.DateField, models.DateTimeField, models.AutoField, models.NullBooleanField,
models.TimeField]),
(models.CharField, 'choices', [CountryField])
]
original_deconstruct = models.Field.deconstruct
def new_deconstruct(self):
name, path, args, kwargs = original_deconstruct(self)
for ftype, attr, banlist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in banlist):
kwargs.pop(attr, None)
return name, path, args, kwargs
models.Field.deconstruct = new_deconstruct

View File

@@ -0,0 +1,82 @@
#
# 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 decimal import Decimal
from django.core.management.base import BaseCommand
from django.db import models
from django.db.models import Case, F, OuterRef, Q, Subquery, Sum, Value, When
from django.db.models.functions import Coalesce
from django_scopes import scopes_disabled
from pretix.base.models import Order, OrderFee, OrderPosition
from pretix.base.models.orders import Transaction
class Command(BaseCommand):
help = "Check order for consistency with their transactions"
@scopes_disabled()
def handle(self, *args, **options):
qs = Order.objects.annotate(
position_total=Coalesce(
Subquery(
OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('price')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
fee_total=Coalesce(
Subquery(
OrderFee.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('value')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
tx_total=Coalesce(
Subquery(
Transaction.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum(F('price') * F('count'))).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).annotate(
correct_total=Case(
When(Q(status=Order.STATUS_CANCELED) | Q(status=Order.STATUS_EXPIRED) | Q(require_approval=True),
then=Value(0)),
default=F('position_total') + F('fee_total'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).exclude(
total=F('position_total') + F('fee_total'),
tx_total=F('correct_total')
).select_related('event')
for o in qs:
if abs(o.tx_total - o.correct_total) < Decimal('0.00001') and abs(o.position_total + o.fee_total - o.total) < Decimal('0.00001'):
# Ignore SQLite which treats Decimals like floats…
continue
print(f"Error in order {o.full_code}: status={o.status}, sum(positions)+sum(fees)={o.position_total + o.fee_total}, "
f"order.total={o.total}, sum(transactions)={o.tx_total}, expected={o.correct_total}")
self.stderr.write(self.style.SUCCESS(f'Check completed.'))

View File

@@ -0,0 +1,95 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import time
from django.core.management.base import BaseCommand
from django.db.models import F, Max, Q
from django.utils.timezone import now
from django_scopes import scopes_disabled
from tqdm import tqdm
from pretix.base.models import Order
class Command(BaseCommand):
help = "Create missing order transactions"
def add_arguments(self, parser):
parser.add_argument(
"--slowdown",
dest="interval",
type=int,
default=0,
help="Interval for staggered execution. If set to a value different then zero, we will "
"wait this many milliseconds between every order we process.",
)
@scopes_disabled()
def handle(self, *args, **options):
t = 0
qs = Order.objects.annotate(
last_transaction=Max('transactions__created')
).filter(
Q(last_transaction__isnull=True) | Q(last_modified__gt=F('last_transaction')),
require_approval=False,
).prefetch_related(
'all_positions', 'all_fees'
).order_by(
'pk'
)
last_pk = 0
with tqdm(total=qs.count()) as pbar:
while True:
batch = list(qs.filter(pk__gt=last_pk)[:5000])
if not batch:
break
for o in batch:
if o.last_transaction is None:
tn = o.create_transactions(
positions=o.all_positions.all(),
fees=o.all_fees.all(),
dt_now=o.datetime,
migrated=True,
is_new=True,
_backfill_before_cancellation=True,
)
o.create_transactions(
positions=o.all_positions.all(),
fees=o.all_fees.all(),
dt_now=o.cancellation_date or (o.expires if o.status == Order.STATUS_EXPIRED else o.datetime),
migrated=True,
)
else:
tn = o.create_transactions(
positions=o.all_positions.all(),
fees=o.all_fees.all(),
dt_now=now(),
migrated=True,
)
if tn:
t += 1
time.sleep(0)
pbar.update(1)
last_pk = batch[-1].pk
self.stderr.write(self.style.SUCCESS(f'Created transactions for {t} orders.'))

View File

@@ -103,7 +103,7 @@ class Command(BaseCommand):
with language(locale), override(timezone):
for receiver, response in signal_result:
ex = response(e, report_status)
ex = response(e, o, report_status)
if ex.identifier == options['export_provider']:
params = json.loads(options.get('parameters') or '{}')
with open(options['output_file'], 'wb') as f:

View File

@@ -32,53 +32,11 @@
# 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.
"""
Django, for theoretically very valid reasons, creates migrations for *every single thing*
we change on a model. Even the `help_text`! This makes sense, as we don't know if any
database backend unknown to us might actually use this information for its database schema.
However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
certain that some changes to models will never require a change to the database. In this case,
not creating a migration for certain changes will save us some performance while applying them
*and* allow for a cleaner git history. Win-win!
Only caveat is that we need to do some dirty monkeypatching to achieve it...
"""
from django.core.management.commands.makemigrations import Command as Parent
from django.db import models
from django.db.migrations.operations import models as modelops
from django_countries.fields import CountryField
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name_plural")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("ordering")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("get_latest_by")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
IGNORED_ATTRS = [
# (field type, attribute name, banlist of field sub-types)
(models.Field, 'verbose_name', []),
(models.Field, 'help_text', []),
(models.Field, 'validators', []),
(models.Field, 'editable', [models.DateField, models.DateTimeField, models.DateField, models.BinaryField]),
(models.Field, 'blank', [models.DateField, models.DateTimeField, models.AutoField, models.NullBooleanField,
models.TimeField]),
(models.CharField, 'choices', [CountryField])
]
from ._migrations import monkeypatch_migrations
original_deconstruct = models.Field.deconstruct
def new_deconstruct(self):
name, path, args, kwargs = original_deconstruct(self)
for ftype, attr, banlist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in banlist):
kwargs.pop(attr, None)
return name, path, args, kwargs
models.Field.deconstruct = new_deconstruct
monkeypatch_migrations()
class Command(Parent):

View File

@@ -32,12 +32,6 @@
# 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.
"""
Django tries to be helpful by suggesting to run "makemigrations" in red font on every "migrate"
run when there are things we have no migrations for. Usually, this is intended, and running
"makemigrations" can really screw up the environment of a user, so we want to prevent novice
users from doing that by going really dirty and filtering it from the output.
"""
import sys
from django.core.management.base import OutputWrapper
@@ -45,9 +39,15 @@ from django.core.management.commands.migrate import Command as Parent
class OutputFilter(OutputWrapper):
"""
Django tries to be helpful by suggesting to run "makemigrations" in red font on every "migrate"
run when there are things we have no migrations for. Usually, this is intended, and running
"makemigrations" can really screw up the environment of a user, so we want to prevent novice
users from doing that by going really dirty and filtering it from the output.
"""
banlist = (
"Your models have changes that are not yet reflected",
"Run 'manage.py makemigrations' to make new "
"have changes that are not yet reflected",
"re-run 'manage.py migrate'"
)
def write(self, msg, style_func=None, ending=None):

View File

@@ -208,7 +208,7 @@ def _parse_csp(header):
def _render_csp(h):
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items())
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
def _merge_csp(a, b):

View File

@@ -0,0 +1,38 @@
# Generated by Django 3.2.2 on 2021-05-23 13:22
import django.db.models.deletion
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0195_auto_20210622_1457'),
]
operations = [
migrations.AddField(
model_name='invoiceaddress',
name='customer',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invoice_addresses', to='pretixbase.customer'),
),
migrations.CreateModel(
name='AttendeeProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('attendee_name_cached', models.CharField(max_length=255, null=True)),
('attendee_name_parts', models.JSONField(default=dict)),
('attendee_email', models.EmailField(max_length=254, null=True)),
('company', models.CharField(max_length=255, null=True)),
('street', models.TextField(null=True)),
('zipcode', models.CharField(max_length=30, null=True)),
('city', models.CharField(max_length=255, null=True)),
('country', pretix.helpers.countries.FastCountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2, null=True)),
('state', models.CharField(max_length=255, null=True)),
('answers', models.JSONField(default=list)),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attendee_profiles', to='pretixbase.customer')),
],
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 3.2.4 on 2021-09-14 08:14
from django.db import migrations, models
import pretix.base.models.fields
import pretix.base.models.items
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0196_auto_20210523_1322'),
]
operations = [
migrations.AddField(
model_name='itemvariation',
name='available_from',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='itemvariation',
name='available_until',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='itemvariation',
name='hide_without_voucher',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='itemvariation',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=pretix.base.models.items._all_sales_channels_identifiers),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.2.4 on 2021-09-30 10:25
from datetime import datetime
from django.db import migrations, models
from pytz import UTC
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0197_auto_20210914_0814'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='sent_to_customer',
field=models.DateTimeField(blank=True, null=True, default=UTC.localize(datetime(1970, 1, 1, 0, 0, 0, 0))),
preserve_default=False,
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-10-05 10:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0198_invoice_sent_to_customer'),
]
operations = [
migrations.AddField(
model_name='item',
name='require_membership_hidden',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='itemvariation',
name='require_membership_hidden',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 3.2.4 on 2021-10-18 10:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0199_auto_20211005_1050'),
]
operations = [
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
('datetime', models.DateTimeField(db_index=True)),
('migrated', models.BooleanField(default=False)),
('positionid', models.PositiveIntegerField(default=1, null=True)),
('count', models.IntegerField(default=1)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('tax_rate', models.DecimalField(decimal_places=2, max_digits=7)),
('tax_value', models.DecimalField(decimal_places=2, max_digits=10)),
('fee_type', models.CharField(max_length=100, null=True)),
('internal_type', models.CharField(max_length=255, null=True)),
('item', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.item')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions', to='pretixbase.order')),
('subevent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.subevent')),
('tax_rule', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.taxrule')),
('variation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.itemvariation')),
],
options={
'ordering': ('datetime', 'pk'),
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-11-03 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0200_transaction'),
]
operations = [
migrations.AddField(
model_name='invoiceline',
name='event_location',
field=models.TextField(null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.9 on 2021-11-04 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0201_invoiceline_event_location'),
]
operations = [
migrations.AddField(
model_name='user',
name='needs_password_change',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.2 on 2021-11-08 07:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0202_user_needs_password_change'),
]
operations = [
migrations.AddField(
model_name='orderposition',
name='is_bundled',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 3.2.2 on 2021-11-08 07:51
from django.db import migrations, models
from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce
def fill_is_bundled(apps, schema_editor):
# We cannot really know if a position was bundled or an add-on, but we can at least guess
ItemBundle = apps.get_model("pretixbase", "ItemBundle")
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
for ib in ItemBundle.objects.iterator():
OrderPosition.all.alias(
pos_earlier=Coalesce(Subquery(
OrderPosition.all.filter(
canceled=False,
addon_to=OuterRef('addon_to'),
item=ib.bundled_item,
variation=ib.bundled_variation,
positionid__lt=OuterRef('positionid'),
).values('addon_to').order_by().annotate(c=Count('*')).values('c'),
output_field=models.IntegerField()
), 0)
).filter(
canceled=False,
addon_to__item=ib.base_item,
item=ib.bundled_item,
variation=ib.bundled_variation,
pos_earlier__lt=ib.count,
).update(
is_bundled=True
)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0203_orderposition_is_bundled'),
]
operations = [
migrations.RunPython(
fill_is_bundled,
migrations.RunPython.noop,
),
]

View File

@@ -42,8 +42,9 @@ from .notifications import NotificationSetting
from .orders import (
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
QuestionAnswer, RevokedTicketSecret, cachedcombinedticket_name,
cachedticket_name, generate_position_secret, generate_secret,
QuestionAnswer, RevokedTicketSecret, Transaction,
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
generate_secret,
)
from .organizer import (
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,

View File

@@ -0,0 +1,113 @@
#
# 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/>.
#
"""
This module contains helper functions that are supposed to call out code paths missing calls to
``Order.create_transaction()`` by actively breaking them. Read the docstring of the ``Transaction`` class for a
detailed reasoning why this exists.
"""
import inspect
import logging
import os
import threading
from django.conf import settings
from django.db import transaction
dirty_transactions = threading.local()
logger = logging.getLogger(__name__)
fail_loudly = os.getenv('PRETIX_DIRTY_TRANSACTIONS_QUIET', 'false' if settings.DEBUG else 'true') not in ('true', 'True', 'on', '1')
class DirtyTransactionsForOrderException(Exception):
pass
def _fail(message):
if fail_loudly:
raise DirtyTransactionsForOrderException(message)
else:
if settings.SENTRY_ENABLED:
import sentry_sdk
sentry_sdk.capture_message(message, "fatal")
logger.warning(message, stack_info=True)
def _check_for_dirty_orders():
if getattr(dirty_transactions, 'order_ids', None) is None:
dirty_transactions.order_ids = set()
try:
if dirty_transactions.order_ids and dirty_transactions.order_ids != {None}:
_fail(
f"In the transaction that just ended, you created or modified an Order, OrderPosition, or OrderFee "
f"object in a way that you should have called `order.create_transactions()` afterwards. The transaction "
f"still went through and your data can be fixed with the `create_order_transactions` management command "
f"but you should update your code to prevent this from happening. Affected order IDs: {dirty_transactions.order_ids}"
)
finally:
dirty_transactions.order_ids.clear()
def _transactions_mark_order_dirty(order_id, using=None):
if "PYTEST_CURRENT_TEST" in os.environ:
# We don't care about Order.objects.create() calls in test code so let's try to figure out if this is test code
# or not.
for frame in inspect.stack():
if 'pretix/base/models/orders' in frame.filename:
continue
elif 'test_' in frame.filename or 'conftest.py in frame.filename':
return
elif 'pretix/' in frame.filename or 'pretix_' in frame.filename:
# This went through non-test code, let's consider it non-test
break
if order_id is None:
return
conn = transaction.get_connection(using)
if not conn.in_atomic_block:
_fail(
"You modified an Order, OrderPosition, or OrderFee object in a way that should create "
"a new Transaction object within the same database transaction, however you are not "
"doing it inside a database transaction!"
)
if getattr(dirty_transactions, 'order_ids', None) is None:
dirty_transactions.order_ids = set()
if _check_for_dirty_orders not in [func for savepoint_id, func in conn.run_on_commit]:
transaction.on_commit(_check_for_dirty_orders, using)
dirty_transactions.order_ids.clear() # This is necessary to clean up after old threads with rollbacked transactions
dirty_transactions.order_ids.add(order_id)
def _transactions_mark_order_clean(order_id):
if getattr(dirty_transactions, 'order_ids', None) is None:
dirty_transactions.order_ids = set()
try:
dirty_transactions.order_ids.remove(order_id)
except KeyError:
pass

View File

@@ -113,6 +113,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:type date_joined: datetime
:param locale: The user's preferred locale code.
:type locale: str
:param needs_password_change: Whether this user's password needs to be changed.
:type needs_password_change: bool
:param timezone: The user's preferred timezone.
:type timezone: str
"""
@@ -130,6 +132,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
verbose_name=_('Is site admin'))
date_joined = models.DateTimeField(auto_now_add=True,
verbose_name=_('Date joined'))
needs_password_change = models.BooleanField(default=False,
verbose_name=_('Force user to select a new password'))
locale = models.CharField(max_length=50,
choices=settings.LANGUAGES,
default=settings.LANGUAGE_CODE,

View File

@@ -19,19 +19,22 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import pycountry
from django.conf import settings
from django.contrib.auth.hashers import (
check_password, is_password_usable, make_password,
)
from django.db import models
from django.db.models import F, Q
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.banlist import banned
from pretix.base.models.base import LoggedModel
from pretix.base.models.organizer import Organizer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.helpers.countries import FastCountryField
class Customer(LoggedModel):
@@ -88,6 +91,8 @@ class Customer(LoggedModel):
self.all_logentries().update(data={}, shredded=True)
self.orders.all().update(customer=None)
self.memberships.all().update(attendee_name_parts=None)
self.attendee_profiles.all().delete()
self.invoice_addresses.all().delete()
@scopes_disabled()
def assign_identifier(self):
@@ -174,3 +179,94 @@ class Customer(LoggedModel):
continue
ctx['name_%s' % f] = self.name_parts.get(f, '')
return ctx
@property
def stored_addresses(self):
return self.invoice_addresses(manager='profiles')
def usable_memberships(self, for_event, testmode=False):
return self.memberships.active(for_event).with_usages().filter(
Q(membership_type__max_usages__isnull=True) | Q(usages__lt=F('membership_type__max_usages')),
testmode=testmode,
)
class AttendeeProfile(models.Model):
customer = models.ForeignKey(
Customer,
related_name='attendee_profiles',
on_delete=models.CASCADE
)
attendee_name_cached = models.CharField(
max_length=255,
verbose_name=_("Attendee name"),
blank=True, null=True,
)
attendee_name_parts = models.JSONField(
blank=True, default=dict
)
attendee_email = models.EmailField(
verbose_name=_("Attendee email"),
blank=True, null=True,
)
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
country = FastCountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
answers = models.JSONField(default=list)
objects = ScopedManager(organizer='customer__organizer')
@property
def attendee_name(self):
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
@property
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return self.state
@property
def state_for_address(self):
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
if not self.state or str(self.country) not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return ""
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.country)][1] == 'long':
return self.state_name
return self.state
def describe(self):
from .items import Question
from .orders import QuestionAnswer
parts = [
self.attendee_name,
self.attendee_email,
self.company,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name,
]
for a in self.answers:
value = a.get('value')
try:
value = ", ".join(value.values())
except AttributeError:
value = str(value)
answer = QuestionAnswer(question=Question(type=a.get('question_type')), answer=value)
val = str(answer)
parts.append(f'{a["field_label"]}: {val}')
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])

View File

@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import logging
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
@@ -56,6 +57,7 @@ from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext, gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
@@ -73,6 +75,8 @@ from pretix.helpers.thumb import get_thumbnail
from ..settings import settings_hierarkey
from .organizer import Organizer, Team
logger = logging.getLogger(__name__)
class EventMixin:
def clean(self):
@@ -142,7 +146,7 @@ class EventMixin:
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
)
def get_date_range_display(self, tz=None, force_show_end=False) -> str:
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False) -> str:
"""
Returns a formatted string containing the start date and the end date
of the event with respect to the current locale and to the ``show_date_to``
@@ -150,8 +154,40 @@ class EventMixin:
"""
tz = tz or self.timezone
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
if as_html:
return format_html(
"<time datetime=\"{}\">{}</time>",
_date(self.date_from.astimezone(tz), "Y-m-d"),
_date(self.date_from.astimezone(tz), "DATE_FORMAT"),
)
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz))
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz), as_html)
def get_date_range_display_as_html(self, tz=None, force_show_end=False) -> str:
return self.get_date_range_display(tz, force_show_end, as_html=True)
def get_time_range_display(self, tz=None, force_show_end=False) -> str:
"""
Returns a formatted string containing the start time and sometimes the end time
of the event with respect to the current locale and to the ``show_date_to``
setting. Dates are not shown. This is usually used in combination with get_date_range_display
"""
tz = tz or self.timezone
show_date_to = self.date_to and (self.settings.show_date_to or force_show_end) and (
# Show date to if start and end are on the same day ("08:00-10:00")
self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() or
# Show date to if start and end are on consecutive days and less than 24h ("23:00-03:00")
(self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() + timedelta(days=1) and
self.date_to.astimezone(tz).time() < self.date_from.astimezone(tz).time())
# Do not show end time if this is a 5-day event because there's no way to make it understandable
)
if show_date_to:
return '{} {}'.format(
_date(self.date_from.astimezone(tz), "TIME_FORMAT"),
_date(self.date_to.astimezone(tz), "TIME_FORMAT"),
)
return _date(self.date_from.astimezone(tz), "TIME_FORMAT")
@property
def timezone(self):
@@ -242,6 +278,10 @@ class EventMixin:
).values('items')
sq_active_variation = ItemVariation.objects.filter(
Q(active=True)
& Q(sales_channels__contains=channel)
& Q(hide_without_voucher=False)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(item__active=True)
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
@@ -492,7 +532,7 @@ class Event(EventMixin, LoggedModel):
default=False
)
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='events')
related_name='events', verbose_name=_('Seating plan'))
last_modified = models.DateTimeField(
auto_now=True, db_index=True
@@ -525,6 +565,8 @@ class Event(EventMixin, LoggedModel):
self.settings.ticketoutput_pdf__enabled = True
self.settings.ticketoutput_passbook__enabled = True
self.settings.event_list_type = 'calendar'
self.settings.invoice_email_attachment = True
self.settings.name_scheme = 'given_family'
@property
def social_image(self):
@@ -534,9 +576,17 @@ class Event(EventMixin, LoggedModel):
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
og_file = self.settings.get('og_image', as_type=str, default='')[7:]
if og_file:
img = get_thumbnail(og_file, '1200').thumb.url
try:
img = get_thumbnail(og_file, '1200').thumb.url
except:
logger.exception(f'Failed to create thumbnail of {og_file}')
img = default_storage.url(og_file)
elif logo_file:
img = get_thumbnail(logo_file, '5000x120').thumb.url
try:
img = get_thumbnail(logo_file, '5000x1200').thumb.url
except:
logger.exception(f'Failed to create thumbnail of {logo_file}')
img = default_storage.url(logo_file)
if img:
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
@@ -741,7 +791,9 @@ class Event(EventMixin, LoggedModel):
ia.bundled_variation = variation_map[ia.bundled_variation.pk]
ia.save()
quota_map = {}
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
quota_map[q.pk] = q
items = list(q.items.all())
vars = list(q.variations.all())
oldid = q.pk
@@ -865,7 +917,7 @@ class Event(EventMixin, LoggedModel):
event_copy_data.send(
sender=self, other=other,
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
question_map=question_map, checkin_list_map=checkin_list_map
question_map=question_map, checkin_list_map=checkin_list_map, quota_map=quota_map,
)
if has_custom_style:
@@ -1004,6 +1056,11 @@ class Event(EventMixin, LoggedModel):
| Q(date_to__gte=now() - timedelta(hours=24))
)
) # order_by doesn't make sense with I18nField
if ordering in ("date_ascending", "date_descending"):
# if primary order is by date, then order in database
# this allows to limit/slice results
return subevs.order_by(*orderfields)
for f in reversed(orderfields):
if f.startswith('-'):
subevs = sorted(subevs, key=attrgetter(f[1:]), reverse=True)
@@ -1113,15 +1170,18 @@ class Event(EventMixin, LoggedModel):
self.items.all().delete()
self.subevents.all().delete()
def set_active_plugins(self, modules, allow_restricted=False):
def get_available_plugins(self):
from pretix.base.plugins import get_all_plugins
plugins_active = self.get_plugins()
plugins_available = {
return {
p.module: p for p in get_all_plugins(self)
if not p.name.startswith('.') and getattr(p, 'visible', True)
}
def set_active_plugins(self, modules, allow_restricted=False):
plugins_active = self.get_plugins()
plugins_available = self.get_available_plugins()
enable = [m for m in modules if m not in plugins_active and m in plugins_available]
for module in enable:
@@ -1150,6 +1210,10 @@ class Event(EventMixin, LoggedModel):
plugins_active.remove(module)
self.set_active_plugins(plugins_active)
plugins_available = self.get_available_plugins()
if hasattr(plugins_available[module].app, 'uninstalled'):
getattr(plugins_available[module].app, 'uninstalled')(self)
regenerate_css.apply_async(args=(self.pk,))
@staticmethod
@@ -1259,7 +1323,7 @@ class SubEvent(EventMixin, LoggedModel):
verbose_name=_("Frontpage text")
)
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='subevents')
related_name='subevents', verbose_name=_('Seating plan'))
items = models.ManyToManyField('Item', through='SubEventItem')
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
@@ -1369,7 +1433,7 @@ class SubEvent(EventMixin, LoggedModel):
return self.event.currency
def allow_delete(self):
return not self.orderposition_set.exists()
return not self.orderposition_set.exists() and not self.transaction_set.exists()
def delete(self, *args, **kwargs):
clear_cache = kwargs.pop('clear_cache', False)

View File

@@ -159,6 +159,8 @@ class Invoice(models.Model):
# False: The invoice wasn't sent and never will, because sending was not configured at the time of the check.
sent_to_organizer = models.BooleanField(null=True, blank=True)
sent_to_customer = models.DateTimeField(null=True, blank=True)
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
objects = ScopedManager(organizer='event__organizer')
@@ -235,7 +237,7 @@ class Invoice(models.Model):
def _get_invoice_number_from_order(self):
return '{order}-{count}'.format(
order=self.order.code,
count=Invoice.objects.filter(event=self.event, order=self.order).count() + 1,
count=Invoice.objects.filter(event=self.event, prefix=self.prefix, invoice_no__startswith=f"{self.order.code}-", order=self.order).count() + 1,
)
def save(self, *args, **kwargs):
@@ -262,6 +264,7 @@ class Invoice(models.Model):
self.invoice_no = self._get_invoice_number_from_order()
try:
with transaction.atomic():
self.full_invoice_no = self.prefix + self.invoice_no
return super().save(*args, **kwargs)
except DatabaseError:
# Suppress duplicate key errors and try again
@@ -300,6 +303,9 @@ class Invoice(models.Model):
def __repr__(self):
return '<Invoice {} / {}>'.format(self.full_invoice_no, self.pk)
def __str__(self):
return self.full_invoice_no
class InvoiceLine(models.Model):
"""
@@ -323,6 +329,8 @@ class InvoiceLine(models.Model):
:type event_date_from: datetime
:param event_date_to: Event end date of the (sub)event at the time the invoice was created
:type event_date_to: datetime
:param event_location: Event location of the (sub)event at the time the invoice was created
:type event_location: str
:param item: The item this line refers to
:type item: Item
:param variation: The variation this line refers to
@@ -340,6 +348,7 @@ class InvoiceLine(models.Model):
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
event_date_from = models.DateTimeField(null=True)
event_date_to = models.DateTimeField(null=True)
event_location = models.TextField(null=True, blank=True)
item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
attendee_name = models.TextField(null=True, blank=True)

View File

@@ -523,6 +523,12 @@ class Item(LoggedModel):
verbose_name=_('Allowed membership types'),
blank=True,
)
require_membership_hidden = models.BooleanField(
verbose_name=_('Hide without a valid membership'),
help_text=_('Do not show this unless the customer is logged in and has a valid membership. Be aware that '
'this means it will never be visible in the widget.'),
default=False,
)
grant_membership_type = models.ForeignKey(
'MembershipType',
null=True, blank=True,
@@ -687,9 +693,9 @@ class Item(LoggedModel):
return res
def allow_delete(self):
from pretix.base.models.orders import OrderPosition
from pretix.base.models.orders import OrderPosition, Transaction
return not OrderPosition.all.filter(item=self).exists()
return not Transaction.objects.filter(item=self).exists() and not OrderPosition.all.filter(item=self).exists()
@property
def includes_mixed_tax_rate(self):
@@ -736,6 +742,11 @@ class Item(LoggedModel):
return OrderedDict((k, v) for k, v in sorted(data.items(), key=lambda k: k[0]))
def _all_sales_channels_identifiers():
from pretix.base.channels import get_all_sales_channels
return list(get_all_sales_channels().keys())
class ItemVariation(models.Model):
"""
A variation of a product. For example, if your item is 'T-Shirt'
@@ -761,7 +772,7 @@ class ItemVariation(models.Model):
)
value = I18nCharField(
max_length=255,
verbose_name=_('Description')
verbose_name=_('Variation')
)
active = models.BooleanField(
default=True,
@@ -797,6 +808,35 @@ class ItemVariation(models.Model):
verbose_name=_('Membership types'),
blank=True,
)
require_membership_hidden = models.BooleanField(
verbose_name=_('Hide without a valid membership'),
help_text=_('Do not show this unless the customer is logged in and has a valid membership. Be aware that '
'this means it will never be visible in the widget.'),
default=False,
)
available_from = models.DateTimeField(
verbose_name=_("Available from"),
null=True, blank=True,
help_text=_('This variation will not be sold before the given date.')
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This variation will not be sold after the given date.')
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=_all_sales_channels_identifiers,
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
'selected here but not on product level, the variation will not be available.'),
blank=True,
)
hide_without_voucher = models.BooleanField(
verbose_name=_('This variation will only be shown if a voucher matching the product is redeemed.'),
default=False,
help_text=_('This variation will be hidden from the event page until the user enters a voucher '
'that unlocks this variation.')
)
objects = ScopedManager(organizer='item__event__organizer')
@@ -918,16 +958,37 @@ class ItemVariation(models.Model):
return self.position < other.position
def allow_delete(self):
from pretix.base.models.orders import CartPosition, OrderPosition
from pretix.base.models.orders import (
CartPosition, OrderPosition, Transaction,
)
return (
not OrderPosition.objects.filter(variation=self).exists()
not Transaction.objects.filter(variation=self).exists()
and not OrderPosition.objects.filter(variation=self).exists()
and not CartPosition.objects.filter(variation=self).exists()
)
def is_only_variation(self):
return ItemVariation.objects.filter(item=self.item).count() == 1
def is_available_by_time(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or now()
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
return False
return True
def is_available(self, now_dt: datetime=None) -> bool:
"""
Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields
"""
now_dt = now_dt or now()
if not self.active or not self.is_available_by_time(now_dt):
return False
return True
class ItemAddOn(models.Model):
"""
@@ -1051,7 +1112,7 @@ class ItemBundle(models.Model):
)
count = models.PositiveIntegerField(
default=1,
verbose_name=_('Number')
verbose_name=_('Quantity')
)
designated_price = models.DecimalField(
default=Decimal('0.00'), blank=True,
@@ -1620,6 +1681,8 @@ class Quota(LoggedModel):
@staticmethod
def clean_items(event, items, variations):
if not items:
return
for item in items:
if event != item.event:
raise ValidationError(_('One or more items do not belong to this event.'))

View File

@@ -95,6 +95,7 @@ class MembershipQuerySet(models.QuerySet):
def active(self, ev):
return self.filter(
canceled=False,
date_start__lte=ev.date_from,
date_end__gte=ev.date_from
)
@@ -175,7 +176,7 @@ class Membership(models.Model):
else:
dt = now()
return dt >= self.date_start and dt <= self.date_end
return not self.canceled and dt >= self.date_start and dt <= self.date_end
def allow_delete(self):
return self.testmode and not self.orderposition_set.exists()

View File

@@ -75,11 +75,14 @@ from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Customer, User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.locking import NoLockManager
from pretix.base.services.locking import LOCK_TIMEOUT, NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from ...helpers.countries import CachedCountries, FastCountryField
from ._transactions import (
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
)
from .base import LockModel, LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation, Question, QuestionOption, Quota
@@ -262,6 +265,11 @@ class Order(LockModel, LoggedModel):
def __str__(self):
return self.full_code
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'require_approval' not in self.get_deferred_fields() and 'status' not in self.get_deferred_fields():
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
def gracefully_delete(self, user=None, auth=None):
from . import GiftCard, GiftCardTransaction, Membership, Voucher
@@ -289,6 +297,7 @@ class Order(LockModel, LoggedModel):
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order=self).delete()
OrderFee.all.filter(order=self).delete()
Transaction.objects.filter(order=self).delete()
self.refunds.all().delete()
self.payments.all().delete()
self.event.cache.delete('complain_testmode_orders')
@@ -419,6 +428,8 @@ class Order(LockModel, LoggedModel):
is_underpaid=Case(
When(Q(status=Order.STATUS_PAID) & Q(pending_sum_t__gt=1e-8),
then=Value(1)),
When(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__gt=1e-8),
then=Value(1)),
default=Value(0),
output_field=models.IntegerField()
)
@@ -442,7 +453,27 @@ class Order(LockModel, LoggedModel):
self.datetime = now()
if not self.expires:
self.set_expires()
super().save(**kwargs)
is_new = not self.pk
update_fields = kwargs.get('update_fields', [])
if 'require_approval' not in self.get_deferred_fields() and 'status' not in self.get_deferred_fields():
status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
if status_paid_or_pending != self.__initial_status_paid_or_pending:
_transactions_mark_order_dirty(self.pk, using=kwargs.get('using', None))
elif (
not kwargs.get('force_save_with_deferred_fields', None) and
(not update_fields or ('require_approval' not in update_fields and 'status' not in update_fields))
):
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
r = super().save(**kwargs)
if is_new:
_transactions_mark_order_dirty(self.pk, using=kwargs.get('using', None))
return r
def touch(self):
self.save(update_fields=['last_modified'])
@@ -541,7 +572,7 @@ class Order(LockModel, LoggedModel):
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * (self.total - fee)
if self.event.settings.cancel_allow_user_paid_keep:
fee += self.event.settings.cancel_allow_user_paid_keep
return round_decimal(fee, self.event.currency)
return round_decimal(min(fee, self.total), self.event.currency)
@property
@scopes_disabled()
@@ -550,6 +581,7 @@ class Order(LockModel, LoggedModel):
Returns whether or not this order can be canceled by the user.
"""
from .checkin import Checkin
from .items import ItemAddOn
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID) or not self.count_positions:
return False
@@ -575,7 +607,10 @@ class Order(LockModel, LoggedModel):
if self.user_change_deadline and now() > self.user_change_deadline:
return False
return self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])
return (
(self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])) or
(self.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
)
@property
@scopes_disabled()
@@ -783,6 +818,7 @@ class Order(LockModel, LoggedModel):
def ticket_download_available(self):
return self.event.settings.ticket_download and (
self.event.settings.ticket_download_date is None
or self.ticket_download_date is None
or now() > self.ticket_download_date
) and (
self.status == Order.STATUS_PAID
@@ -996,6 +1032,59 @@ class Order(LockModel, LoggedModel):
continue
yield op
def create_transactions(self, is_new=False, positions=None, fees=None, dt_now=None, migrated=False,
_backfill_before_cancellation=False, save=True):
dt_now = dt_now or now()
# Count the transactions we already have
current_transaction_count = Counter()
if not is_new:
for t in Transaction.objects.filter(order=self): # do not use related manager, we want to avoid cached data
current_transaction_count[Transaction.key(t)] += t.count
# Count the transactions we'd actually need
target_transaction_count = Counter()
if (_backfill_before_cancellation or self.status in (Order.STATUS_PENDING, Order.STATUS_PAID)) and not self.require_approval:
positions = self.positions.all() if positions is None else positions
for p in positions:
if p.canceled and not _backfill_before_cancellation:
continue
target_transaction_count[Transaction.key(p)] += 1
fees = self.fees.all() if fees is None else fees
for f in fees:
if f.canceled and not _backfill_before_cancellation:
continue
target_transaction_count[Transaction.key(f)] += 1
keys = set(target_transaction_count.keys()) | set(current_transaction_count.keys())
create = []
for k in keys:
positionid, itemid, variationid, subeventid, price, taxrate, taxruleid, taxvalue, feetype, internaltype = k
d = target_transaction_count[k] - current_transaction_count[k]
if d:
create.append(Transaction(
order=self,
datetime=dt_now,
migrated=migrated,
positionid=positionid,
count=d,
item_id=itemid,
variation_id=variationid,
subevent_id=subeventid,
price=price,
tax_rate=taxrate,
tax_rule_id=taxruleid,
tax_value=taxvalue,
fee_type=feetype,
internal_type=internaltype,
))
create.sort(key=lambda t: (0 if t.count < 0 else 1, t.positionid or 0))
if save:
Transaction.objects.bulk_create(create)
_transactions_mark_order_clean(self.pk)
return create
def answerfile_name(instance, filename: str) -> str:
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
@@ -1221,6 +1310,7 @@ class AbstractPosition(models.Model):
seat = models.ForeignKey(
'Seat', null=True, blank=True, on_delete=models.PROTECT
)
is_bundled = models.BooleanField(default=False)
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
@@ -1457,7 +1547,7 @@ class OrderPayment(models.Model):
return self.order.event.get_payment_providers(cached=True).get(self.provider)
@transaction.atomic()
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
from pretix.base.signals import order_paid
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force)
if can_be_paid is not True:
@@ -1465,6 +1555,7 @@ class OrderPayment(models.Model):
'message': can_be_paid
}, user=user, auth=auth)
raise Quota.QuotaExceededException(can_be_paid)
status_change = self.order.status != Order.STATUS_PENDING
self.order.status = Order.STATUS_PAID
self.order.save(update_fields=['status'])
@@ -1478,6 +1569,8 @@ class OrderPayment(models.Model):
if overpaid:
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
order_paid.send(self.order.event, order=self.order)
if status_change:
self.order.create_transactions()
def fail(self, info=None, user=None, auth=None):
"""
@@ -1530,10 +1623,6 @@ class OrderPayment(models.Model):
:type mail_text: str
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
from pretix.base.services.invoices import (
generate_invoice, invoice_qualified,
)
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
@@ -1577,7 +1666,15 @@ class OrderPayment(models.Model):
))
return
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum)
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_refund_sum=0):
from pretix.base.services.invoices import (
generate_invoice, invoice_qualified,
)
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(seconds=LOCK_TIMEOUT * 2)) or not lock:
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
# database transaction is more than enough.
lockfn = NoLockManager
@@ -1585,8 +1682,8 @@ class OrderPayment(models.Model):
lockfn = self.order.event.lock
with lockfn():
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total,
ignore_date=ignore_date)
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total,
ignore_date=ignore_date)
invoice = None
if invoice_qualified(self.order):
@@ -1905,6 +2002,7 @@ class OrderFee(models.Model):
FEE_TYPE_SHIPPING = "shipping"
FEE_TYPE_SERVICE = "service"
FEE_TYPE_CANCELLATION = "cancellation"
FEE_TYPE_INSURANCE = "insurance"
FEE_TYPE_OTHER = "other"
FEE_TYPE_GIFTCARD = "giftcard"
FEE_TYPES = (
@@ -1912,6 +2010,7 @@ class OrderFee(models.Model):
(FEE_TYPE_SHIPPING, _("Shipping fee")),
(FEE_TYPE_SERVICE, _("Service fee")),
(FEE_TYPE_CANCELLATION, _("Cancellation fee")),
(FEE_TYPE_INSURANCE, _("Insurance fee")),
(FEE_TYPE_OTHER, _("Other fees")),
(FEE_TYPE_GIFTCARD, _("Gift card")),
)
@@ -1953,6 +2052,12 @@ class OrderFee(models.Model):
def net_value(self):
return self.value - self.tax_value
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.get_deferred_fields():
self.__initial_transaction_key = Transaction.key(self)
self.__initial_canceled = self.canceled
def __str__(self):
if self.description:
return '{} - {}'.format(self.get_fee_type_display(), self.description)
@@ -1991,6 +2096,15 @@ class OrderFee(models.Model):
if self.tax_rate is None:
self._calculate_tax()
self.order.touch()
if not self.get_deferred_fields():
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
_transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', None))
elif not kwargs.get('force_save_with_deferred_fields', None):
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
return super().save(*args, **kwargs)
def delete(self, **kwargs):
@@ -2005,7 +2119,7 @@ class OrderPosition(AbstractPosition):
AbstractPosition.
The default ``OrderPosition.objects`` manager only contains fees that are not ``canceled``. If
you ant all objects, you need to use ``OrderPosition.all`` instead.
you want all objects, you need to use ``OrderPosition.all`` instead.
:param order: The order this position is a part of
:type order: Order
@@ -2056,6 +2170,12 @@ class OrderPosition(AbstractPosition):
all = ScopedManager(organizer='order__event__organizer')
objects = ActivePositionManager()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.get_deferred_fields():
self.__initial_transaction_key = Transaction.key(self)
self.__initial_canceled = self.canceled
class Meta:
verbose_name = _("Order position")
verbose_name_plural = _("Order positions")
@@ -2099,6 +2219,7 @@ class OrderPosition(AbstractPosition):
op._calculate_tax()
op.positionid = i + 1
op.save()
ops.append(op)
cp_mapping[cartpos.pk] = op
for answ in cartpos.answers.all():
answ.orderposition = op
@@ -2164,6 +2285,14 @@ class OrderPosition(AbstractPosition):
if not self.pseudonymization_id:
self.assign_pseudonymization_id()
if not self.get_deferred_fields():
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
_transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', None))
elif not kwargs.get('force_save_with_deferred_fields', None):
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
return super().save(*args, **kwargs)
@scopes_disabled()
@@ -2207,7 +2336,7 @@ class OrderPosition(AbstractPosition):
:param attach_ical: Attach relevant ICS files
"""
from pretix.base.services.mail import (
SendMailException, mail, render_mail,
SendMailException, TolerantDict, mail, render_mail,
)
if not self.attendee_email:
@@ -2220,6 +2349,7 @@ class OrderPosition(AbstractPosition):
recipient = self.attendee_email
try:
email_content = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
@@ -2258,6 +2388,151 @@ class OrderPosition(AbstractPosition):
)
class Transaction(models.Model):
"""
Transactions are a data structure that is redundant on the first sight but makes it possible to create good
financial reporting.
To understand this, think of "orders" as something like a contractual relationship between the organizer and the
customer which requires to customer to pay some money and the organizer to provide a ticket.
The ``Order``, ``OrderPosition``, and ``OrderFee`` models combined give a representation of the current contractual
status of this relationship, i.e. how much and what is owed. The ``OrderPayment`` and ``OrderRefund`` models indicate
the "other side" of the relationship, i.e. how much of the financial obligation has been met so far.
However, while ``OrderPayment`` and ``OrderRefund`` objects are "final" and no longer change once they reached their
final state, ``Order``, ``OrderPosition`` and ``OrderFee`` are highly mutable and can change at any time, e.g. if
the customer moves their booking to a different day or a discount is applied retroactively.
Therefore those models can be used to answer the question "how many tickets of type X have been sold for my event
as of today?" but they cannot accurately answer the question "how many tickets of type X have been sold for my event
as of last month?" because they lack this kind of historical information.
Transactions help here because they are "immutable copies" or "modification records" of call positions and fees
at the time of their creation and change. They only record data that is usually relevant for financial reporting,
such as amounts, prices, products and dates involved. They do not record data like attendee names etc.
Even before the introduction of the Transaction Model pretix *did* store historical data for auditability in the
LogEntry model. However, it's almost impossible to do efficient reporting on that data.
Transactions should never be generated manually but only through the ``order.create_transactions()``
method which should be called **within the same database transaction**.
The big downside of this approach is that you need to remember to update transaction records every time you change
or create orders in new code paths. The mechanism introduced in ``pretix.base.models._transactions`` as well as
the ``save()`` methods of ``Order``, ``OrderPosition`` and ``OrderFee`` intends to help you notice if you missed
it. The only thing this *doesn't* catch is usage of ``OrderPosition.objects.bulk_create`` (and likewise for
``bulk_update`` and ``OrderFee``).
:param id: ID of the transaction
:param order: Order the transaction belongs to
:param datetime: Date and time of the transaction
:param migrated: Whether this object was reconstructed because the order was created before transactions where introduced
:param positionid: Affected Position ID, in case this transaction represents a change in an order position
:param count: An amount, multiplicator for price etc. For order positions this can *currently* only be -1 or +1, for
fees it can also be more in special cases
:param item: ``Item``, in case this transaction represents a change in an order position
:param variation: ``ItemVariation``, in case this transaction represents a change in an order position
:param subevent: ``subevent``, in case this transaction represents a change in an order position
:param price: Price of the changed position
:param tax_rate: Tax rate of the changed position
:param tax_rule: Used tax rule
:param tax_value: Tax value in event currency
:param fee_type: Fee type code in case this transaction represents a change in an order fee
:param internal_type: Internal fee type in case this transaction represents a change in an order fee
"""
id = models.BigAutoField(primary_key=True)
order = models.ForeignKey(
Order,
verbose_name=_("Order"),
related_name='transactions',
on_delete=models.PROTECT
)
created = models.DateTimeField(
auto_now_add=True,
db_index=True,
)
datetime = models.DateTimeField(
verbose_name=_("Date"),
db_index=True,
)
migrated = models.BooleanField(
default=False
)
positionid = models.PositiveIntegerField(default=1, null=True, blank=True)
count = models.IntegerField(
default=1
)
item = models.ForeignKey(
Item,
null=True, blank=True,
verbose_name=_("Item"),
on_delete=models.PROTECT
)
variation = models.ForeignKey(
ItemVariation,
null=True, blank=True,
verbose_name=_("Variation"),
on_delete=models.PROTECT
)
subevent = models.ForeignKey(
SubEvent,
null=True, blank=True,
on_delete=models.PROTECT,
verbose_name=pgettext_lazy("subevent", "Date"),
)
price = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Price")
)
tax_rate = models.DecimalField(
max_digits=7, decimal_places=2,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
'TaxRule',
on_delete=models.PROTECT,
null=True, blank=True
)
tax_value = models.DecimalField(
max_digits=10, decimal_places=2,
verbose_name=_('Tax value')
)
fee_type = models.CharField(
max_length=100, choices=OrderFee.FEE_TYPES, null=True, blank=True
)
internal_type = models.CharField(max_length=255, null=True, blank=True)
class Meta:
ordering = 'datetime', 'pk'
def save(self, *args, **kwargs):
if not self.fee_type and not self.item:
raise ValidationError('Should set either item or fee type')
return super().save(*args, **kwargs)
@staticmethod
def key(obj):
if isinstance(obj, Transaction):
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
elif isinstance(obj, OrderPosition):
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
obj.tax_rule_id, obj.tax_value, None, None)
elif isinstance(obj, OrderFee):
return (None, None, None, None, obj.value, obj.tax_rate,
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
raise ValueError('invalid state') # noqa
@property
def full_price(self):
return self.price * self.count
@property
def full_tax_value(self):
return self.tax_value * self.count
class CartPosition(AbstractPosition):
"""
A cart position is similar to an order line, except that it is not
@@ -2296,7 +2571,6 @@ class CartPosition(AbstractPosition):
max_digits=10, decimal_places=2,
null=True, blank=True
)
is_bundled = models.BooleanField(default=False)
objects = ScopedManager(organizer='event__organizer')
@@ -2329,6 +2603,12 @@ class CartPosition(AbstractPosition):
class InvoiceAddress(models.Model):
last_modified = models.DateTimeField(auto_now=True)
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
customer = models.ForeignKey(
Customer,
related_name='invoice_addresses',
null=True, blank=True,
on_delete=models.CASCADE
)
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
@@ -2340,8 +2620,7 @@ class InvoiceAddress(models.Model):
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'))
vat_id_validated = models.BooleanField(default=False)
custom_field = models.CharField(max_length=255, null=True, blank=True)
internal_reference = models.TextField(
@@ -2355,6 +2634,7 @@ class InvoiceAddress(models.Model):
)
objects = ScopedManager(organizer='order__event__organizer')
profiles = ScopedManager(organizer='customer__organizer')
def save(self, **kwargs):
if self.order:
@@ -2367,6 +2647,20 @@ class InvoiceAddress(models.Model):
self.name_parts = {}
super().save(**kwargs)
def describe(self):
parts = [
self.company,
self.name,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name,
self.vat_id,
self.custom_field,
self.internal_reference,
(_('Beneficiary') + ': ' + self.beneficiary) if self.beneficiary else '',
]
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])
@property
def is_empty(self):
return (
@@ -2402,6 +2696,30 @@ class InvoiceAddress(models.Model):
raise TypeError("Invalid name given.")
return scheme['concatenation'](self.name_parts).strip()
def for_js(self):
d = {}
if self.name_parts:
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
for i, (k, l, w) in enumerate(scheme['fields']):
d[f'name_parts_{i}'] = self.name_parts.get(k) or ''
d.update({
'company': self.company,
'is_business': self.is_business,
'street': self.street,
'zipcode': self.zipcode,
'city': self.city,
'country': str(self.country) if self.country else None,
'state': str(self.state) if self.state else None,
'vat_id': self.vat_id,
'custom_field': self.custom_field,
'internal_reference': self.internal_reference,
'beneficiary': self.beneficiary,
})
return d
def cachedticket_name(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)

View File

@@ -25,7 +25,6 @@ from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import localize
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.fields import I18nCharField
from i18nfield.strings import LazyI18nString
@@ -93,7 +92,7 @@ TAXED_ZERO = TaxedPrice(
EU_COUNTRIES = {
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT',
'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB'
'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
}
EU_CURRENCIES = {
'BG': 'BGN',
@@ -106,17 +105,21 @@ EU_CURRENCIES = {
'RO': 'RON',
'SE': 'SEK'
}
VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH'}
def is_eu_country(cc):
cc = str(cc)
if cc == 'GB':
return now().astimezone(get_current_timezone()).year <= 2020
else:
return cc in EU_COUNTRIES
return cc in EU_COUNTRIES
def ask_for_vat_id(cc):
cc = str(cc)
return cc in VAT_ID_COUNTRIES
def cc_to_vat_prefix(country_code):
country_code = str(country_code)
if country_code == 'GR':
return 'EL'
return country_code
@@ -162,10 +165,13 @@ class TaxRule(LoggedModel):
pass
def allow_delete(self):
from pretix.base.models.orders import OrderFee, OrderPosition
from pretix.base.models.orders import (
OrderFee, OrderPosition, Transaction,
)
return (
not OrderFee.objects.filter(tax_rule=self, order__event=self.event).exists()
not Transaction.objects.filter(tax_rule=self, order__event=self.event).exists()
and not OrderFee.objects.filter(tax_rule=self, order__event=self.event).exists()
and not OrderPosition.all.filter(tax_rule=self, order__event=self.event).exists()
and not self.event.items.filter(tax_rule=self).exists()
and self.event.settings.tax_rate_default != self

View File

@@ -38,7 +38,7 @@ from decimal import ROUND_HALF_UP, Decimal
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator
from django.db import models
from django.db import connection, models
from django.db.models import F, OuterRef, Q, Subquery, Sum
from django.db.models.functions import Coalesce
from django.utils.crypto import get_random_string
@@ -74,6 +74,55 @@ def generate_code(prefix=None):
return code
def generate_codes(organizer, num=1, prefix=None):
codes = set()
batch_size = 500
if 'postgres' in settings.DATABASES['default']['ENGINE']:
batch_size = 5_000
"""
We're trying to check if any of the requested codes already exists in the database. Generally, this is a
SELECT code FROM voucher WHERE code IN (…)
query. However, it turns out that this query get's rather slow if an organizer has lots of vouchers, even
with a organizer with just over 50_000 vouchers, we've seen that creating 20_000 new voucher codes took
just over 30 seconds. There's another way of doing this query on PostgreSQL, which is joining with a
temporary table
SELECT code FROM voucher INNER JOIN (VALUES …) vals(v) ON (code = v)
This is significantly faster, inserting 20_000 vouchers now takes 2-3s instead of 31s on the same dataset.
It's still slow, and removing the JOIN to the event table doesn't significantly speed it up. We might need
an entirely different approach at some point.
"""
while len(codes) < num:
new_codes = set()
for i in range(min(num - len(codes), batch_size)): # Work around SQLite's SQLITE_MAX_VARIABLE_NUMBER
new_codes.add(_generate_random_code(prefix=prefix))
if 'postgres' in settings.DATABASES['default']['ENGINE']:
with connection.cursor() as cursor:
args = list(new_codes) + [organizer.pk]
tmptable = "VALUES " + (", ".join(['(%s)'] * len(new_codes)))
cursor.execute(
f'SELECT code '
f'FROM "{Voucher._meta.db_table}" '
f'INNER JOIN ({tmptable}) vals(v) ON ("{Voucher._meta.db_table}"."code" = "v")'
f'INNER JOIN "{Event._meta.db_table}" ON ("{Voucher._meta.db_table}"."event_id" = "{Event._meta.db_table}"."id") '
f'WHERE "{Event._meta.db_table}"."organizer_id" = %s',
args
)
for row in cursor.fetchall():
new_codes.remove(row[0])
else:
new_codes -= set([v['code'] for v in Voucher.objects.filter(code__in=new_codes).values('code')])
codes |= new_codes
return list(codes)
class Voucher(LoggedModel):
"""
A Voucher can reserve ticket quota or allow special prices.

View File

@@ -21,8 +21,9 @@
#
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models, transaction
from django.db.models import F, Q, Sum
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
@@ -114,9 +115,12 @@ class WaitingListEntry(LoggedModel):
return '%s waits for %s' % (str(self.email), str(self.item))
def clean(self):
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
try:
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
except ObjectDoesNotExist:
raise ValidationError('Invalid input')
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
@@ -147,6 +151,34 @@ class WaitingListEntry(LoggedModel):
)
if availability[1] is None or availability[1] < 1:
raise WaitingListException(_('This product is currently not available.'))
ev = self.subevent or self.event
if ev.seat_category_mappings.filter(product=self.item).exists():
# Generally, we advertise the waiting list to be based on quotas only. This makes it dangerous
# to use in combination with seating plans. If your event has 50 seats and a quota of 50 and
# default settings, everything is fine and the waiting list will work as usual. However, as soon
# as those two numbers diverge, either due to misconfiguration or due to intentional features such
# as our COVID-19 minimum distance feature, things get ugly. Theoretically, there could be
# significant quota available but not a single seat! The waiting list would happily send out vouchers
# which do not work at all. Generally, we consider this a "known bug" and not fixable with the current
# design of the waiting list and seating features.
# However, we've put in a simple safeguard that makes sure the waiting list on its own does not screw
# everything up. Specifically, we will not send out vouchers if the number of available seats is less
# than the number of valid vouchers *issued through the waiting list*. Things can still go wrong due to
# manually created vouchers, manually blocked seats or the minimum distance feature, but this reduces
# the possible damage a bit.
num_free_seats_for_product = ev.free_seats().filter(product=self.item).count()
num_valid_vouchers_for_product = self.event.vouchers.filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
block_quota=True,
item_id=self.item_id,
subevent_id=self.subevent_id,
waitinglistentries__isnull=False
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
if not free_seats:
raise WaitingListException(_('No seat with this product is currently available.'))
if self.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.'))
if '@' not in self.email:

View File

@@ -36,6 +36,8 @@ from django.utils.translation import (
from django_countries import countries
from django_countries.fields import Country
from i18nfield.strings import LazyI18nString
from phonenumber_field.phonenumber import to_python
from phonenumbers import SUPPORTED_REGIONS
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.questions import guess_country
@@ -156,6 +158,29 @@ class EmailColumn(ImportColumn):
order.email = value
class PhoneColumn(ImportColumn):
identifier = 'phone'
verbose_name = gettext_lazy('Phone number')
def clean(self, value, previous_values):
if value:
if self.event.settings.region in SUPPORTED_REGIONS:
region = self.event.settings.region
elif self.event.settings.locale[:2].upper() in SUPPORTED_REGIONS:
region = self.event.settings.locale[:2].upper()
else:
region = None
phone_number = to_python(value, region)
if not phone_number or not phone_number.is_valid():
raise ValidationError(_('Enter a valid phone number.'))
return phone_number
return value
def assign(self, value, order, position, invoice_address, **kwargs):
order.phone = value
class SubeventColumn(ImportColumn):
identifier = 'subevent'
verbose_name = pgettext_lazy('subevents', 'Date')
@@ -653,9 +678,12 @@ class SeatColumn(ImportColumn):
if value:
try:
value = Seat.objects.get(
event=self.event,
seat_guid=value,
subevent=previous_values.get('subevent')
)
except Seat.MultipleObjectsReturned:
raise ValidationError(_('Multiple matching seats were found.'))
except Seat.DoesNotExist:
raise ValidationError(_('No matching seat was found.'))
if not value.is_available() or value in self._cached:
@@ -756,6 +784,7 @@ def get_all_columns(event):
default.append(SubeventColumn(event))
default += [
EmailColumn(event),
PhoneColumn(event),
ItemColumn(event),
Variation(event),
InvoiceAddressCompany(event),

View File

@@ -703,7 +703,7 @@ class BasePaymentProvider:
try:
ia = order.invoice_address
except InvoiceAddress.DoesNotExist:
return True
pass
else:
if str(ia.country) not in restricted_countries:
return False

View File

@@ -99,6 +99,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": "1",
"evaluate": lambda orderposition, order, event: str(orderposition.positionid)
}),
("order_positionid", {
"label": _("Order code and position number"),
"editor_sample": "A1B2C-1",
"evaluate": lambda orderposition, order, event: f"{orderposition.order.code}-{orderposition.positionid}"
}),
("item", {
"label": _("Product name"),
"editor_sample": _("Sample product"),
@@ -429,7 +434,7 @@ def images_from_questions(sender, *args, **kwargs):
except IndexError:
pass
else:
a = op.answers.filter(question_id=question_id).first()
a = op.answers.filter(question_id=question_id).first() or a
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")):
return None
@@ -469,7 +474,7 @@ def variables_from_questions(sender, *args, **kwargs):
except IndexError:
pass
else:
a = op.answers.filter(question_id=question_id).first()
a = op.answers.filter(question_id=question_id).first() or a
if not a:
return ""

View File

@@ -283,13 +283,16 @@ class CartManager:
if op.item.require_voucher and op.voucher is None:
raise CartError(error_messages['voucher_required'])
if op.item.hide_without_voucher and (op.voucher is None or not op.voucher.show_hidden_items):
if (
(op.item.hide_without_voucher or (op.variation and op.variation.hide_without_voucher)) and
(op.voucher is None or not op.voucher.show_hidden_items)
):
raise CartError(error_messages['voucher_required'])
if not op.item.is_available() or (op.variation and not op.variation.active):
if not op.item.is_available() or (op.variation and not op.variation.is_available()):
raise CartError(error_messages['unavailable'])
if self._sales_channel not in op.item.sales_channels:
if self._sales_channel not in op.item.sales_channels or (op.variation and self._sales_channel not in op.variation.sales_channels):
raise CartError(error_messages['unavailable'])
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():

View File

@@ -39,7 +39,7 @@ import dateutil
import dateutil.parser
import pytz
from django.core.files import File
from django.db import transaction
from django.db import IntegrityError, transaction
from django.db.models import (
BooleanField, Count, ExpressionWrapper, F, IntegerField, OuterRef, Q,
Subquery, Value,
@@ -512,6 +512,21 @@ class RequiredQuestionsError(Exception):
def _save_answers(op, answers, given_answers):
def _create_answer(question, answer):
try:
return op.answers.create(question=question, answer=answer)
except IntegrityError:
# Since we prefill ``field.answer`` at form creation time, there's a possible race condition
# here if the user submits their scan a second time while the first one is still running,
# thus leading to duplicate QuestionAnswer objects. Since Django doesn't support UPSERT, the "proper"
# fix would be a transaction with select_for_update(), or at least fetching using get_or_create here
# again. However, both of these approaches have a significant performance overhead for *all* requests,
# while the issue happens very very rarely. So we opt for just catching the error and retrying properly.
qa = op.answers.get(question=question)
qa.answer = answer
qa.save(update_fields=['answer'])
qa.options.clear()
written = False
for q, a in given_answers.items():
if not a:
@@ -528,7 +543,7 @@ def _save_answers(op, answers, given_answers):
written = True
qa.options.clear()
else:
qa = op.answers.create(question=q, answer=str(a.answer))
qa = _create_answer(question=q, answer=str(a.answer))
qa.options.add(a)
elif isinstance(a, list):
if q in answers:
@@ -538,13 +553,13 @@ def _save_answers(op, answers, given_answers):
written = True
qa.options.clear()
else:
qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a]))
qa = _create_answer(question=q, answer=", ".join([str(o) for o in a]))
qa.options.add(*a)
elif isinstance(a, File):
if q in answers:
qa = answers[q]
else:
qa = op.answers.create(question=q, answer=str(a))
qa = _create_answer(question=q, answer=str(a))
qa.file.save(os.path.basename(a.name), a, save=False)
qa.answer = 'file://' + qa.file.name
qa.save()
@@ -555,7 +570,7 @@ def _save_answers(op, answers, given_answers):
qa.answer = str(a)
qa.save()
else:
op.answers.create(question=q, answer=str(a))
_create_answer(question=q, answer=str(a))
written = True
if written:
@@ -567,7 +582,7 @@ def _save_answers(op, answers, given_answers):
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
raw_barcode=None):
raw_barcode=None, from_revoked_secret=False):
"""
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
not valid at this time.
@@ -582,6 +597,11 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
:param nonce: A random nonce to prevent race conditions.
:param datetime: The datetime of the checkin, defaults to now.
"""
# !!!!!!!!!
# Update doc/images/checkin_online.puml if you make substantial changes here!
# !!!!!!!!!
dt = datetime or now()
if op.canceled or op.order.status not in (Order.STATUS_PAID, Order.STATUS_PENDING):
@@ -649,15 +669,16 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if isinstance(auth, Device):
device = auth
last_ci = op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce').first()
last_cis = list(op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce'))
entry_allowed = (
type == Checkin.TYPE_EXIT or
clist.allow_multiple_entries or
last_ci is None or
(clist.allow_entry_after_exit and last_ci.type == Checkin.TYPE_EXIT)
not last_cis or
all(c.type == Checkin.TYPE_EXIT for c in last_cis) or
(clist.allow_entry_after_exit and last_cis[0].type == Checkin.TYPE_EXIT)
)
if nonce and ((last_ci and last_ci.nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
if nonce and ((last_cis and last_cis[0].nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
return
if entry_allowed or force:
@@ -669,7 +690,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
device=device,
gate=device.gate if device else None,
nonce=nonce,
forced=force and not entry_allowed,
forced=force and (not entry_allowed or from_revoked_secret),
raw_barcode=raw_barcode,
)
op.order.log_action('pretix.event.checkin', data={
@@ -715,7 +736,11 @@ def process_exit_all(sender, **kwargs):
exit_all_at__isnull=False
).select_related('event', 'event__organizer')
for cl in qs:
for p in cl.positions_inside:
positions = cl.positions_inside.filter(
Q(last_exit__isnull=True) | Q(last_exit__lte=cl.exit_all_at),
last_entry__lte=cl.exit_all_at,
)
for p in positions:
with scope(organizer=cl.event.organizer):
ci = Checkin.objects.create(
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
@@ -727,6 +752,9 @@ def process_exit_all(sender, **kwargs):
cl.event.settings.delete(f'autocheckin_dst_hack_{cl.pk}')
try:
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone)
except pytz.exceptions.AmbiguousTimeError:
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone,
is_dst=False)
except pytz.exceptions.NonExistentTimeError:
cl.event.settings.set(f'autocheckin_dst_hack_{cl.pk}', True)
d += timedelta(hours=1)

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