Compare commits

...
Author SHA1 Message Date
Richard Schreiber 2f8e6a2a4b only auto-submit subevent-select when context var "auto_submit" is True 2021-03-30 08:52:44 +02:00
Richard Schreiber c084b91ab3 added columns to filter-form 2021-03-24 21:05:45 +01:00
Martin Gross 8baaa0a8c6 Add select2-subevent picker; display subevent start time 2021-03-24 13:50:27 +01:00
Richard Schreiber 53070f5d4b Cart: fix call to del if attribute is unknown when rendering a form label 2021-03-22 17:48:29 +01:00
Richard Schreiber 5685a349ea fix code style issues - missing whitespace around = operator 2021-03-22 17:05:13 +01:00
Richard Schreiber 1af69d5c76 Cart: Hide attendee information if not provided 2021-03-22 16:38:10 +01:00
Richard Schreiber adddc7a71e A11y: add role=group and labels to multi-widgets (#2006)
* add role=group aria-labelledby to multiwidgets

* remove for-attribute from parent-label for grouped inputs

* add aria-labels to PhoneNumber-fields

* add aria-label to name multi-inputs
2021-03-22 15:19:29 +01:00
Richard Schreiber 11f23c3fd2 [a11y] Improved form error messages, descriptive labels, focusable toggle-link (#2002) 2021-03-19 16:13:25 +01:00
Raphael Michel 954fece6cf Log view: Page size selector 2021-03-19 10:49:03 +01:00
Richard Schreiber 8ef6adc3d5 A11y: make toggle-link for "view other date" focusable 2021-03-19 08:00:41 +01:00
Aksh Gupta 88ba7ab53a Refactor code quality issues (#2001) 2021-03-16 19:13:02 +01:00
Raphael Michel eae55e4b5a Widget: Support button with subevent but without items 2021-03-16 19:01:43 +01:00
Raphael Michel 5ae839f62e Security Profile: Allow badge layouts for POS 2021-03-16 19:01:43 +01:00
Raphael Michel 7314d32422 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4019 of 4019 strings)

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

powered by weblate
2021-03-16 17:30:23 +01:00
Maarten van den Berg 97d6ae8e55 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (4019 of 4019 strings)

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

powered by weblate
2021-03-16 17:30:23 +01:00
Maarten van den Berg 13063cb9d2 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.3% (3993 of 4019 strings)

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

powered by weblate
2021-03-16 17:30:23 +01:00
Raphael Michel 2792813d95 Widget: Fix possible redirect loop 2021-03-16 17:26:20 +01:00
Martin Gross d6aeefdf09 Add force-reactivate checkbox to order (#1997) 2021-03-16 16:49:37 +01:00
Raphael Michel 13056ef477 Widget: Do not prefill field with 0 2021-03-16 16:46:39 +01:00
Raphael Michel 6e2b5eae9a Widget: Open iframe even on mobile (to prevent breakage in WkWebView) 2021-03-16 16:16:59 +01:00
Raphael Michel 4cfb10b254 Widget: Make close icon independent of system font 2021-03-16 12:50:19 +01:00
Raphael Michel ebd336e8cb Use new red color everywhere 2021-03-16 12:17:54 +01:00
Richard Schreiber 1357b010de [a11y] add missing labels on voucher-input and fix input.focus when revealing voucher-input via JS (#1998) 2021-03-16 12:17:47 +01:00
Richard SchreiberandRaphael Michel 09b2e69178 [a11y] Increase contrast on some colors for WCAG conformance (#1996)
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2021-03-16 12:10:37 +01:00
Raphael Michel 5e34032821 Fix #256 -- Allow exact filtering of voucher tags 2021-03-15 16:16:49 +01:00
Richard Schreiber 46cee890f0 QuestionAnswer: Add UNIQUE keys on (orderposition, question) and (cartposition, question) (#1994) 2021-03-15 15:34:33 +01:00
Raphael Michel 4a2ac110b3 Voucher bulk creation: More efficient implementation and async task 2021-03-14 18:19:49 +01:00
Raphael Michel 7eefd3dc59 Recommend upper-case index on pretixbase_voucher.code 2021-03-14 18:04:19 +01:00
Raphael Michel fdca62685c Revert "Update Django to 3.1 as well as other dependencies"
This reverts commit b3c9dca024.
2021-03-12 10:52:02 +01:00
Raphael Michel 7ae38b5e97 Fix TypeError during invoice creation 2021-03-12 10:51:50 +01:00
Raphael Michel 76e9093fea Fix email sending during tests 2021-03-11 22:46:07 +01:00
Raphael Michel b3c9dca024 Update Django to 3.1 as well as other dependencies 2021-03-11 21:59:04 +01:00
Raphael Michel f4710cf019 Add index to documentation 2021-03-11 21:43:27 +01:00
Raphael Michel 5f192fd0ce Remove order status from emails 2021-03-11 17:56:28 +01:00
Raphael Michel a897f60fc5 Fix crash during copying of check-in rules 2021-03-11 12:43:33 +01:00
Raphael Michel 74107781ce Email context: Auto-set position_or_address if position is set 2021-03-10 16:18:46 +01:00
Raphael Michel ad219df7cf Fix incorrect attribute parameter in thumbnailed_file_input 2021-03-10 16:14:06 +01:00
Raphael Michel 002ab4aa06 runperiodic: --exclude 2021-03-09 11:50:19 +01:00
Raphael Michel a84a726185 Try to avoid race condition when sending emails 2021-03-09 09:06:14 +01:00
Raphael Michel 5f58b93c71 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4019 of 4019 strings)

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

powered by weblate
2021-03-08 18:09:09 +01:00
Raphael Michel 3eaaf80c0a Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4019 of 4019 strings)

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

powered by weblate
2021-03-08 18:09:09 +01:00
Raphael Michel 3b5d811b27 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-03-08 17:40:02 +01:00
Raphael Michel f0da2b7233 E-mails: add additional information on order positions 2021-03-08 16:50:38 +01:00
lapor-kris d8d7440b52 Translated on translate.pretix.eu (Slovenian)
Currently translated at 27.1% (1085 of 4002 strings)

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

powered by weblate
2021-03-08 16:50:33 +01:00
Raphael Michel a1ec9fceb0 Quota list exporter: Add subevent information 2021-03-08 14:47:41 +01:00
Raphael Michel 27ff73255b Add new fields to invoice model and API 2021-03-08 14:28:04 +01:00
Raphael Michel bba103156c Allow to cancel an order without creating a cancellation invoice 2021-03-08 11:26:52 +01:00
Raphael Michel f1a98b5c30 Add event_info_text parameters 2021-03-05 17:56:29 +01:00
Raphael Michel 405b3a22e1 Fix bug when changing quotas in subevent bulk editor 2021-03-05 13:05:04 +01:00
Raphael Michel a51c2a36a6 SubEvent search: Fix inconsistent ordering 2021-03-05 11:57:24 +01:00
8e00970f04 Waiting list: Add name and phone number (#1987)
* add name and phone to waitinglist

* add options whether to ask for name/phone in waitinglist

* changed rendermode to checkout and added required-css-class

* changed default to original behaviour to not ask name or phone at all

* add name and phone to list-view and export

* add name and phone to Meta-class so they automagically get saved

* update shredder

* fixed isort

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 19.9% (799 of 3996 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 21.6% (865 of 3996 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 23.8% (955 of 3996 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 26.3% (1051 of 3996 strings)

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

powered by weblate

* add validation to WaitingListSerializer

* update API-description

* fixed test_waitinglist.py

* Revert more of de597ba86

* Paginate list of gift cards

* Change texts on order confirmation page if no attachments are sent

* Update locales

* Added translation on translate.pretix.eu (Sinhala)

* Added translation on translate.pretix.eu (Sinhala)

* Translated on translate.pretix.eu (Sinhala)

Currently translated at 0.4% (18 of 4002 strings)

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

powered by weblate

* Fix initial value of phone number

* add colon to enumeration in description

Co-authored-by: Raphael Michel <michel@rami.io>

* update API-description with null-fields

* add name and phone to waitinglist

* add options whether to ask for name/phone in waitinglist

* changed rendermode to checkout and added required-css-class

* changed default to original behaviour to not ask name or phone at all

* add name and phone to list-view and export

* add name and phone to Meta-class so they automagically get saved

* update shredder

* fixed isort

* add validation to WaitingListSerializer

* update API-description

* fixed test_waitinglist.py

* Fix initial value of phone number

* update API-description with null-fields

* add colon to enumeration in description

Co-authored-by: Raphael Michel <michel@rami.io>

* fixed isort on migration

Co-authored-by: lapor-kris <kristijan.tkalec@posteo.si>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: helabasa <R45XvezA@pm.me>
Co-authored-by: Raphael Michel <michel@rami.io>
2021-03-05 10:02:37 +01:00
Raphael Michel 8ca2fe7707 Stripe: Deal with conflicting settings 2021-03-04 20:13:10 +01:00
Raphael Michel b93e2307d0 CachedFileField: Prevent double upload leading to empty file 2021-03-04 18:11:11 +01:00
Raphael Michel 97f3b72254 CachedFileInput: Fix links to download file 2021-03-04 14:54:11 +01:00
Panawat Wong-kleaw 00a77d3de9 Manual payment: Add amount placeholder (#1990) 2021-03-03 15:04:44 +01:00
helabasa 35d9a0dacf Translated on translate.pretix.eu (Sinhala)
Currently translated at 0.4% (18 of 4002 strings)

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

powered by weblate
2021-03-03 12:53:23 +01:00
helabasa d2e6320e1e Added translation on translate.pretix.eu (Sinhala) 2021-03-03 12:53:23 +01:00
helabasa 671eb902a8 Added translation on translate.pretix.eu (Sinhala) 2021-03-03 12:53:23 +01:00
Raphael Michel be67059099 Update locales 2021-03-02 18:42:39 +01:00
Raphael Michel 6e3791a49e Change texts on order confirmation page if no attachments are sent 2021-03-02 18:36:00 +01:00
Raphael Michel e3bd665093 Paginate list of gift cards 2021-03-02 18:31:05 +01:00
Raphael Michel 748e2bb2fa Revert more of de597ba86 2021-03-02 18:26:01 +01:00
lapor-kris b13b34f00d Translated on translate.pretix.eu (Slovenian)
Currently translated at 26.3% (1051 of 3996 strings)

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

powered by weblate
2021-03-02 11:23:51 +01:00
lapor-kris 641e3216d9 Translated on translate.pretix.eu (Slovenian)
Currently translated at 23.8% (955 of 3996 strings)

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

powered by weblate
2021-03-02 11:23:51 +01:00
lapor-kris c70901c129 Translated on translate.pretix.eu (Slovenian)
Currently translated at 21.6% (865 of 3996 strings)

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

powered by weblate
2021-03-02 11:23:51 +01:00
lapor-kris 460d39b8c2 Translated on translate.pretix.eu (Slovenian)
Currently translated at 19.9% (799 of 3996 strings)

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

powered by weblate
2021-03-02 11:23:51 +01:00
Raphael Michel a9963aead1 Fix import order 2021-03-02 10:54:11 +01:00
160 changed files with 62525 additions and 36609 deletions
+4
View File
@@ -60,6 +60,10 @@ Here is the currently recommended set of commands::
CREATE INDEX CONCURRENTLY pretix_addidx_ia_company CREATE INDEX CONCURRENTLY pretix_addidx_ia_company
ON pretixbase_invoiceaddress ON pretixbase_invoiceaddress
USING gin (upper("company") gin_trgm_ops); USING gin (upper("company") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email_upper
ON public.pretixbase_orderposition (upper((attendee_email)::text));
CREATE INDEX CONCURRENTLY pretix_addidx_voucher_code_upper
ON public.pretixbase_voucher (upper((code)::text));
Also, if you use our ``pretix-shipping`` plugin:: Also, if you use our ``pretix-shipping`` plugin::
+95 -6
View File
@@ -15,8 +15,24 @@ number string Invoice number
order string Order code of the order this invoice belongs to order string Order code of the order this invoice belongs to
is_cancellation boolean ``true``, if this invoice is the cancellation of a is_cancellation boolean ``true``, if this invoice is the cancellation of a
different invoice. different invoice.
invoice_from string Sender address invoice_from_name string Sender address: Name
invoice_to string Receiver address invoice_from string Sender address: Address lines
invoice_from_zipcode string Sender address: ZIP code
invoice_from_city string Sender address: City
invoice_from_country string Sender address: Country code
invoice_from_tax_id string Sender address: Local Tax ID
invoice_from_vat_id string Sender address: EU VAT ID
invoice_to string Full recipient address
invoice_to_company string Recipient address: Company name
invoice_to_name string Recipient address: Person name
invoice_to_street string Recipient address: Address lines
invoice_to_zipcode string Recipient address: ZIP code
invoice_to_city string Recipient address: City
invoice_to_state string Recipient address: State (only used in some countries)
invoice_to_country string Recipient address: Country code
invoice_to_vat_id string Recipient address: EU VAT ID
invoice_to_beneficiary string Invoice beneficiary
custom_field string Custom invoice address field
date date Invoice date date date Invoice date
refers string Invoice number of an invoice this invoice refers to refers string Invoice number of an invoice this invoice refers to
(for example a cancellation refers to the invoice it (for example a cancellation refers to the invoice it
@@ -30,6 +46,31 @@ footer_text string Text to be prin
lines list of objects The actual invoice contents lines list of objects The actual invoice contents
├ position integer Number of the line within an invoice. ├ position integer Number of the line within an invoice.
├ description string Text representing the invoice line (e.g. product name) ├ description string Text representing the invoice line (e.g. product name)
├ item integer Product used to create this line. Note that everything
about the product might have changed since the creation
of the invoice. Can be ``null`` for all invoice lines
created before this field was introduced as well as for
all lines not created by a product (e.g. a shipping or
cancellation fee).
├ variation integer Product variation used to create this line. Note that everything
about the product might have changed since the creation
of the invoice. Can be ``null`` for all invoice lines
created before this field was introduced as well as for
all lines not created by a product (e.g. a shipping or
cancellation fee).
├ event_date_from datetime Start date 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).
├ event_date_to datetime End date 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 end date 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 ├ gross_value money (string) Price including taxes
├ tax_value money (string) Tax amount included ├ tax_value money (string) Tax amount included
├ tax_name string Name of used tax rate (e.g. "VAT") ├ tax_name string Name of used tax rate (e.g. "VAT")
@@ -50,6 +91,12 @@ internal_reference string Customer's refe
The attribute ``lines.number`` has been added. The attribute ``lines.number`` has been added.
.. versionchanged:: 3.17
The attribute ``invoice_to_*``, ``invoice_from_*``, ``custom_field``, ``lines.item``, ``lines.variation``, ``lines.event_date_from``,
``lines.event_date_to``, and ``lines.attendee_name`` have been added.
``refers`` now returns an invoice number including the prefix.
Endpoints Endpoints
--------- ---------
@@ -83,8 +130,24 @@ Endpoints
"number": "SAMPLECONF-00001", "number": "SAMPLECONF-00001",
"order": "ABC12", "order": "ABC12",
"is_cancellation": false, "is_cancellation": false,
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town", "invoice_from_name": "Big Events LLC",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789", "invoice_from": "Demo street 12",
"invoice_from_zipcode":"",
"invoice_from_city":"Demo town",
"invoice_from_country":"US",
"invoice_from_tax_id":"",
"invoice_from_vat_id":"",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
"invoice_to_company": "Sample company",
"invoice_to_name": "John Doe",
"invoice_to_street": "Test street 12",
"invoice_to_zipcode": "12345",
"invoice_to_city": "Testington",
"invoice_to_state": null,
"invoice_to_country": "TE",
"invoice_to_vat_id": "EU123456789",
"invoice_to_beneficiary": "",
"custom_field": null,
"date": "2017-12-01", "date": "2017-12-01",
"refers": null, "refers": null,
"locale": "en", "locale": "en",
@@ -97,6 +160,11 @@ Endpoints
{ {
"position": 1, "position": 1,
"description": "Budget Ticket", "description": "Budget Ticket",
"item": 1234,
"variation": 245,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"attendee_name": null,
"gross_value": "23.00", "gross_value": "23.00",
"tax_value": "0.00", "tax_value": "0.00",
"tax_name": "VAT", "tax_name": "VAT",
@@ -148,8 +216,24 @@ Endpoints
"number": "SAMPLECONF-00001", "number": "SAMPLECONF-00001",
"order": "ABC12", "order": "ABC12",
"is_cancellation": false, "is_cancellation": false,
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town", "invoice_from_name": "Big Events LLC",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789", "invoice_from": "Demo street 12",
"invoice_from_zipcode":"",
"invoice_from_city":"Demo town",
"invoice_from_country":"US",
"invoice_from_tax_id":"",
"invoice_from_vat_id":"",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
"invoice_to_company": "Sample company",
"invoice_to_name": "John Doe",
"invoice_to_street": "Test street 12",
"invoice_to_zipcode": "12345",
"invoice_to_city": "Testington",
"invoice_to_state": null,
"invoice_to_country": "TE",
"invoice_to_vat_id": "EU123456789",
"invoice_to_beneficiary": "",
"custom_field": null,
"date": "2017-12-01", "date": "2017-12-01",
"refers": null, "refers": null,
"locale": "en", "locale": "en",
@@ -162,6 +246,11 @@ Endpoints
{ {
"position": 1, "position": 1,
"description": "Budget Ticket", "description": "Budget Ticket",
"item": 1234,
"variation": 245,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"attendee_name": null,
"gross_value": "23.00", "gross_value": "23.00",
"tax_value": "0.00", "tax_value": "0.00",
"tax_name": "VAT", "tax_name": "VAT",
+3
View File
@@ -13,7 +13,10 @@ Field Type Description
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
id integer Internal ID of the waiting list entry id integer Internal ID of the waiting list entry
created datetime Creation date of the waiting list entry created datetime Creation date of the waiting list entry
name string Name of the user on the waiting list (or ``null``)
name_parts object of strings Decomposition of name of the user (or ``null``)
email string Email address of the user on the waiting list email string Email address of the user on the waiting list
phone string Phone number of the user on the waiting list (or ``null``)
voucher integer Internal ID of the voucher sent to this user. If voucher integer Internal ID of the voucher sent to this user. If
this field is set, the user has been sent a voucher this field is set, the user has been sent a voucher
and is no longer waiting. If it is ``null``, the and is no longer waiting. If it is ``null``, the
+7 -2
View File
@@ -6,8 +6,8 @@ localecompile:
./manage.py compilemessages ./manage.py compilemessages
localegen: localegen:
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" $(LNGS) ./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS) ./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
staticfiles: jsi18n staticfiles: jsi18n
./manage.py collectstatic --noinput ./manage.py collectstatic --noinput
@@ -23,3 +23,8 @@ test:
coverage: coverage:
coverage run -m py.test coverage run -m py.test
npminstall:
mkdir -p pretix/static.dist/node_prefix
npm install --prefix=pretix/static.dist/node_prefix pretix/static/npm_dir/
+3 -1
View File
@@ -10,7 +10,7 @@ class FullAccessSecurityProfile:
class AllowListSecurityProfile: class AllowListSecurityProfile:
allowlist = tuple() allowlist = ()
def is_allowed(self, request): def is_allowed(self, request):
key = (request.method, f"{request.resolver_match.namespace}:{request.resolver_match.url_name}") key = (request.method, f"{request.resolver_match.namespace}:{request.resolver_match.url_name}")
@@ -95,6 +95,8 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:taxrule-list'), ('GET', 'api-v1:taxrule-list'),
('GET', 'api-v1:ticketlayout-list'), ('GET', 'api-v1:ticketlayout-list'),
('GET', 'api-v1:ticketlayoutitem-list'), ('GET', 'api-v1:ticketlayoutitem-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:order-list'), ('GET', 'api-v1:order-list'),
('POST', 'api-v1:order-list'), ('POST', 'api-v1:order-list'),
('GET', 'api-v1:order-detail'), ('GET', 'api-v1:order-detail'),
+7 -1
View File
@@ -309,7 +309,7 @@ class EventSerializer(I18nAwareModelSerializer):
# Item Meta properties # Item Meta properties
if item_meta_properties is not None: if item_meta_properties is not None:
current = [imp for imp in event.item_meta_properties.all()] current = list(event.item_meta_properties.all())
for key, value in item_meta_properties.items(): for key, value in item_meta_properties.items():
prop = self.item_meta_props.get(key) prop = self.item_meta_props.get(key)
if prop in current: if prop in current:
@@ -614,6 +614,11 @@ class EventSettingsSerializer(SettingsSerializer):
'waiting_list_enabled', 'waiting_list_enabled',
'waiting_list_hours', 'waiting_list_hours',
'waiting_list_auto', 'waiting_list_auto',
'waiting_list_names_asked',
'waiting_list_names_required',
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'max_items_per_order', 'max_items_per_order',
'reservation_time', 'reservation_time',
'contact_mail', 'contact_mail',
@@ -624,6 +629,7 @@ class EventSettingsSerializer(SettingsSerializer):
'frontpage_subevent_ordering', 'frontpage_subevent_ordering',
'event_list_type', 'event_list_type',
'frontpage_text', 'frontpage_text',
'event_info_text',
'attendee_names_asked', 'attendee_names_asked',
'attendee_names_required', 'attendee_names_required',
'attendee_emails_asked', 'attendee_emails_asked',
+11 -11
View File
@@ -18,18 +18,18 @@ class FormFieldWrapperField(serializers.Field):
simple_mappings = ( simple_mappings = (
(forms.DateField, serializers.DateField, tuple()), (forms.DateField, serializers.DateField, ()),
(forms.TimeField, serializers.TimeField, tuple()), (forms.TimeField, serializers.TimeField, ()),
(forms.SplitDateTimeField, serializers.DateTimeField, tuple()), (forms.SplitDateTimeField, serializers.DateTimeField, ()),
(forms.DateTimeField, serializers.DateTimeField, tuple()), (forms.DateTimeField, serializers.DateTimeField, ()),
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')), (forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
(forms.FloatField, serializers.FloatField, tuple()), (forms.FloatField, serializers.FloatField, ()),
(forms.IntegerField, serializers.IntegerField, tuple()), (forms.IntegerField, serializers.IntegerField, ()),
(forms.EmailField, serializers.EmailField, tuple()), (forms.EmailField, serializers.EmailField, ()),
(forms.UUIDField, serializers.UUIDField, tuple()), (forms.UUIDField, serializers.UUIDField, ()),
(forms.URLField, serializers.URLField, tuple()), (forms.URLField, serializers.URLField, ()),
(forms.NullBooleanField, serializers.NullBooleanField, tuple()), (forms.NullBooleanField, serializers.NullBooleanField, ()),
(forms.BooleanField, serializers.BooleanField, tuple()), (forms.BooleanField, serializers.BooleanField, ()),
) )
+18 -3
View File
@@ -45,6 +45,14 @@ class CompatibleCountryField(serializers.Field):
return instance.country_old return instance.country_old
class CountryField(serializers.Field):
def to_internal_value(self, data):
return {self.field_name: Country(data)}
def to_representation(self, src):
return str(src) if src else None
class InvoiceAddressSerializer(I18nAwareModelSerializer): class InvoiceAddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*') country = CompatibleCountryField(source='*')
name = serializers.CharField(required=False) name = serializers.CharField(required=False)
@@ -1322,17 +1330,24 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
model = InvoiceLine model = InvoiceLine
fields = ('position', 'description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name') fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
class InvoiceSerializer(I18nAwareModelSerializer): class InvoiceSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True) order = serializers.SlugRelatedField(slug_field='code', read_only=True)
refers = serializers.SlugRelatedField(slug_field='invoice_no', read_only=True) refers = serializers.SlugRelatedField(slug_field='full_invoice_no', read_only=True)
lines = InlineInvoiceLineSerializer(many=True) lines = InlineInvoiceLineSerializer(many=True)
invoice_to_country = CountryField()
invoice_from_country = CountryField()
class Meta: class Meta:
model = Invoice model = Invoice
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale', fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
'custom_field', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines', 'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date', 'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
'internal_reference') 'internal_reference')
+8 -1
View File
@@ -8,7 +8,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
model = WaitingListEntry model = WaitingListEntry
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority') fields = ('id', 'created', 'name', 'name_parts', 'email', 'phone', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
read_only_fields = ('id', 'created', 'voucher') read_only_fields = ('id', 'created', 'voucher')
def validate(self, data): def validate(self, data):
@@ -32,4 +32,11 @@ class WaitingListSerializer(I18nAwareModelSerializer):
if availability[0] == 100: if availability[0] == 100:
raise ValidationError("This product is currently available.") raise ValidationError("This product is currently available.")
if data.get('name') and data.get('name_parts'):
raise ValidationError(
{'name': ['Do not specify name if you specified name_parts.']}
)
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = event.settings.name_scheme
return data return data
+2 -2
View File
@@ -53,8 +53,8 @@ class DeviceSerializer(serializers.ModelSerializer):
class InitializeView(APIView): class InitializeView(APIView):
authentication_classes = tuple() authentication_classes = ()
permission_classes = tuple() permission_classes = ()
def post(self, request, format=None): def post(self, request, format=None):
serializer = InitializationRequestSerializer(data=request.data) serializer = InitializationRequestSerializer(data=request.data)
+16
View File
@@ -2,10 +2,12 @@ import inspect
import logging import logging
from datetime import timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from itertools import groupby
from smtplib import SMTPResponseException from smtplib import SMTPResponseException
from django.conf import settings from django.conf import settings
from django.core.mail.backends.smtp import EmailBackend from django.core.mail.backends.smtp import EmailBackend
from django.db.models import Count
from django.dispatch import receiver from django.dispatch import receiver
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.timezone import now from django.utils.timezone import now
@@ -128,9 +130,21 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
if order: if order:
htmlctx['order'] = order htmlctx['order'] = order
positions = list(order.positions.select_related(
'item', 'variation', 'subevent', 'addon_to'
).annotate(
has_addons=Count('addons')
))
htmlctx['cart'] = [(k, list(v)) for k, v in groupby(
positions, key=lambda op: (
op.item, op.variation, op.subevent, op.attendee_name,
(op.pk if op.addon_to_id else None), (op.pk if op.has_addons else None)
)
)]
if position: if position:
htmlctx['position'] = position htmlctx['position'] = position
htmlctx['ev'] = position.subevent or self.event
tpl = get_template(self.template_name) tpl = get_template(self.template_name)
body_html = inline_css(tpl.render(htmlctx)) body_html = inline_css(tpl.render(htmlctx))
@@ -237,6 +251,8 @@ def get_email_context(**kwargs):
from pretix.base.models import InvoiceAddress from pretix.base.models import InvoiceAddress
event = kwargs['event'] event = kwargs['event']
if 'position' in kwargs:
kwargs.setdefault("position_or_address", kwargs['position'])
if 'order' in kwargs: if 'order' in kwargs:
try: try:
kwargs['invoice_address'] = kwargs['order'].invoice_address kwargs['invoice_address'] = kwargs['order'].invoice_address
+1 -1
View File
@@ -13,12 +13,12 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _, pgettext from django.utils.translation import gettext, gettext_lazy as _, pgettext
from pretix.base.models import Invoice, InvoiceLine, OrderPayment from pretix.base.models import Invoice, InvoiceLine, OrderPayment
from ..services.export import ExportError
from ...control.forms.filter import get_all_payment_providers from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable from ...helpers.iter import chunked_iterable
from ..exporter import BaseExporter, MultiSheetListExporter from ..exporter import BaseExporter, MultiSheetListExporter
from ..services.export import ExportError
from ..services.invoices import invoice_pdf_task from ..services.invoices import invoice_pdf_task
from ..signals import ( from ..signals import (
register_data_exporters, register_multievent_data_exporters, register_data_exporters, register_multievent_data_exporters,
+18 -1
View File
@@ -691,13 +691,18 @@ class QuotaListExporter(ListExporter):
verbose_name = gettext_lazy('Quota availabilities') verbose_name = gettext_lazy('Quota availabilities')
def iterate_list(self, form_data): def iterate_list(self, form_data):
has_subevents = self.event.has_subevents
headers = [ headers = [
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'), _('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
_('Current user\'s carts'), _('Waiting list'), _('Exited orders'), _('Current availability') _('Current user\'s carts'), _('Waiting list'), _('Exited orders'), _('Current availability')
] ]
if has_subevents:
headers.append(pgettext('subevent', 'Date'))
headers.append(_('Start date'))
headers.append(_('End date'))
yield headers yield headers
quotas = list(self.event.quotas.all()) quotas = list(self.event.quotas.select_related('subevent'))
qa = QuotaAvailability(full_results=True) qa = QuotaAvailability(full_results=True)
qa.queue(*quotas) qa.queue(*quotas)
qa.compute() qa.compute()
@@ -715,6 +720,18 @@ class QuotaListExporter(ListExporter):
qa.count_exited_orders[quota], qa.count_exited_orders[quota],
_('Infinite') if avail[1] is None else avail[1] _('Infinite') if avail[1] is None else avail[1]
] ]
if has_subevents:
if quota.subevent:
row.append(quota.subevent.name)
row.append(quota.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
if quota.subevent.date_to:
row.append(quota.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
else:
row.append('')
else:
row.append('')
row.append('')
row.append('')
yield row yield row
def get_filename(self): def get_filename(self):
+4
View File
@@ -82,7 +82,9 @@ class WaitingListExporter(ListExporter):
headers = [ headers = [
_('Date'), _('Date'),
_('Name'),
_('Email'), _('Email'),
_('Phone number'),
_('Product name'), _('Product name'),
_('Variation'), _('Variation'),
_('Event slug'), _('Event slug'),
@@ -117,7 +119,9 @@ class WaitingListExporter(ListExporter):
row = [ row = [
entry.created.astimezone(tz).strftime(datetime_format), # alternative: .isoformat(), entry.created.astimezone(tz).strftime(datetime_format), # alternative: .isoformat(),
entry.name,
entry.email, entry.email,
entry.phone,
str(entry.item) if entry.item else "", str(entry.item) if entry.item else "",
str(entry.variation) if entry.variation else "", str(entry.variation) if entry.variation else "",
entry.event.slug, entry.event.slug,
+8 -3
View File
@@ -100,7 +100,7 @@ class NamePartsWidget(forms.MultiWidget):
if not isinstance(value, list): if not isinstance(value, list):
value = self.decompress(value) value = self.decompress(value)
output = [] output = []
final_attrs = self.build_attrs(attrs or dict()) final_attrs = self.build_attrs(attrs or {})
if 'required' in final_attrs: if 'required' in final_attrs:
del final_attrs['required'] del final_attrs['required']
id_ = final_attrs.get('id', None) id_ = final_attrs.get('id', None)
@@ -122,6 +122,8 @@ class NamePartsWidget(forms.MultiWidget):
these_attrs.pop('data-no-required-attr', None) these_attrs.pop('data-no-required-attr', None)
these_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip() these_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip()
these_attrs['data-size'] = self.scheme['fields'][i][2] these_attrs['data-size'] = self.scheme['fields'][i][2]
if len(self.widgets) > 1:
these_attrs['aria-label'] = self.scheme['fields'][i][1]
else: else:
these_attrs = final_attrs these_attrs = final_attrs
output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer)) output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer))
@@ -220,7 +222,7 @@ class WrappedPhonePrefixSelect(Select):
country_name = locale.territories.get(country_code) country_name = locale.territories.get(country_code)
if country_name: if country_name:
choices.append((prefix, "{} {}".format(country_name, prefix))) choices.append((prefix, "{} {}".format(country_name, prefix)))
super().__init__(choices=sorted(choices, key=lambda item: item[1])) super().__init__(choices=sorted(choices, key=lambda item: item[1]), attrs={'aria-label': pgettext_lazy('phonenumber', 'International area code')})
def render(self, name, value, *args, **kwargs): def render(self, name, value, *args, **kwargs):
return super().render(name, value or self.initial, *args, **kwargs) return super().render(name, value or self.initial, *args, **kwargs)
@@ -243,7 +245,10 @@ class WrappedPhonePrefixSelect(Select):
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget): class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def __init__(self, attrs=None, initial=None): def __init__(self, attrs=None, initial=None):
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput()) attrs = {
'aria-label': pgettext_lazy('phonenumber', 'Phone number (without international area code)')
}
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput(attrs=attrs))
super(PhoneNumberPrefixWidget, self).__init__(widgets, attrs) super(PhoneNumberPrefixWidget, self).__init__(widgets, attrs)
def render(self, name, value, attrs=None, renderer=None): def render(self, name, value, attrs=None, renderer=None):
+4 -4
View File
@@ -445,7 +445,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.custom_field: if self.invoice.custom_field:
story.append(Paragraph( story.append(Paragraph(
'{}: {}'.format( '{}: {}'.format(
bleach.clean(self.invoice.event.settings.invoice_address_custom_field, tags=[]).strip().replace('\n', '<br />\n'), bleach.clean(str(self.invoice.event.settings.invoice_address_custom_field), tags=[]).strip().replace('\n', '<br />\n'),
bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'), bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'),
), ),
self.stylesheet['Normal'] self.stylesheet['Normal']
@@ -475,7 +475,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.introductory_text: if self.invoice.introductory_text:
story.append(Paragraph( story.append(Paragraph(
bleach.clean(self.invoice.introductory_text, tags=[]).strip().replace('\n', '<br />\n'), self.invoice.introductory_text,
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
story.append(Spacer(1, 10 * mm)) story.append(Spacer(1, 10 * mm))
@@ -578,13 +578,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.payment_provider_text: if self.invoice.payment_provider_text:
story.append(Paragraph( story.append(Paragraph(
bleach.clean(self.invoice.payment_provider_text, tags=[]).strip().replace('\n', '<br />\n'), self.invoice.payment_provider_text,
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.additional_text: if self.invoice.additional_text:
story.append(Paragraph( story.append(Paragraph(
bleach.clean(self.invoice.additional_text, tags=[]).strip().replace('\n', '<br />\n'), self.invoice.additional_text,
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
story.append(Spacer(1, 15 * mm)) story.append(Spacer(1, 15 * mm))
@@ -16,6 +16,8 @@ class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name ' parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
'(dotted path, comma separation)') '(dotted path, comma separation)')
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
'(dotted path, comma separation)')
def handle(self, *args, **options): def handle(self, *args, **options):
verbosity = int(options['verbosity']) verbosity = int(options['verbosity'])
@@ -28,6 +30,9 @@ class Command(BaseCommand):
if options.get('tasks'): if options.get('tasks'):
if name not in options.get('tasks').split(','): if name not in options.get('tasks').split(','):
continue continue
if options.get('exclude'):
if name in options.get('exclude').split(','):
continue
if verbosity > 1: if verbosity > 1:
self.stdout.write(f'INFO Running {name}…') self.stdout.write(f'INFO Running {name}…')
@@ -0,0 +1,30 @@
# Generated by Django 3.0.10 on 2021-03-01 15:10
import jsonfallback.fields
import phonenumber_field.modelfields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0176_auto_20210205_1512'),
]
operations = [
migrations.AddField(
model_name='waitinglistentry',
name='name_cached',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='waitinglistentry',
name='name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AddField(
model_name='waitinglistentry',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None),
),
]
@@ -0,0 +1,34 @@
# Generated by Django 3.0.12 on 2021-03-08 13:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0177_auto_20210301_1510'),
]
operations = [
migrations.AddField(
model_name='invoiceline',
name='attendee_name',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='invoiceline',
name='event_date_to',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='invoiceline',
name='item',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item'),
),
migrations.AddField(
model_name='invoiceline',
name='variation',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation'),
),
]
@@ -0,0 +1,49 @@
# Generated by Django 3.0.10 on 2021-03-11 16:53
from django.db import migrations
def clean_duplicates(apps, schema_editor):
while True:
delete_options = """
DELETE
FROM pretixbase_questionanswer_options
WHERE questionanswer_id IN (
SELECT MIN(qa.id)
FROM pretixbase_questionanswer qa
GROUP BY qa.cartposition_id, qa.orderposition_id, qa.question_id
HAVING COUNT(*) > 1
);
"""
delete_answers = """
DELETE
FROM pretixbase_questionanswer
WHERE pretixbase_questionanswer.id IN (
SELECT MIN(qa.id)
FROM pretixbase_questionanswer qa
GROUP BY qa.cartposition_id, qa.orderposition_id, qa.question_id
HAVING COUNT(*) > 1
);
"""
with schema_editor.connection.cursor() as cursor:
cursor.execute(delete_options)
cursor.execute(delete_answers)
if cursor.rowcount == 0:
return
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0178_auto_20210308_1326'),
]
operations = [
migrations.RunPython(
clean_duplicates,
migrations.RunPython.noop,
),
migrations.AlterUniqueTogether(
name='questionanswer',
unique_together={('orderposition', 'question'), ('cartposition', 'question')},
),
]
+2 -2
View File
@@ -697,9 +697,9 @@ class Event(EventMixin, LoggedModel):
for k, v in rules.items(): for k, v in rules.items():
if k == 'lookup': if k == 'lookup':
if v[0] == 'product': if v[0] == 'product':
v[1] = str(item_map.get(int(v[1]), 0).pk) v[1] = str(item_map.get(int(v[1]), 0).pk) if int(v[1]) in item_map else "0"
elif v[0] == 'variation': elif v[0] == 'variation':
v[1] = str(variation_map.get(int(v[1]), 0).pk) v[1] = str(variation_map.get(int(v[1]), 0).pk) if int(v[1]) in variation_map else "0"
else: else:
_walk_rules(v) _walk_rules(v)
elif isinstance(rules, list): elif isinstance(rules, list):
+12
View File
@@ -273,6 +273,14 @@ class InvoiceLine(models.Model):
:type subevent: SubEvent :type subevent: SubEvent
:param event_date_from: Event date of the (sub)event at the time the invoice was created :param event_date_from: Event date of the (sub)event at the time the invoice was created
:type event_date_from: datetime :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 item: The item this line refers to
:type item: Item
:param variation: The variation this line refers to
:type variation: ItemVariation
:param attendee_name: The attendee name at the time the invoice was created
:type attendee_name: str
""" """
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE) invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
position = models.PositiveIntegerField(default=0) position = models.PositiveIntegerField(default=0)
@@ -283,6 +291,10 @@ class InvoiceLine(models.Model):
tax_name = models.CharField(max_length=190) tax_name = models.CharField(max_length=190)
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT) subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
event_date_from = models.DateTimeField(null=True) event_date_from = models.DateTimeField(null=True)
event_date_to = models.DateTimeField(null=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)
@property @property
def net_value(self): def net_value(self):
+3
View File
@@ -983,6 +983,9 @@ class QuestionAnswer(models.Model):
objects = ScopedManager(organizer='question__event__organizer') objects = ScopedManager(organizer='question__event__organizer')
class Meta:
unique_together = [['orderposition', 'question'], ['cartposition', 'question']]
@property @property
def backend_file_url(self): def backend_file_url(self):
if self.file: if self.file:
+36
View File
@@ -5,11 +5,14 @@ from django.db import models, transaction
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager from django_scopes import ScopedManager
from jsonfallback.fields import FallbackJSONField
from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Voucher from pretix.base.models import Voucher
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.base.settings import PERSON_NAME_SCHEMES
from .base import LoggedModel from .base import LoggedModel
from .event import Event, SubEvent from .event import Event, SubEvent
@@ -37,9 +40,21 @@ class WaitingListEntry(LoggedModel):
verbose_name=_("On waiting list since"), verbose_name=_("On waiting list since"),
auto_now_add=True auto_now_add=True
) )
name_cached = models.CharField(
max_length=255,
verbose_name=_("Name"),
blank=True, null=True,
)
name_parts = FallbackJSONField(
blank=True, default=dict
)
email = models.EmailField( email = models.EmailField(
verbose_name=_("E-mail address") verbose_name=_("E-mail address")
) )
phone = PhoneNumberField(
null=True, blank=True,
verbose_name=_("Phone number")
)
voucher = models.ForeignKey( voucher = models.ForeignKey(
'Voucher', 'Voucher',
verbose_name=_("Assigned voucher"), verbose_name=_("Assigned voucher"),
@@ -83,6 +98,27 @@ class WaitingListEntry(LoggedModel):
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation) WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent) WaitingListEntry.clean_subevent(self.event, self.subevent)
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
if 'name_parts' in update_fields:
update_fields.append('name_cached')
self.name_cached = self.name
if self.name_parts is None:
self.name_parts = {}
super().save(*args, **kwargs)
@property
def name(self):
if not self.name_parts:
return None
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.name_parts).strip()
def send_voucher(self, quota_cache=None, user=None, auth=None): def send_voucher(self, quota_cache=None, user=None, auth=None):
availability = ( availability = (
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache) self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
+13 -10
View File
@@ -970,17 +970,17 @@ class ManualPayment(BasePaymentProvider):
label=_('Payment process description in order confirmation emails'), label=_('Payment process description in order confirmation emails'),
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation ' help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
'mails. It should instruct the user on how to proceed with the payment. You can use ' 'mails. It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {total}, {currency} and {total_with_currency}.'), 'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
widget=I18nTextarea, widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])], validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
)), )),
('pending_description', I18nFormField( ('pending_description', I18nFormField(
label=_('Payment process description for pending orders'), label=_('Payment process description for pending orders'),
help_text=_('This text will be shown on the order confirmation page for pending orders. ' help_text=_('This text will be shown on the order confirmation page for pending orders. '
'It should instruct the user on how to proceed with the payment. You can use ' 'It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {total}, {currency} and {total_with_currency}.'), 'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
widget=I18nTextarea, widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])], validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
)), )),
] + list(super().settings_form_fields.items()) ] + list(super().settings_form_fields.items())
) )
@@ -1001,21 +1001,24 @@ class ManualPayment(BasePaymentProvider):
def checkout_confirm_render(self, request): def checkout_confirm_render(self, request):
return self.payment_form_render(request) return self.payment_form_render(request)
def format_map(self, order): def format_map(self, order, payment):
return { return {
'order': order.code, 'order': order.code,
'total': order.total, 'amount': payment.amount,
'currency': self.event.currency, 'currency': self.event.currency,
'total_with_currency': money_filter(order.total, self.event.currency) 'amount_with_currency': money_filter(payment.amount, self.event.currency),
# {total} and {total_with_currency} are deprecated
'total': order.total,
'total_with_currency': money_filter(order.total, self.event.currency),
} }
def order_pending_mail_render(self, order) -> str: def order_pending_mail_render(self, order, payment) -> str:
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order)) msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order, payment))
return msg return msg
def payment_pending_render(self, request, payment) -> str: def payment_pending_render(self, request, payment) -> str:
return rich_text( return rich_text(
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order)) str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order, payment))
) )
+5
View File
@@ -298,6 +298,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Event organizer info text"), "editor_sample": _("Event organizer info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text) "evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
}), }),
("event_info_text", {
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.event_info_text)
}),
("now_date", { ("now_date", {
"label": _("Printing date"), "label": _("Printing date"),
"editor_sample": _("2017-05-31"), "editor_sample": _("2017-05-31"),
+2 -2
View File
@@ -241,7 +241,7 @@ class CartManager:
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,)) raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
def _check_item_constraints(self, op, current_ops=[]): def _check_item_constraints(self, op, current_ops=[]):
if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation): if isinstance(op, (self.AddOperation, self.ExtendOperation)):
if not ( if not (
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or (isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
(isinstance(op, self.ExtendOperation) and op.position.is_bundled) (isinstance(op, self.ExtendOperation) and op.position.is_bundled)
@@ -863,7 +863,7 @@ class CartManager:
op.position.addons.all().delete() op.position.addons.all().delete()
op.position.delete() op.position.delete()
elif isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation): elif isinstance(op, (self.AddOperation, self.ExtendOperation)):
# Create a CartPosition for as much items as we can # Create a CartPosition for as much items as we can
requested_count = quota_available_count = voucher_available_count = op.count requested_count = quota_available_count = voucher_available_count = op.count
+13 -3
View File
@@ -171,9 +171,17 @@ def build_invoice(invoice: Invoice) -> Invoice:
if invoice.event.has_subevents: if invoice.event.has_subevents:
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent) desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
InvoiceLine.objects.create( InvoiceLine.objects.create(
position=i, invoice=invoice, description=desc, position=i,
gross_value=p.price, tax_value=p.tax_value, invoice=invoice,
subevent=p.subevent, event_date_from=(p.subevent.date_from if p.subevent else invoice.event.date_from), description=desc,
gross_value=p.price,
tax_value=p.tax_value,
subevent=p.subevent,
item=p.item,
variation=p.variation,
attendee_name=p.attendee_name if invoice.event.settings.invoice_attendee_name else None,
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else '' tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
) )
@@ -198,6 +206,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice=invoice, invoice=invoice,
description=fee_title, description=fee_title,
gross_value=fee.value, gross_value=fee.value,
event_date_from=None if invoice.event.has_subevents else invoice.event.date_from,
event_date_to=None if invoice.event.has_subevents else invoice.event.date_to,
tax_value=fee.tax_value, tax_value=fee.tax_value,
tax_rate=fee.tax_rate, tax_rate=fee.tax_rate,
tax_name=fee.tax_rule.name if fee.tax_rule else '' tax_name=fee.tax_rule.name if fee.tax_rule else ''
+10 -1
View File
@@ -19,6 +19,7 @@ from django.core.mail import (
EmailMultiAlternatives, SafeMIMEMultipart, get_connection, EmailMultiAlternatives, SafeMIMEMultipart, get_connection,
) )
from django.core.mail.message import SafeMIMEText from django.core.mail.message import SafeMIMEText
from django.db import transaction
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import gettext as _, pgettext from django.utils.translation import gettext as _, pgettext
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled
@@ -240,7 +241,15 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
task_chain = [] task_chain = []
task_chain.append(send_task) task_chain.append(send_task)
chain(*task_chain).apply_async()
if 'locmem' in settings.EMAIL_BACKEND:
# This clause is triggered during unit tests, because transaction.on_commit never fires due to the nature
# Django's unit tests work
chain(*task_chain).apply_async()
else:
transaction.on_commit(
lambda: chain(*task_chain).apply_async()
)
class CustomEmail(EmailMultiAlternatives): class CustomEmail(EmailMultiAlternatives):
+9 -7
View File
@@ -327,7 +327,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None, def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
cancellation_fee=None, keep_fees=None): cancellation_fee=None, keep_fees=None, cancel_invoice=True):
""" """
Mark this order as canceled Mark this order as canceled
:param order: The order to change :param order: The order to change
@@ -351,9 +351,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if not order.cancel_allowed(): if not order.cancel_allowed():
raise OrderError(_('You cannot cancel this order.')) raise OrderError(_('You cannot cancel this order.'))
invoices = [] invoices = []
i = order.invoices.filter(is_cancellation=False).last() if cancel_invoice:
if i and not i.refered.exists(): i = order.invoices.filter(is_cancellation=False).last()
invoices.append(generate_cancellation(i)) if i and not i.refered.exists():
invoices.append(generate_cancellation(i))
for position in order.positions.all(): for position in order.positions.all():
for gc in position.issued_gift_cards.all(): for gc in position.issued_gift_cards.all():
@@ -403,7 +404,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
order.cancellation_date = now() order.cancellation_date = now()
order.save(update_fields=['status', 'cancellation_date', 'total']) order.save(update_fields=['status', 'cancellation_date', 'total'])
if i: if cancel_invoice and i:
invoices.append(generate_invoice(order)) invoices.append(generate_invoice(order))
else: else:
with order.event.lock(): with order.event.lock():
@@ -2152,11 +2153,12 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) @app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
@scopes_disabled() @scopes_disabled()
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None, def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None): device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None,
cancel_invoice=True):
try: try:
try: try:
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application, ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
cancellation_fee) cancellation_fee, cancel_invoice=cancel_invoice)
if try_auto_refund: if try_auto_refund:
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard, _try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
comment=comment) comment=comment)
+16 -11
View File
@@ -3,22 +3,21 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Event, User, Voucher from pretix.base.models import Event, LogEntry, User, Voucher
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.base.services.tasks import TransactionAwareProfiledEventTask
from pretix.celery_app import app
@app.task(base=TransactionAwareProfiledEventTask, acks_late=True) def vouchers_send(event: Event, vouchers: list, subject: str, message: str, recipients: list, user: int,
def vouchers_send(event: Event, vouchers: list, subject: str, message: str, recipients: list, user: int) -> None: progress=None) -> None:
vouchers = list(Voucher.objects.filter(id__in=vouchers).order_by('id')) vouchers = list(Voucher.objects.filter(id__in=vouchers).order_by('id'))
user = User.objects.get(pk=user) user = User.objects.get(pk=user)
for r in recipients: for ir, r in enumerate(recipients):
voucher_list = [] voucher_list = []
for i in range(r['number']): for i in range(r['number']):
voucher_list.append(vouchers.pop()) voucher_list.append(vouchers.pop())
with language(event.settings.locale): with language(event.settings.locale):
email_context = get_email_context(event=event, name=r.get('name') or '', voucher_list=[v.code for v in voucher_list]) email_context = get_email_context(event=event, name=r.get('name') or '',
voucher_list=[v.code for v in voucher_list])
mail( mail(
r['email'], r['email'],
subject, subject,
@@ -27,14 +26,14 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
event, event,
locale=event.settings.locale, locale=event.settings.locale,
) )
logs = []
for v in voucher_list: for v in voucher_list:
if r.get('tag') and r.get('tag') != v.tag: if r.get('tag') and r.get('tag') != v.tag:
v.tag = r.get('tag') v.tag = r.get('tag')
if v.comment: if v.comment:
v.comment += '\n\n' v.comment += '\n\n'
v.comment = gettext('The voucher has been sent to {recipient}.').format(recipient=r['email']) v.comment = gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
v.save(update_fields=['tag', 'comment']) logs.append(v.log_action(
v.log_action(
'pretix.voucher.sent', 'pretix.voucher.sent',
user=user, user=user,
data={ data={
@@ -42,5 +41,11 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
'name': r.get('name'), 'name': r.get('name'),
'subject': subject, 'subject': subject,
'message': message, 'message': message,
} },
) save=False
))
Voucher.objects.bulk_update(voucher_list, fields=['comment', 'tag'], batch_size=500)
LogEntry.objects.bulk_create(logs, batch_size=500)
if progress and ir % 50 == 0:
progress(ir / len(recipients))
+68 -1
View File
@@ -975,6 +975,61 @@ DEFAULTS = {
widget=forms.NumberInput(), widget=forms.NumberInput(),
) )
}, },
'waiting_list_names_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for a name"),
help_text=_("Ask for a name when signing up to the waiting list."),
)
},
'waiting_list_names_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require name"),
help_text=_("Require a name when signing up to the waiting list.."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-waiting_list_names_asked'}),
)
},
'waiting_list_phones_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for a phone number"),
help_text=_("Ask for a phone number when signing up to the waiting list."),
)
},
'waiting_list_phones_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require phone number"),
help_text=_("Require a phone number when signing up to the waiting list.."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-waiting_list_phones_asked'}),
)
},
'waiting_list_phones_explanation_text': {
'default': '',
'type': LazyI18nString,
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Phone number explanation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
)
},
'ticket_download': { 'ticket_download': {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool,
@@ -1744,7 +1799,7 @@ Your {event} team"""))
), ),
}, },
'theme_color_danger': { 'theme_color_danger': {
'default': '#D36060', 'default': '#C44F4F',
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
@@ -1945,6 +2000,18 @@ Your {event} team"""))
widget=I18nTextarea widget=I18nTextarea
) )
}, },
'event_info_text': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('Info text'),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_('Not displayed anywhere by default, but if you want to, you can use this e.g. in ticket templates.')
)
},
'banner_text': { 'banner_text': {
'default': '', 'default': '',
'type': LazyI18nString, 'type': LazyI18nString,
+9 -2
View File
@@ -203,7 +203,7 @@ class EmailAddressShredder(BaseDataShredder):
class WaitingListShredder(BaseDataShredder): class WaitingListShredder(BaseDataShredder):
verbose_name = _('Waiting list') verbose_name = _('Waiting list')
identifier = 'waiting_list' identifier = 'waiting_list'
description = _('This will remove all email addresses from the waiting list.') description = _('This will remove all names, email addresses, and phone numbers from the waiting list.')
def generate_files(self) -> List[Tuple[str, str, str]]: def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'waiting-list.json', 'application/json', json.dumps([ yield 'waiting-list.json', 'application/json', json.dumps([
@@ -213,7 +213,7 @@ class WaitingListShredder(BaseDataShredder):
@transaction.atomic @transaction.atomic
def shred_data(self): def shred_data(self):
self.event.waitinglistentries.update(email='█') self.event.waitinglistentries.update(name_cached=None, name_parts={'_shredded': True}, email='█', phone='█')
for wle in self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False): for wle in self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False):
if '@' in wle.voucher.comment: if '@' in wle.voucher.comment:
@@ -222,7 +222,14 @@ class WaitingListShredder(BaseDataShredder):
for le in self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data=""): for le in self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data=""):
d = le.parsed_data d = le.parsed_data
if 'name' in d:
d['name'] = '█'
if 'name_parts' in d:
d['name_parts'] = {
'_legacy': '█'
}
d['email'] = '█' d['email'] = '█'
d['phone'] = '█'
le.data = json.dumps(d) le.data = json.dumps(d)
le.shredded = True le.shredded = True
le.save(update_fields=['data', 'shredded']) le.save(update_fields=['data', 'shredded'])
@@ -136,6 +136,31 @@
text-decoration: none; text-decoration: none;
color: {{ color }}; color: {{ color }};
} }
.order-button {
padding-top: 5px
}
.order-button a.button {
font-size: 12px;
}
.order-info {
padding-bottom: 5px
}
.order {
font-size: 12px;
}
.cart-table > tr > td:first-child {
width: 40px;
}
.order-details > tr > td:first-child {
width: 20%;
}
.order-details td {
font-size: 12px;
}
{% if rtl %} {% if rtl %}
body { body {
direction: rtl; direction: rtl;
@@ -0,0 +1,142 @@
{% load eventurl %}
{% load i18n %}
{% if position %}
<div class="order-info">
{% trans "You are receiving this email because someone signed you up for the following event:" %}
</div>
<table class="order-details">
<tr>
<td>
<strong>{% trans "Event:" %}</strong>
</td>
<td>
{{ event.name }}
<br>
{% if event.has_subevents and ev.name|upper != event.name|upper %}{{ ev.name }}<br>{% endif %}
{{ ev.get_date_range_display }}
{% if event.settings.show_times %}
{{ ev.date_from|date:"TIME_FORMAT" }}
{% endif %}
</td>
</tr>
<tr>
<td>
<strong>{% trans "Order code:" %}</strong>
</td>
<td>
{{ order.code }} ({{ order.datetime|date:"SHORT_DATE_FORMAT" }})<br>
{% if order.email %}
{% trans "created by" %} {{ order.email }}
{% endif %}
</td>
</tr>
<tr>
<td>
<strong>{% trans "Organizer:" %}</strong>
</td>
<td>
{{ event.organizer }}
{% if event.settings.contact_mail %}
<br>
<a href="mailto:{{ event.settings.contact_mail }}">
{{ event.settings.contact_mail }}
</a>
{% endif %}
</td>
</tr>
</table>
<div class="order-button">
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}" class="button">
{% trans "View registration details" %}
</a>
</div>
{% else %}
<div class="order-info">
{% trans "You are receiving this email because you placed an order for the following event:" %}
</div>
<table class="order-details">
<tr>
<td>
<strong>{% trans "Event:" %}</strong>
</td>
<td>
{{ event.name }}
{% if not event.has_subevents and event.settings.show_dates_on_frontpage %}
<br>
{{ event.get_date_range_display }}
{% if event.settings.show_times %}
{{ event.date_from|date:"TIME_FORMAT" }}
{% endif %}
{% endif %}
</td>
</tr>
<tr>
<td>
<strong>{% trans "Order code:" %}</strong>
</td>
<td>
{{ order.code }} ({{ order.datetime|date:"SHORT_DATE_FORMAT" }})
</td>
</tr>
{% if cart %}
<tr>
<td>
<strong>{% trans "Details:" %}</strong>
</td>
<td>
<table class="cart-table">
{% for groupkey, positions in cart %}
<tr>
<td>
{% if not groupkey.4 %} {# is addon #}
{{ positions|length }}x
{% endif %}
</td>
<td>
{% if groupkey.4 %} {# is addon #}
+
{% endif %}
{{ groupkey.0.name }}{% if groupkey.1 %} – {{ groupkey.1.value }}{% endif %}
{% if groupkey.2 %} {# subevent #}
<br>
{% if groupkey.2.name|upper != event.name|upper %}
{{ groupkey.2.name }} &middot;
{% endif %}
{{ groupkey.2.get_date_range_display }}
{% if event.settings.show_times %}
{{ groupkey.2.date_from|date:"TIME_FORMAT" }}
{% endif %}
{% endif %}
{% if groupkey.3 %} {# attendee name #}
<br>
{{ groupkey.3.name }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</td>
</tr>
{% endif %}
<tr>
<td>
<strong>{% trans "Organizer:" %}</strong>
</td>
<td>
{{ event.organizer }}
{% if event.settings.contact_mail %}
<br>
<a href="mailto:{{ event.settings.contact_mail }}">
{{ event.settings.contact_mail }}
</a>
{% endif %}
</td>
</tr>
</table>
<div class="order-button">
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}" class="button">
{% trans "View order details" %}
</a>
</div>
{% endif %}
@@ -23,23 +23,7 @@
<table cellpadding="20"><tr><td> <table cellpadding="20"><tr><td>
<![endif]--> <![endif]-->
<div class="content"> <div class="content">
{% if position %} {% include "pretixbase/email/order_details.html" %}
{% trans "You are receiving this email because someone signed you up for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}">
{% trans "View registration details" %}
</a>
{% else %}
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
{% endif %}
</div> </div>
<!--[if gte mso 9]> <!--[if gte mso 9]>
</td></tr></table> </td></tr></table>
@@ -147,6 +147,31 @@
text-decoration: none; text-decoration: none;
color: {{ color }}; color: {{ color }};
} }
.order-button {
padding-top: 5px
}
.order-button a.button {
font-size: 12px;
}
.order-info {
padding-bottom: 5px
}
.order {
font-size: 12px;
}
.cart-table > tr > td:first-child {
width: 40px;
}
.order-details > tr > td:first-child {
width: 20%;
}
.order-details td {
font-size: 12px;
}
{% if rtl %} {% if rtl %}
body { body {
direction: rtl; direction: rtl;
@@ -226,23 +251,7 @@
<table cellpadding="20"><tr><td> <table cellpadding="20"><tr><td>
<![endif]--> <![endif]-->
<div class="content"> <div class="content">
{% if position %} {% include "pretixbase/email/order_details.html" %}
{% trans "You are receiving this email because someone signed you up for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}">
{% trans "View registration details" %}
</a>
{% else %}
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
{% endif %}
</div> </div>
<!--[if gte mso 9]> <!--[if gte mso 9]>
</td></tr></table> </td></tr></table>
@@ -1,7 +1,7 @@
{% load thumb %} {% load thumb %}
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %} {% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"> <input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}">
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.value.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br> <label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
{{ widget.input_text }}:{% endif %} {{ widget.input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}> <input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
{% if widget.cachedfile %}<input type="hidden" name="{{ widget.hidden_name }}" value="{{ widget.cachedfile.id }}">{% endif %} {% if widget.cachedfile %}<input type="hidden" name="{{ widget.hidden_name }}" value="{{ widget.cachedfile.id }}">{% endif %}
+2 -2
View File
@@ -11,7 +11,7 @@ register = template.Library()
@register.filter("money") @register.filter("money")
def money_filter(value: Decimal, arg='', hide_currency=False): def money_filter(value: Decimal, arg='', hide_currency=False):
if isinstance(value, float) or isinstance(value, int): if isinstance(value, (float, int)):
value = Decimal(value) value = Decimal(value)
if not isinstance(value, Decimal): if not isinstance(value, Decimal):
if value == '': if value == '':
@@ -47,7 +47,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
@register.filter("money_numberfield") @register.filter("money_numberfield")
def money_numberfield_filter(value: Decimal, arg=''): def money_numberfield_filter(value: Decimal, arg=''):
if isinstance(value, float) or isinstance(value, int): if isinstance(value, (float, int)):
value = Decimal(value) value = Decimal(value)
if not isinstance(value, Decimal): if not isinstance(value, Decimal):
raise TypeError("Invalid data type passed to money filter: %r" % type(value)) raise TypeError("Invalid data type passed to money filter: %r" % type(value))
+1 -1
View File
@@ -48,7 +48,7 @@ def page_not_found(request, exception):
except (AttributeError, IndexError): except (AttributeError, IndexError):
pass pass
else: else:
if isinstance(message, str) or isinstance(message, Promise): if isinstance(message, (str, Promise)):
exception_repr = str(message) exception_repr = str(message)
context = { context = {
'request_path': request.path, 'request_path': request.path,
+129 -31
View File
@@ -4,43 +4,25 @@ import celery.exceptions
from celery.result import AsyncResult from celery.result import AsyncResult
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.test import RequestFactory
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView
from pretix.base.models import User
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app from pretix.celery_app import app
logger = logging.getLogger('pretix.base.tasks') logger = logging.getLogger('pretix.base.tasks')
class AsyncAction: class AsyncMixin:
task = None
success_url = None success_url = None
error_url = None error_url = None
known_errortypes = [] known_errortypes = []
def do(self, *args, **kwargs):
if not isinstance(self.task, app.Task):
raise TypeError('Method has no task attached')
try:
res = self.task.apply_async(args=args, kwargs=kwargs)
except ConnectionError:
# Task very likely not yet sent, due to redis restarting etc. Let's try once agan
res = self.task.apply_async(args=args, kwargs=kwargs)
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
data = self._return_ajax_result(res)
data['check_url'] = self.get_check_url(res.id, True)
return JsonResponse(data)
else:
if res.ready():
if res.successful() and not isinstance(res.info, Exception):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))
def get_success_url(self, value): def get_success_url(self, value):
return self.success_url return self.success_url
@@ -50,11 +32,6 @@ class AsyncAction:
def get_check_url(self, task_id, ajax): def get_check_url(self, task_id, ajax):
return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '') return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '')
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
return self.http_method_not_allowed(request)
def _ajax_response_data(self): def _ajax_response_data(self):
return {} return {}
@@ -86,7 +63,7 @@ class AsyncAction:
if smes: if smes:
messages.success(self.request, smes) messages.success(self.request, smes)
# TODO: Do not store message if the ajax client states that it will not redirect # TODO: Do not store message if the ajax client states that it will not redirect
# but handle the mssage itself # but handle the message itself
data.update({ data.update({
'redirect': self.get_success_url(res.info), 'redirect': self.get_success_url(res.info),
'success': True, 'success': True,
@@ -95,7 +72,7 @@ class AsyncAction:
else: else:
messages.error(self.request, self.get_error_message(res.info)) messages.error(self.request, self.get_error_message(res.info))
# TODO: Do not store message if the ajax client states that it will not redirect # TODO: Do not store message if the ajax client states that it will not redirect
# but handle the mssage itself # but handle the message itself
data.update({ data.update({
'redirect': self.get_error_url(), 'redirect': self.get_error_url(),
'success': False, 'success': False,
@@ -159,3 +136,124 @@ class AsyncAction:
def get_success_message(self, value): def get_success_message(self, value):
return _('The task has been completed.') return _('The task has been completed.')
class AsyncAction(AsyncMixin):
task = None
def do(self, *args, **kwargs):
if not isinstance(self.task, app.Task):
raise TypeError('Method has no task attached')
try:
res = self.task.apply_async(args=args, kwargs=kwargs)
except ConnectionError:
# Task very likely not yet sent, due to redis restarting etc. Let's try once again
res = self.task.apply_async(args=args, kwargs=kwargs)
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
data = self._return_ajax_result(res)
data['check_url'] = self.get_check_url(res.id, True)
return JsonResponse(data)
else:
if res.ready():
if res.successful() and not isinstance(res.info, Exception):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
return self.http_method_not_allowed(request)
class AsyncFormView(AsyncMixin, FormView):
"""
FormView variant in which instead of ``form_valid``, an ``async_form_valid``
is executed in a celery task. Note that this places some severe limitations
on the form and the view, e.g. neither ``get_form*`` nor the form itself
may depend on the request object unless specifically supported by this class.
Also, all form keyword arguments except ``instance`` need to be serializable.
"""
known_errortypes = ['ValidationError']
def __init_subclass__(cls):
def async_execute(self, request_path, form_kwargs, organizer=None, event=None, user=None):
view_instance = cls()
view_instance.request = RequestFactory().post(request_path)
if organizer:
view_instance.request.event = event
if organizer:
view_instance.request.organizer = organizer
if user:
view_instance.request.user = User.objects.get(pk=user)
form_class = view_instance.get_form_class()
if form_kwargs.get('instance'):
cls.model.objects.get(pk=form_kwargs['instance'])
form_kwargs = view_instance.get_async_form_kwargs(form_kwargs, organizer, event)
form = form_class(**form_kwargs)
return view_instance.async_form_valid(self, form)
cls.async_execute = app.task(
base=ProfiledEventTask,
bind=True,
name=cls.__module__ + '.' + cls.__name__ + '.async_execute',
throws=(ValidationError,)
)(async_execute)
def async_form_valid(self, task, form):
pass
def get_async_form_kwargs(self, form_kwargs, organizer=None, event=None):
return form_kwargs
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
return super().get(request, *args, **kwargs)
def form_valid(self, form):
if form.files:
raise TypeError('File upload currently not supported in AsyncFormView')
form_kwargs = {
k: v for k, v in self.get_form_kwargs().items()
}
if form_kwargs.get('instance'):
if form_kwargs['instance'].pk:
form_kwargs['instance'] = form_kwargs['instance'].pk
else:
form_kwargs['instance'] = None
form_kwargs.setdefault('data', {})
kwargs = {
'request_path': self.request.path,
'form_kwargs': form_kwargs,
}
if hasattr(self.request, 'organizer'):
kwargs['organizer'] = self.request.organizer.pk
if self.request.user.is_authenticated:
kwargs['user'] = self.request.user.pk
if hasattr(self.request, 'event'):
kwargs['event'] = self.request.event.pk
try:
res = type(self).async_execute.apply_async(kwargs=kwargs)
except ConnectionError:
# Task very likely not yet sent, due to redis restarting etc. Let's try once again
res = type(self).async_execute.apply_async(kwargs=kwargs)
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
data = self._return_ajax_result(res)
data['check_url'] = self.get_check_url(res.id, True)
return JsonResponse(data)
else:
if res.ready():
if res.successful() and not isinstance(res.info, Exception):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))
+8 -1
View File
@@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError
from django.core.files import File from django.core.files import File
from django.core.files.uploadedfile import UploadedFile from django.core.files.uploadedfile import UploadedFile
from django.forms.utils import from_current_timezone from django.forms.utils import from_current_timezone
from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -122,7 +123,7 @@ class CachedFileInput(forms.ClearableFileInput):
@property @property
def url(self): def url(self):
return self.file.file.url return reverse('cachedfile.download', kwargs={'id': self.file.id})
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
from ...base.models import CachedFile from ...base.models import CachedFile
@@ -200,6 +201,8 @@ class CachedFileField(ExtFileField):
from ...base.models import CachedFile from ...base.models import CachedFile
if isinstance(data, File): if isinstance(data, File):
if hasattr(data, '_uploaded_to'):
return data._uploaded_to
cf = CachedFile.objects.create( cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1), expires=now() + datetime.timedelta(days=1),
date=now(), date=now(),
@@ -209,6 +212,7 @@ class CachedFileField(ExtFileField):
) )
cf.file.save(data.name, data.file) cf.file.save(data.name, data.file)
cf.save() cf.save()
data._uploaded_to = cf
return cf return cf
return super().bound_data(data, initial) return super().bound_data(data, initial)
@@ -217,6 +221,8 @@ class CachedFileField(ExtFileField):
data = super().clean(*args, **kwargs) data = super().clean(*args, **kwargs)
if isinstance(data, File): if isinstance(data, File):
if hasattr(data, '_uploaded_to'):
return data._uploaded_to
cf = CachedFile.objects.create( cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1), expires=now() + datetime.timedelta(days=1),
web_download=True, web_download=True,
@@ -226,6 +232,7 @@ class CachedFileField(ExtFileField):
) )
cf.file.save(data.name, data.file) cf.file.save(data.name, data.file)
cf.save() cf.save()
data._uploaded_to = cf
return cf return cf
return data return data
+6
View File
@@ -449,6 +449,11 @@ class EventSettingsForm(SettingsForm):
'waiting_list_enabled', 'waiting_list_enabled',
'waiting_list_hours', 'waiting_list_hours',
'waiting_list_auto', 'waiting_list_auto',
'waiting_list_names_asked',
'waiting_list_names_required',
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'max_items_per_order', 'max_items_per_order',
'reservation_time', 'reservation_time',
'contact_mail', 'contact_mail',
@@ -459,6 +464,7 @@ class EventSettingsForm(SettingsForm):
'frontpage_subevent_ordering', 'frontpage_subevent_ordering',
'event_list_type', 'event_list_type',
'frontpage_text', 'frontpage_text',
'event_info_text',
'attendee_names_asked', 'attendee_names_asked',
'attendee_names_required', 'attendee_names_required',
'attendee_emails_asked', 'attendee_emails_asked',
+18 -1
View File
@@ -5,7 +5,9 @@ from urllib.parse import urlencode
from django import forms from django import forms
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.db.models import Exists, F, Max, Model, OuterRef, Q, QuerySet from django.db.models import (
Count, Exists, F, Max, Model, OuterRef, Q, QuerySet,
)
from django.db.models.functions import Coalesce, ExtractWeekDay from django.db.models.functions import Coalesce, ExtractWeekDay
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.formats import date_format, localize from django.utils.formats import date_format, localize
@@ -153,6 +155,7 @@ class OrderFilterForm(FilterForm):
(Order.STATUS_CANCELED, _('Canceled (fully)')), (Order.STATUS_CANCELED, _('Canceled (fully)')),
('cp', _('Canceled (fully or with paid fee)')), ('cp', _('Canceled (fully or with paid fee)')),
('rc', _('Cancellation requested')), ('rc', _('Cancellation requested')),
('cni', _('Fully canceled but invoice not canceled')),
)), )),
(_('Payment process'), ( (_('Payment process'), (
(Order.STATUS_EXPIRED, _('Expired')), (Order.STATUS_EXPIRED, _('Expired')),
@@ -264,6 +267,18 @@ class OrderFilterForm(FilterForm):
status=Order.STATUS_PAID, status=Order.STATUS_PAID,
pending_sum_t__gt=0 pending_sum_t__gt=0
) )
elif s == 'cni':
i = Invoice.objects.filter(
order=OuterRef('pk'),
is_cancellation=False,
refered__isnull=True,
).order_by().values('order').annotate(k=Count('id')).values('k')
qs = qs.annotate(
icnt=i
).filter(
icnt__gt=0,
status=Order.STATUS_CANCELED,
)
elif s == 'pa': elif s == 'pa':
qs = qs.filter( qs = qs.filter(
status=Order.STATUS_PENDING, status=Order.STATUS_PENDING,
@@ -1424,6 +1439,8 @@ class VoucherFilterForm(FilterForm):
s = fdata.get('tag').strip() s = fdata.get('tag').strip()
if s == '<>': if s == '<>':
qs = qs.filter(Q(tag__isnull=True) | Q(tag='')) qs = qs.filter(Q(tag__isnull=True) | Q(tag=''))
elif s[0] == '"' and s[-1] == '"':
qs = qs.filter(tag__iexact=s[1:-1])
else: else:
qs = qs.filter(tag__icontains=s) qs = qs.filter(tag__icontains=s)
+1 -1
View File
@@ -337,7 +337,7 @@ class ItemCreateForm(I18nModelForm):
setattr(self.instance, f, getattr(self.cleaned_data['copy_from'], f)) setattr(self.instance, f, getattr(self.cleaned_data['copy_from'], f))
else: else:
# Add to all sales channels by default # Add to all sales channels by default
self.instance.sales_channels = [k for k in get_all_sales_channels().keys()] self.instance.sales_channels = list(get_all_sales_channels().keys())
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1 self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
instance = super().save(*args, **kwargs) instance = super().save(*args, **kwargs)
+17 -2
View File
@@ -75,7 +75,7 @@ class ExtendForm(I18nModelForm):
return super().save(commit) return super().save(commit)
class ConfirmPaymentForm(forms.Form): class ForceQuotaConfirmationForm(forms.Form):
force = forms.BooleanField( force = forms.BooleanField(
label=_('Overbook quota and ignore late payment'), label=_('Overbook quota and ignore late payment'),
help_text=_('If you check this box, this operation will be performed even if it leads to an overbooked quota ' help_text=_('If you check this box, this operation will be performed even if it leads to an overbooked quota '
@@ -101,7 +101,15 @@ class ConfirmPaymentForm(forms.Form):
del self.fields['force'] del self.fields['force']
class CancelForm(ConfirmPaymentForm): class ConfirmPaymentForm(ForceQuotaConfirmationForm):
pass
class ReactivateOrderForm(ForceQuotaConfirmationForm):
pass
class CancelForm(ForceQuotaConfirmationForm):
send_email = forms.BooleanField( send_email = forms.BooleanField(
required=False, required=False,
label=_('Notify customer by email'), label=_('Notify customer by email'),
@@ -117,6 +125,11 @@ class CancelForm(ConfirmPaymentForm):
'in your cancellation fee if you want to keep them. Please always enter a gross value, ' 'in your cancellation fee if you want to keep them. Please always enter a gross value, '
'tax will be calculated automatically.'), 'tax will be calculated automatically.'),
) )
cancel_invoice = forms.BooleanField(
label=_('Generate cancellation for invoice'),
initial=True,
required=False
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -130,6 +143,8 @@ class CancelForm(ConfirmPaymentForm):
self.fields['cancellation_fee'].max_value = prs self.fields['cancellation_fee'].max_value = prs
else: else:
del self.fields['cancellation_fee'] del self.fields['cancellation_fee']
if not self.instance.invoices.exists():
del self.fields['cancel_invoice']
def clean_cancellation_fee(self): def clean_cancellation_fee(self):
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00') val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')
+5 -26
View File
@@ -5,7 +5,7 @@ from io import StringIO
from django import forms from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import EmailValidator from django.core.validators import EmailValidator
from django.db.models.functions import Lower from django.db.models.functions import Upper
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField from django_scopes.forms import SafeModelChoiceField
@@ -346,8 +346,8 @@ class VoucherBulkForm(VoucherForm):
data = super().clean() data = super().clean()
vouchers = self.instance.event.vouchers.annotate( vouchers = self.instance.event.vouchers.annotate(
code_lower=Lower('code') code_upper=Upper('code')
).filter(code_lower__in=[c.lower() for c in data['codes']]) ).filter(code_upper__in=[c.upper() for c in data['codes']])
if vouchers.exists(): if vouchers.exists():
raise ValidationError(_('A voucher with one of these codes already exists.')) raise ValidationError(_('A voucher with one of these codes already exists.'))
@@ -377,26 +377,5 @@ class VoucherBulkForm(VoucherForm):
return data return data
def save(self, event, *args, **kwargs): def post_bulk_save(self, objs):
objs = [] pass
for code in self.cleaned_data['codes']:
obj = modelcopy(self.instance)
obj.event = event
obj.code = code
try:
obj.seat = self.cleaned_data['seats'].pop()
obj.item = obj.seat.product
except IndexError:
pass
data = dict(self.cleaned_data)
data['code'] = code
data['bulk'] = True
del data['codes']
objs.append(obj)
Voucher.objects.bulk_create(objs, batch_size=200)
objs = []
for v in event.vouchers.filter(code__in=self.cleaned_data['codes']):
# We need to query them again as bulk_create does not fill in .pk values on databases
# other than PostgreSQL
objs.append(v)
return objs
+1 -1
View File
@@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
def current_url(request): def current_url(request):
if len(request.GET): if request.GET:
return request.path + '?' + request.GET.urlencode() return request.path + '?' + request.GET.urlencode()
else: else:
return request.path return request.path
+5
View File
@@ -154,6 +154,11 @@ This signal allows you to replace the form class that is used for modifying vouc
You will receive the default form class (or the class set by a previous plugin) in the You will receive the default form class (or the class set by a previous plugin) in the
``cls`` argument so that you can inherit from it. ``cls`` argument so that you can inherit from it.
Note that this is also called for the voucher bulk creation form, which is executed in
an asynchronous context. For the bulk creation form, ``save()`` is not called. Instead,
you can implement ``post_bulk_save(saved_vouchers)`` which may be called multiple times
for every batch persisted to the database.
As with all plugin signals, the ``sender`` keyword argument will contain the event. As with all plugin signals, the ``sender`` keyword argument will contain the event.
""" """
@@ -29,7 +29,7 @@
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<form class="form-inline helper-display-inline" action="" method="get"> <form class="form-inline helper-display-inline" action="" method="get">
<form class="form-inline helper-display-inline" action="" method="get"> <form class="form-inline helper-display-inline" action="" method="get">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %} {% include "pretixcontrol/event/fragment_subevent_choice_simple.html" with auto_submit=True %}
</form> </form>
</form> </form>
{% endif %} {% endif %}
@@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
<p> <p>
<select name="subevent" class="form-control simple-subevent-choice" data-model-select2="event" <select name="subevent" class="form-control{% if auto_submit %} simple-subevent-choice{% endif %}" data-model-select2="event"
data-select2-url="{% url "control:event.subevents.select2" organizer=request.event.organizer.slug event=request.event.slug %}" data-select2-url="{% url "control:event.subevents.select2" organizer=request.event.organizer.slug event=request.event.slug %}"
data-placeholder="{% trans "All dates" context "subevent" %}"> data-placeholder="{% trans "All dates" context "subevent" %}">
{% for se in selected_subevents %} {% for se in selected_subevents %}
@@ -98,7 +98,7 @@
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<form class="form-inline helper-display-inline" action="" method="get"> <form class="form-inline helper-display-inline" action="" method="get">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %} {% include "pretixcontrol/event/fragment_subevent_choice_simple.html" with auto_submit=True %}
</form> </form>
{% endif %} {% endif %}
{% if not request.event.has_subevents or subevent %} {% if not request.event.has_subevents or subevent %}
@@ -193,6 +193,7 @@
{% bootstrap_field sform.checkout_phone_helptext layout="control" %} {% bootstrap_field sform.checkout_phone_helptext layout="control" %}
{% bootstrap_field sform.banner_text layout="control" %} {% bootstrap_field sform.banner_text layout="control" %}
{% bootstrap_field sform.banner_text_bottom layout="control" %} {% bootstrap_field sform.banner_text_bottom layout="control" %}
{% bootstrap_field sform.event_info_text layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Shop design" %}</legend> <legend>{% trans "Shop design" %}</legend>
@@ -248,6 +249,9 @@
{% bootstrap_field sform.waiting_list_enabled layout="control" %} {% bootstrap_field sform.waiting_list_enabled layout="control" %}
{% bootstrap_field sform.waiting_list_auto layout="control" %} {% bootstrap_field sform.waiting_list_auto layout="control" %}
{% bootstrap_field sform.waiting_list_hours layout="control" %} {% bootstrap_field sform.waiting_list_hours layout="control" %}
{% bootstrap_field sform.waiting_list_names_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Item metadata" %}</legend> <legend>{% trans "Item metadata" %}</legend>
@@ -15,7 +15,7 @@
</p> </p>
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<form class="form-inline helper-display-inline" action="" method="get"> <form class="form-inline helper-display-inline" action="" method="get">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %} {% include "pretixcontrol/event/fragment_subevent_choice_simple.html" with auto_submit=True %}
</form> </form>
{% endif %} {% endif %}
{% if quotas|length == 0 %} {% if quotas|length == 0 %}
@@ -23,13 +23,16 @@
<input type="hidden" name="status" value="c"/> <input type="hidden" name="status" value="c"/>
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
{% bootstrap_field form.send_email layout='' %} {% bootstrap_field form.send_email layout='' %}
{% if form.cancel_invoice %}
{% bootstrap_field form.cancel_invoice layout='' %}
{% endif %}
{% if form.cancellation_fee %} {% if form.cancellation_fee %}
{% bootstrap_field form.cancellation_fee layout='' %} {% bootstrap_field form.cancellation_fee layout='' %}
{% endif %} {% endif %}
<div class="row checkout-button-row"> <div class="row checkout-button-row">
<div class="col-md-4"> <div class="col-md-4">
<a class="btn btn-block btn-default btn-lg" <a class="btn btn-block btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"> href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% trans "No, take me back" %} {% trans "No, take me back" %}
</a> </a>
</div> </div>
@@ -24,6 +24,7 @@
<form method="post" class="form-horizontal" href=""> <form method="post" class="form-horizontal" href="">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
<div class="form-group submit-group"> <div class="form-group submit-group">
<a class="btn btn-default btn-lg" <a class="btn btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"> href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
@@ -170,6 +170,10 @@
</span> </span>
{% endif %} {% endif %}
{{ o.total|money:request.event.currency }} {{ o.total|money:request.event.currency }}
{% if o.status == "c" and o.icnt %}
<br>
<span class="label label-warning">{% trans "INVOICE NOT CANCELED" %}</span>
{% endif %}
</td> </td>
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td> <td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td>
<td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td> <td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
@@ -5,7 +5,7 @@
{% block title %}{% trans "Voucher" %}{% endblock %} {% block title %}{% trans "Voucher" %}{% endblock %}
{% block inside %} {% block inside %}
<h1>{% trans "Create multiple vouchers" %}</h1> <h1>{% trans "Create multiple vouchers" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal" data-asynctask>
{% csrf_token %} {% csrf_token %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
<fieldset> <fieldset>
@@ -49,7 +49,7 @@
<td> <td>
<strong> <strong>
{% if t.tag %} {% if t.tag %}
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ t.tag|urlencode }}"> <a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
{{ t.tag }} {{ t.tag }}
</a> </a>
{% else %} {% else %}
@@ -48,15 +48,9 @@
</p> </p>
{% endif %} {% endif %}
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<select name="subevent" class="form-control"> <div class="col-md-6">
<option value="">{% trans "All dates" context "subevent" %}</option> {% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
{% for se in request.event.subevents.all %} </div>
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} – {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
{% endif %} {% endif %}
<button class="btn btn-large btn-primary" type="submit"> <button class="btn btn-large btn-primary" type="submit">
{% trans "Send as many vouchers as possible" %} {% trans "Send as many vouchers as possible" %}
@@ -80,59 +74,63 @@
</div> </div>
</div> </div>
<p> <form class="row filter-form" action="" method="get">
<form class="form-inline helper-display-inline" action="" method="get"> <div class="col-lg-2 col-md-3 col-xs-6">
<select name="status" class="form-control"> <select name="status" class="form-control">
<option value="a" <option value="a"
{% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "All entries" %}</option> {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "All entries" %}</option>
<option value="w" <option value="w"
{% if request.GET.status == "w" or not request.GET.status %}selected="selected"{% endif %}> {% if request.GET.status == "w" or not request.GET.status %}selected="selected"{% endif %}>
{% trans "Waiting for a voucher" %}</option> {% trans "Waiting for a voucher" %}</option>
<option value="s" <option value="s"
{% if request.GET.status == "s" %}selected="selected"{% endif %}>{% trans "Voucher assigned" %}</option> {% if request.GET.status == "s" %}selected="selected"{% endif %}>{% trans "Voucher assigned" %}</option>
<option value="v" <option value="v"
{% if request.GET.status == "v" %}selected="selected"{% endif %}> {% if request.GET.status == "v" %}selected="selected"{% endif %}>
{% trans "Waiting for redemption" %}</option> {% trans "Waiting for redemption" %}</option>
<option value="r" <option value="r"
{% if request.GET.status == "r" %}selected="selected"{% endif %}> {% if request.GET.status == "r" %}selected="selected"{% endif %}>
{% trans "Successfully redeemed" %}</option> {% trans "Successfully redeemed" %}</option>
<option value="e" <option value="e"
{% if request.GET.status == "e" %}selected="selected"{% endif %}> {% if request.GET.status == "e" %}selected="selected"{% endif %}>
{% trans "Voucher expired" %}</option> {% trans "Voucher expired" %}</option>
</select> </select>
<select name="item" class="form-control"> </div>
<option value="">{% trans "All products" %}</option> <div class="col-lg-2 col-md-3 col-xs-6">
{% for item in items %} <select name="item" class="form-control">
<option value="{{ item.id }}" <option value="">{% trans "All products" %}</option>
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}> {% for item in items %}
{{ item }} <option value="{{ item.id }}"
</option> {% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
{% endfor %} {{ item }}
</select>
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} – {{ se.get_date_range_display }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div>
{% if request.event.has_subevents %}
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
</div>
{% endif %} {% endif %}
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button> <div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<a href="?{% url_replace request "download" "yes" %}" <button class="btn btn-primary" type="submit"><span class="fa fa-filter"></span> {% trans "Filter" %}</button>
<a href="?{% url_replace request "download" "yes" %}"
class="btn btn-default"><i class="fa fa-download"></i> class="btn btn-default"><i class="fa fa-download"></i>
{% trans "Download list" %}</a> {% trans "Download list" %}</a>
</div>
</form> </form>
</p>
<form method="post" action="?next={{ request.get_full_path|urlencode }}"> <form method="post" action="?next={{ request.get_full_path|urlencode }}">
{% csrf_token %} {% csrf_token %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-condensed table-hover"> <table class="table table-condensed table-hover">
<thead> <thead>
<tr> <tr>
<th>{% trans "User" %}</th> {% if request.event.settings.waiting_list_names_asked %}
<th>{% trans "Name" %}</th>
{% endif %}
<th>{% trans "Email" %}</th>
{% if request.event.settings.waiting_list_phones_asked %}
<th>{% trans "Phone number" %}</th>
{% endif %}
<th>{% trans "Product" %}</th> <th>{% trans "Product" %}</th>
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %}</th> <th>{% trans "Date" context "subevent" %}</th>
@@ -146,7 +144,13 @@
<tbody> <tbody>
{% for e in entries %} {% for e in entries %}
<tr> <tr>
{% if request.event.settings.waiting_list_names_asked %}
<td>{{ e.name|default:"" }}</td>
{% endif %}
<td>{{ e.email }}</td> <td>{{ e.email }}</td>
{% if request.event.settings.waiting_list_phones_asked %}
<td>{{ e.phone|default:"" }}</td>
{% endif %}
<td> <td>
{{ e.item }} {{ e.item }}
{% if e.variation %} {% if e.variation %}
@@ -154,7 +158,7 @@
{% endif %} {% endif %}
</td> </td>
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<td>{{ e.subevent.name }} – {{ e.subevent.get_date_range_display }}</td> <td>{{ e.subevent.name }} – {{ e.subevent.get_date_range_display }} {{ e.subevent.date_from|date:"TIME_FORMAT" }}</td>
{% endif %} {% endif %}
<td> <td>
{{ e.created|date:"SHORT_DATETIME_FORMAT" }} {{ e.created|date:"SHORT_DATETIME_FORMAT" }}
+5 -6
View File
@@ -691,14 +691,14 @@ class MailSettingsRendererPreview(MailSettingsPreview):
expires=now(), code="PREVIEW", total=119) expires=now(), code="PREVIEW", total=119)
item = request.event.items.create(name=gettext("Sample product"), default_price=42.23, item = request.event.items.create(name=gettext("Sample product"), default_price=42.23,
description=gettext("Sample product description")) description=gettext("Sample product description"))
p = order.positions.create(item=item, attendee_name_parts={'_legacy': gettext("John Doe")}, order.positions.create(item=item, attendee_name_parts={'_legacy': gettext("John Doe")},
price=item.default_price) price=item.default_price, subevent=request.event.subevents.last())
v = renderers[request.GET.get('renderer')].render( v = renderers[request.GET.get('renderer')].render(
v, v,
str(request.event.settings.mail_text_signature), str(request.event.settings.mail_text_signature),
gettext('Your order: %(code)s') % {'code': order.code}, gettext('Your order: %(code)s') % {'code': order.code},
order, order,
position=p position=None
) )
r = HttpResponse(v, content_type='text/html') r = HttpResponse(v, content_type='text/html')
r._csp_ignore = True r._csp_ignore = True
@@ -955,11 +955,10 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
return reverse('control:index') return reverse('control:index')
class EventLog(EventPermissionRequiredMixin, ListView): class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/event/logs.html' template_name = 'pretixcontrol/event/logs.html'
model = LogEntry model = LogEntry
context_object_name = 'logs' context_object_name = 'logs'
paginate_by = 20
def get_queryset(self): def get_queryset(self):
qs = self.request.event.logentry_set.all().select_related( qs = self.request.event.logentry_set.all().select_related(
@@ -1363,7 +1362,7 @@ class QuickSetupView(FormView):
tax_rule=tax_rule, tax_rule=tax_rule,
admission=True, admission=True,
position=i, position=i,
sales_channels=[k for k in get_all_sales_channels().keys()] sales_channels=list(get_all_sales_channels().keys())
) )
item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data)) item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data))
if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']: if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']:
+25 -3
View File
@@ -83,7 +83,7 @@ from pretix.control.forms.orders import (
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm, ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
OrderLocaleForm, OrderMailForm, OrderPositionAddForm, OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm, OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
OrderRefundForm, OtherOperationsForm, OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import order_search_forms from pretix.control.signals import order_search_forms
@@ -151,6 +151,11 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
s = OrderPosition.objects.filter( s = OrderPosition.objects.filter(
order=OuterRef('pk') order=OuterRef('pk')
).order_by().values('order').annotate(k=Count('id')).values('k') ).order_by().values('order').annotate(k=Count('id')).values('k')
i = Invoice.objects.filter(
order=OuterRef('pk'),
is_cancellation=False,
refered__isnull=True,
).order_by().values('order').annotate(k=Count('id')).values('k')
annotated = { annotated = {
o['pk']: o o['pk']: o
for o in for o in
@@ -158,10 +163,11 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
pk__in=[o.pk for o in ctx['orders']] pk__in=[o.pk for o in ctx['orders']]
).annotate( ).annotate(
pcnt=Subquery(s, output_field=IntegerField()), pcnt=Subquery(s, output_field=IntegerField()),
icnt=Subquery(i, output_field=IntegerField()),
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk'))) has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
).values( ).values(
'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund', 'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund',
'has_pending_refund', 'has_cancellation_request', 'computed_payment_refund_sum' 'has_pending_refund', 'has_cancellation_request', 'computed_payment_refund_sum', 'icnt'
) )
} }
@@ -177,6 +183,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
o.has_pending_refund = annotated.get(o.pk)['has_pending_refund'] o.has_pending_refund = annotated.get(o.pk)['has_pending_refund']
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request'] o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum'] o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum']
o.icnt = annotated.get(o.pk)['icnt']
o.sales_channel_obj = scs[o.sales_channel] o.sales_channel_obj = scs[o.sales_channel]
if ctx['page_obj'].paginator.count < 1000: if ctx['page_obj'].paginator.count < 1000:
@@ -1134,6 +1141,7 @@ class OrderTransition(OrderView):
try: try:
cancel_order(self.order.pk, user=self.request.user, cancel_order(self.order.pk, user=self.request.user,
send_mail=self.mark_canceled_form.cleaned_data['send_email'], send_mail=self.mark_canceled_form.cleaned_data['send_email'],
cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True),
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee')) cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
except OrderError as e: except OrderError as e:
messages.error(self.request, str(e)) messages.error(self.request, str(e))
@@ -1416,11 +1424,24 @@ class OrderExtend(OrderView):
class OrderReactivate(OrderView): class OrderReactivate(OrderView):
permission = 'can_change_orders' permission = 'can_change_orders'
@cached_property
def reactivate_form(self):
return ReactivateOrderForm(
instance=self.order,
data=self.request.POST if self.request.method == "POST" else None,
)
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
if not self.reactivate_form.is_valid():
return render(self.request, 'pretixcontrol/order/reactivate.html', {
'form': self.reactivate_form,
'order': self.order,
})
try: try:
reactivate_order( reactivate_order(
self.order, self.order,
user=self.request.user user=self.request.user,
force=self.reactivate_form.cleaned_data.get('force', False)
) )
messages.success(self.request, _('The order has been reactivated.')) messages.success(self.request, _('The order has been reactivated.'))
except OrderError as e: except OrderError as e:
@@ -1445,6 +1466,7 @@ class OrderReactivate(OrderView):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
return render(self.request, 'pretixcontrol/order/reactivate.html', { return render(self.request, 'pretixcontrol/order/reactivate.html', {
'form': self.reactivate_form,
'order': self.order, 'order': self.order,
}) })
+2 -2
View File
@@ -938,6 +938,7 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
template_name = 'pretixcontrol/organizers/giftcards.html' template_name = 'pretixcontrol/organizers/giftcards.html'
permission = 'can_manage_gift_cards' permission = 'can_manage_gift_cards'
context_object_name = 'giftcards' context_object_name = 'giftcards'
paginate_by = 50
def get_queryset(self): def get_queryset(self):
s = GiftCardTransaction.objects.filter( s = GiftCardTransaction.objects.filter(
@@ -1437,12 +1438,11 @@ class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionR
return redirect(success_url) return redirect(success_url)
class LogView(OrganizerPermissionRequiredMixin, ListView): class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/organizers/logs.html' template_name = 'pretixcontrol/organizers/logs.html'
permission = 'can_change_organizer_settings' permission = 'can_change_organizer_settings'
model = LogEntry model = LogEntry
context_object_name = 'logs' context_object_name = 'logs'
paginate_by = 20
def get_queryset(self): def get_queryset(self):
qs = self.request.organizer.all_logentries().select_related( qs = self.request.organizer.all_logentries().select_related(
+1 -1
View File
@@ -1184,7 +1184,7 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
for fname in ('size', 'name', 'release_after_exit'): for fname in ('size', 'name', 'release_after_exit'):
setattr(q, fname, f.cleaned_data.get(fname)) setattr(q, fname, f.cleaned_data.get(fname))
q.save(clear_cache=False) q.save(clear_cache=False)
if 'itemvar' in f.changed_data: if 'itemvars' in f.changed_data:
q.items.set(selected_items) q.items.set(selected_items)
q.variations.set(selected_variations) q.variations.set(selected_variations)
log_entries.append( log_entries.append(
+1 -1
View File
@@ -255,7 +255,7 @@ def subevent_select2(request, **kwargs):
qs = request.event.subevents.filter( qs = request.event.subevents.filter(
qf qf
).order_by('-date_from') ).order_by('-date_from', 'name', 'pk')
total = qs.count() total = qs.count()
pagesize = 20 pagesize = 20
+85 -31
View File
@@ -3,7 +3,8 @@ import io
from defusedcsv import csv from defusedcsv import csv
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.core.exceptions import ValidationError
from django.db import connection, transaction
from django.db.models import Sum from django.db.models import Sum
from django.http import ( from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
@@ -21,7 +22,9 @@ from django.views.generic import (
from pretix.base.models import CartPosition, LogEntry, OrderPosition, Voucher from pretix.base.models import CartPosition, LogEntry, OrderPosition, Voucher
from pretix.base.models.vouchers import _generate_random_code from pretix.base.models.vouchers import _generate_random_code
from pretix.base.services.locking import NoLockManager
from pretix.base.services.vouchers import vouchers_send from pretix.base.services.vouchers import vouchers_send
from pretix.base.views.tasks import AsyncFormView
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
@@ -287,13 +290,19 @@ class VoucherGo(EventPermissionRequiredMixin, View):
return redirect('control:event.vouchers', event=request.event.slug, organizer=request.event.organizer.slug) return redirect('control:event.vouchers', event=request.event.slug, organizer=request.event.organizer.slug)
class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView): class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
model = Voucher model = Voucher
template_name = 'pretixcontrol/vouchers/bulk.html' template_name = 'pretixcontrol/vouchers/bulk.html'
permission = 'can_change_vouchers' permission = 'can_change_vouchers'
context_object_name = 'voucher' context_object_name = 'voucher'
def get_success_url(self) -> str: def get_success_url(self, value) -> str:
return reverse('control:event.vouchers', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
def get_error_url(self):
return reverse('control:event.vouchers', kwargs={ return reverse('control:event.vouchers', kwargs={
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug, 'event': self.request.event.slug,
@@ -316,34 +325,84 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
i.redeemed = 0 i.redeemed = 0
kwargs['instance'] = i kwargs['instance'] = i
else: else:
kwargs['instance'] = Voucher(event=self.request.event) kwargs['instance'] = Voucher(event=self.request.event, code=None)
return kwargs return kwargs
@transaction.atomic def get_async_form_kwargs(self, form_kwargs, organizer=None, event=None):
def form_valid(self, form): if not form_kwargs.get('instance'):
log_entries = [] form_kwargs['instance'] = Voucher(event=self.request.event, code=None)
objs = form.save(self.request.event) return form_kwargs
def async_form_valid(self, task, form):
lockfn = NoLockManager
if form.data.get('block_quota'):
lockfn = self.request.event.lock
batch_size = 500
total_num = 1 # will be set later
def set_progress(percent):
if not task.request.called_directly:
task.update_state(
state='PROGRESS',
meta={'value': percent}
)
def process_batch(batch_vouchers, voucherids):
Voucher.objects.bulk_create(batch_vouchers)
if not connection.features.can_return_rows_from_bulk_insert:
batch_vouchers = list(self.request.event.vouchers.filter(code__in=[v.code for v in batch_vouchers]))
log_entries = []
for v in batch_vouchers:
voucherids.append(v.pk)
data = dict(form.cleaned_data)
data['code'] = code
data['bulk'] = True
del data['codes']
log_entries.append(
v.log_action('pretix.voucher.added', data=data, user=self.request.user, save=False)
)
LogEntry.objects.bulk_create(log_entries)
form.post_bulk_save(batch_vouchers)
batch_vouchers.clear()
set_progress(len(voucherids) / total_num * (50. if form.cleaned_data['send'] else 100.))
voucherids = [] voucherids = []
for v in objs: with lockfn(), transaction.atomic():
log_entries.append( if not form.is_valid():
v.log_action('pretix.voucher.added', data=form.cleaned_data, user=self.request.user, save=False) raise ValidationError(form.errors)
) total_num = len(form.cleaned_data['codes'])
voucherids.append(v.pk)
LogEntry.objects.bulk_create(log_entries, batch_size=200) batch_vouchers = []
for code in form.cleaned_data['codes']:
if len(batch_vouchers) > batch_size:
process_batch(batch_vouchers, voucherids)
obj = modelcopy(form.instance, code=None)
obj.event = self.request.event
obj.code = code
try:
obj.seat = form.cleaned_data['seats'].pop()
obj.item = obj.seat.product
except IndexError:
pass
batch_vouchers.append(obj)
process_batch(batch_vouchers, voucherids)
if form.cleaned_data['send']: if form.cleaned_data['send']:
vouchers_send.apply_async(kwargs={ vouchers_send(
'event': self.request.event.pk, event=self.request.event,
'vouchers': voucherids, vouchers=voucherids,
'subject': form.cleaned_data['send_subject'], subject=form.cleaned_data['send_subject'],
'message': form.cleaned_data['send_message'], message=form.cleaned_data['send_message'],
'recipients': [r._asdict() for r in form.cleaned_data['send_recipients']], recipients=[r._asdict() for r in form.cleaned_data['send_recipients']],
'user': self.request.user.pk, user=self.request.user.pk,
}) progress=lambda p: set_progress(50. + p * 50.)
messages.success(self.request, _('The new vouchers have been created and will be sent out shortly.')) )
else:
messages.success(self.request, _('The new vouchers have been created.')) def get_success_message(self, value):
return HttpResponseRedirect(self.get_success_url()) return _('The new vouchers have been created.')
def get_form_class(self): def get_form_class(self):
form_class = VoucherBulkForm form_class = VoucherBulkForm
@@ -357,11 +416,6 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
ctx['code_length'] = settings.ENTROPY['voucher_code'] ctx['code_length'] = settings.ENTROPY['voucher_code']
return ctx return ctx
def post(self, request, *args, **kwargs):
# TODO: Transform this into an asynchronous call?
with request.event.lock():
return super().post(request, *args, **kwargs)
class VoucherRNG(EventPermissionRequiredMixin, View): class VoucherRNG(EventPermissionRequiredMixin, View):
permission = 'can_change_vouchers' permission = 'can_change_vouchers'
+3 -1
View File
@@ -211,7 +211,7 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",") writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
headers = [ headers = [
_('E-mail address'), _('Product'), _('On list since'), _('Status'), _('Voucher code'), _('Name'), _('E-mail address'), _('Phone number'), _('Product'), _('On list since'), _('Status'), _('Voucher code'),
_('Language'), _('Priority') _('Language'), _('Priority')
] ]
if self.request.event.has_subevents: if self.request.event.has_subevents:
@@ -235,7 +235,9 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
status = _('Waiting') status = _('Waiting')
row = [ row = [
w.name,
w.email, w.email,
w.phone,
prod, prod,
w.created.isoformat(), w.created.isoformat(),
status, status,
+1 -1
View File
@@ -94,7 +94,7 @@ def merge(*args):
"""Implements the 'merge' operator for merging lists.""" """Implements the 'merge' operator for merging lists."""
ret = [] ret = []
for arg in args: for arg in args:
if isinstance(arg, list) or isinstance(arg, tuple): if isinstance(arg, (list, tuple)):
ret += list(arg) ret += list(arg)
else: else:
ret.append(arg) ret.append(arg)
+2 -2
View File
@@ -12,8 +12,8 @@ class Thumbnail(models.Model):
unique_together = (('source', 'size'),) unique_together = (('source', 'size'),)
def modelcopy(obj: models.Model): def modelcopy(obj: models.Model, **kwargs):
n = obj.__class__() n = obj.__class__(**kwargs)
for f in obj._meta.fields: for f in obj._meta.fields:
val = getattr(obj, f.name) val = getattr(obj, f.name)
if isinstance(val, models.Model): if isinstance(val, models.Model):
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-07-30 19:00+0000\n" "PO-Revision-Date: 2020-07-30 19:00+0000\n"
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n" "Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n" "PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n" "Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-12-14 10:00+0000\n" "PO-Revision-Date: 2020-12-14 10:00+0000\n"
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n" "Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-09-15 02:00+0000\n" "PO-Revision-Date: 2020-09-15 02:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n" "Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-08-25 02:00+0000\n" "PO-Revision-Date: 2020-08-25 02:00+0000\n"
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n" "Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-08-25 02:00+0000\n" "PO-Revision-Date: 2020-08-25 02:00+0000\n"
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n" "Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/" "Language-Team: German (informal) <https://translate.pretix.eu/projects/"
+1344 -1239
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2019-10-03 19:00+0000\n" "PO-Revision-Date: 2019-10-03 19:00+0000\n"
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n" "Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-04-27 20:00+0000\n" "PO-Revision-Date: 2020-04-27 20:00+0000\n"
"Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n" "Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2021-01-20 16:10+0000\n" "PO-Revision-Date: 2021-01-20 16:10+0000\n"
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n" "Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: French\n" "Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-09-15 17:00+0000\n" "PO-Revision-Date: 2020-09-15 17:00+0000\n"
"Last-Translator: Martin Gross <martin@pc-coholic.de>\n" "Last-Translator: Martin Gross <martin@pc-coholic.de>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-01-24 08:00+0000\n" "PO-Revision-Date: 2020-01-24 08:00+0000\n"
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n" "Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2020-06-12 20:00+0000\n" "PO-Revision-Date: 2020-06-12 20:00+0000\n"
"Last-Translator: Frank <webappconcept@gmail.com>\n" "Last-Translator: Frank <webappconcept@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: 2019-11-13 06:00+0000\n" "PO-Revision-Date: 2019-11-13 06:00+0000\n"
"Last-Translator: Zane Smite <z.smite@riga-jurmala.com>\n" "Last-Translator: Zane Smite <z.smite@riga-jurmala.com>\n"
"Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n" "POT-Creation-Date: 2021-03-08 16:40+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
File diff suppressed because it is too large Load Diff

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