Compare commits

...

83 Commits

Author SHA1 Message Date
Raphael Michel ffd0612277 API: Add webhooks for customer events 2023-08-31 14:13:48 +02:00
Raphael Michel 53e84dfb08 API: Fix validation of duplicate customer email addresses 2023-08-30 16:57:15 +02:00
Raphael Michel 2e8447486c Improve edge cases in handling of check-in nonces (#3516) 2023-08-30 10:43:24 +02:00
robbi5 5b184bb1a0 Fix leaflet osm tile suggestion (#3549)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-08-30 09:53:41 +02:00
Ash So 8c6f0a5dc1 Translations: Update Chinese (Traditional)
Currently translated at 99.7% (5384 of 5400 strings)

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

powered by weblate
2023-08-30 09:46:11 +02:00
Alain 6a53091b91 Translations: Update Dutch
Currently translated at 84.4% (4559 of 5400 strings)

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

powered by weblate
2023-08-30 09:46:11 +02:00
Mira be4bc9a6f3 TemplateBasedMailRenderer: make markdown compiler call overridable (#3550) 2023-08-30 09:41:34 +02:00
Raphael Michel efb1141d59 PayPal: Add missing payment.fail() statements 2023-08-29 15:10:05 +02:00
Raphael Michel 322a730eb2 PayPal: Fix incorrect Decimal comparison 2023-08-29 15:06:03 +02:00
Raphael Michel 8d2224e725 API: Allow organizer-level access of orders and invoices (#3547) 2023-08-28 16:54:42 +02:00
Raphael Michel 5b819b76f0 Check-in: Fix N+1 query issue identified by sentry 2023-08-28 16:54:09 +02:00
Raphael Michel 5d90a42acf Discounts: Allow "buy X to get Y" with different product sets for X and Y (#3543) 2023-08-28 16:21:52 +02:00
Raphael Michel 5398671fde Fix crash in invoice address detection (PRETIXEU-8XE) 2023-08-28 11:45:30 +02:00
Raphael Michel f7d4460deb Fix N+1 query issues detected by Sentry 2023-08-26 16:24:03 +02:00
Alain f76576a587 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 73.7% (3982 of 5400 strings)

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

powered by weblate
2023-08-24 18:07:17 +02:00
Alain cf5f0dc7f9 Translations: Update Dutch
Currently translated at 80.6% (171 of 212 strings)

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

powered by weblate
2023-08-24 18:07:17 +02:00
Alain 567984bd5e Translations: Update Dutch
Currently translated at 84.2% (4549 of 5400 strings)

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

powered by weblate
2023-08-24 18:07:17 +02:00
Raphael Michel 1c6bd46d21 Order data export: Add product/variation ID (#3542) 2023-08-24 18:02:24 +02:00
Mira 9ba3227837 Checkout: Prefill is_business heuristically (Z#23126061) (#3533) 2023-08-24 17:06:47 +02:00
Richard Schreiber 21864885cb Checkout: improve heuristic to open invoice-panel (#3545) 2023-08-24 16:15:47 +02:00
Mira 38173e3a54 Tax rules: add custom rules for country subdivision (e.g. state) (Z#23111850) (#3520) 2023-08-24 14:11:10 +02:00
Phin Wolkwitz 4baf317934 Automated emails: Extend filter by check-in state (#3489)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Raphael Michel <michel@rami.io>
2023-08-23 16:19:27 +02:00
Raphael Michel c2b25bad06 Event dashboard: Fix incorrect optimization introduced in 8e9f0f07a (#3540) 2023-08-23 14:54:38 +02:00
Raphael Michel 9e3ad6c05c Order payment step: Pass info_data to checkout_confirm_render 2023-08-23 12:18:49 +02:00
Raphael Michel f017de1a21 Voucher bulk creation: Fix validation issue 2023-08-23 12:18:30 +02:00
Raphael Michel b56bd8541e Devices: Fix bulk edit query (#3541) 2023-08-23 11:20:26 +02:00
Raphael Michel 1c9219609a Fix progress callback for slow_delete helper 2023-08-23 10:50:39 +02:00
Raphael Michel 0c96f758a8 Fix quota cache mixup (#3539)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-08-23 10:09:50 +02:00
Richard Schreiber 9bd3444aad PDF: fix deduplicated list of addons (exclude canceled) (#3538) 2023-08-22 14:05:30 +02:00
Raphael Michel 10a83935d9 CartManager: Fix TransactionManagementError
Bug occured when extending a product and deleting it at the same time
2023-08-22 13:42:56 +02:00
Raphael Michel e8ea6e0f5c Item creation: Fix failing test 2023-08-22 12:59:57 +02:00
Raphael Michel e94e5be878 Item creation: Fix bug in copying meta data 2023-08-22 11:32:43 +02:00
Richard Schreiber 1073ea626e Banktransfer: make row-headers sticky (Z#23127000) (#3537) 2023-08-22 10:53:26 +02:00
Raphael Michel 23ab8df443 Translations: Add Welsh 2023-08-22 10:53:15 +02:00
Kian Cross d6caf01a38 Add warning about configuration of Celery in development mode to docs (#3525) 2023-08-22 10:44:11 +02:00
Raphael Michel 1424ae78e9 Revert accidental change 2023-08-22 10:20:19 +02:00
Raphael Michel 827382edc3 Bump redis to 4.6.* 2023-08-22 09:43:21 +02:00
Maurice Kaag 85482bc939 Translations: Update French
Currently translated at 100.0% (5400 of 5400 strings)

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

powered by weblate
2023-08-22 09:20:21 +02:00
Felix Hartnagel 42ce545f2f Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5400 of 5400 strings)

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

powered by weblate
2023-08-22 09:20:21 +02:00
Raphael Michel e49bc5d78d Item creation: Fix crash (PRETIXEU-8VE) 2023-08-22 09:14:23 +02:00
Richard Schreiber 6e7a32ef2a Vouchers: improve batch-select UI 2023-08-22 09:11:14 +02:00
Raphael Michel 37df7a6313 Allow PDF variables to provide a bulk evaluation method (second try at #3517) (#3535) 2023-08-21 17:59:55 +02:00
Raphael Michel d5951415a4 Item creation: Fix saving meta data (#3534) 2023-08-21 16:21:17 +02:00
Raphael Michel 691159ed83 Check-in list: Fix ordering by seat 2023-08-21 15:41:50 +02:00
Raphael Michel 18f517af44 Waiting list: Extend compatibility note 2023-08-21 14:52:39 +02:00
Raphael Michel 89ba2da7e7 QR code generator for voucher URLs and general URLs (#3518)
* QR code generator: Allow other URLs to be used (e.g. for plugins)

* Add QR code to voucher URL view

* Fix allowed_hosts

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-08-17 10:10:27 +02:00
Raphael Michel c1c47e50c3 Voucher redemption: Display event title in some cases (#3519)
* Voucher redemption: Display event title in some cases (Z#23127871)

* Remove unnecessary "with" statement

* fix indentation

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-08-17 09:17:47 +02:00
Richard Schreiber f262cd632c Chekout: make disabling sneak-peek more robust (#3527) 2023-08-16 15:02:02 +02:00
Richard Schreiber 8d58294af1 Fix typeahead item variations order_by 2023-08-16 10:01:27 +02:00
Richard Schreiber ddc94a8a16 Revert "Allow PDF variables to provide a bulk evaluation method (#3517)"
This reverts commit 6ada83df9a.
2023-08-14 15:11:13 +02:00
Raphael Michel 83811c0343 Fix minor CSS issue in button groups 2023-08-10 14:12:19 +02:00
Raphael Michel b2c05a72e5 Voucher list: Fix ordering by product 2023-08-10 11:29:10 +02:00
Martin Gross 8c56a23562 Add logentry plain for pretix.giftcards.acceptance.acceptor.removed 2023-08-10 11:21:54 +02:00
Raphael Michel 53e1d9c6c4 Tests: Fix improper cleanup of SITE_URL 2023-08-10 11:20:26 +02:00
Mira 6250ab2165 Bank transfer: Allow customer to send latest invoice via email (Z#207218) (#3511)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-08-09 18:23:45 +02:00
Raphael Michel 6ada83df9a Allow PDF variables to provide a bulk evaluation method (#3517) 2023-08-09 18:22:56 +02:00
Raphael Michel cfd6376936 Fix transaction view after Django upgrade 2023-08-09 17:11:20 +02:00
Raphael Michel edb0cd0941 Update STORAGES in docker settings 2023-08-09 15:01:21 +02:00
Raphael Michel 88ac407cf3 Cart: Disable sneak peek on very small carts (#3512) 2023-08-09 14:53:50 +02:00
dependabot[bot] 5ba56fb5ac Bump @babel/core from 7.22.5 to 7.22.9 in /src/pretix/static/npm_dir (#3501)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 14:53:34 +02:00
Raphael Michel b51c9f7552 Upgrade to Django 4.2 (#3497) 2023-08-09 14:47:41 +02:00
Ronan LE MEILLAT 0853296663 Translations: Update French
Currently translated at 99.9% (5398 of 5400 strings)

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

powered by weblate
2023-08-09 14:47:26 +02:00
Raphael Michel 721e7549bc Remove forgotten debug statement 2023-08-09 10:34:24 +02:00
Martin Gross aee86de330 Import: Allow to import "False"-value (Z#23127414) (#3505) 2023-08-08 15:36:51 +02:00
Raphael Michel 756a4355d1 Use newer postgres version for test 2023-08-08 15:32:02 +02:00
Mira 5119bbd0b1 Docs: Update i18n.rst (fix dead link) (#3513) 2023-08-08 15:04:51 +02:00
Raphael Michel 728bd74e28 Organizer settings: Move save button to the left 2023-08-07 17:44:52 +02:00
Mira 015ffeecbf Main menu: Add load indicator to event selector (#3508) 2023-08-07 14:25:50 +02:00
Raphael Michel 0365f6d9fc Order change manager: Set new expiry date if splitted order is pending (#3509) 2023-08-07 14:13:44 +02:00
Raphael Michel e208a79c32 Docs: Update implementation docs for URL routing (#3510) 2023-08-07 14:13:19 +02:00
ticketflock 0037d37960 Translations: Add English (Old) 2023-08-07 14:04:34 +02:00
ticketflock 50d9b1e4a3 Translations: Add English (Middle) 2023-08-07 14:04:34 +02:00
Patrizia Cotza 7919d012e6 Translations: Update Spanish
Currently translated at 58.5% (3159 of 5400 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Ronan LE MEILLAT 327f95a9cc Translations: Update French
Currently translated at 100.0% (212 of 212 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Ronan LE MEILLAT 98946ded4b Translations: Update French
Currently translated at 99.9% (5398 of 5400 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Ronan LE MEILLAT cf47b69bd3 Translations: Update French
Currently translated at 99.3% (5366 of 5400 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Raphael Michel fa5c69ce0a Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5400 of 5400 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Raphael Michel 39d85fc112 Event creation: Fix rare crash (PRETIXEU-8RD) 2023-08-07 09:47:14 +02:00
Mira 23e222bf13 Sidebar dropdown: remove menu load delay 2023-08-03 14:28:59 +02:00
Raphael Michel cb068b029f Wallet detection: Fix race condition 2023-07-28 17:31:47 +02:00
Raphael Michel 9e95f3be1b Wallet detection: Extend CSP header for google pay 2023-07-28 16:49:11 +02:00
Raphael Michel 401c02865b Voucher form: Sort quotas by date 2023-07-28 16:29:03 +02:00
Raphael Michel 062450002d Bump to 2023.8.0.dev0 2023-07-28 09:30:04 +02:00
140 changed files with 88537 additions and 1187 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v2
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '11'
postgresql version: '15'
postgresql db: 'pretix'
postgresql user: 'postgres'
postgresql password: 'postgres'
+1 -1
View File
@@ -1,4 +1,4 @@
from pretix.settings import *
LOGGING['handlers']['mail_admins']['include_html'] = True
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STORAGES["staticfiles"]["BACKEND"] = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
+33 -2
View File
@@ -31,9 +31,9 @@ subevent_mode strings Determines h
``"same"`` (discount is only applied for groups within
the same date), or ``"distinct"`` (discount is only applied
for groups with no two same dates).
condition_all_products boolean If ``true``, the discount applies to all items.
condition_all_products boolean If ``true``, the discount condition applies to all items.
condition_limit_products list of integers If ``condition_all_products`` is not set, this is a list
of internal item IDs that the discount applies to.
of internal item IDs that the discount condition applies to.
condition_apply_to_addons boolean If ``true``, the discount applies to add-on products as well,
otherwise it only applies to top-level items. The discount never
applies to bundled products.
@@ -48,6 +48,17 @@ benefit_discount_matching_percent decimal (string) The percenta
benefit_only_apply_to_cheapest_n_matches integer If set higher than 0, the discount will only be applied to
the cheapest matches. Useful for a "3 for 2"-style discount.
Cannot be combined with ``condition_min_value``.
benefit_same_products boolean If ``true``, the discount benefit applies to the same set of items
as the condition (see above).
benefit_limit_products list of integers If ``benefit_same_products`` is not set, this is a list
of internal item IDs that the discount benefit applies to.
benefit_apply_to_addons boolean (Only used if ``benefit_same_products`` is ``false``.)
If ``true``, the discount applies to add-on products as well,
otherwise it only applies to top-level items. The discount never
applies to bundled products.
benefit_ignore_voucher_discounted boolean (Only used if ``benefit_same_products`` is ``false``.)
If ``true``, the discount does not apply to products which have
been discounted by a voucher.
======================================== ========================== =======================================================
@@ -94,6 +105,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}
@@ -146,6 +161,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}
@@ -184,6 +203,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}
@@ -211,6 +234,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}
@@ -267,6 +294,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}
+59 -2
View File
@@ -12,6 +12,7 @@ The invoice resource contains the following public fields:
Field Type Description
===================================== ========================== =======================================================
number string Invoice number (with prefix)
event string The slug of the parent event
order string Order code of the order this invoice belongs to
is_cancellation boolean ``true``, if this invoice is the cancellation of a
different invoice.
@@ -121,9 +122,13 @@ internal_reference string Customer's refe
The attribute ``lines.subevent`` has been added.
.. versionchanged:: 2023.8
Endpoints
---------
The ``event`` attribute has been added. The organizer-level endpoint has been added.
List of all invoices
--------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/
@@ -152,6 +157,7 @@ Endpoints
"results": [
{
"number": "SAMPLECONF-00001",
"event": "sampleconf",
"order": "ABC12",
"is_cancellation": false,
"invoice_from_name": "Big Events LLC",
@@ -221,6 +227,50 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/invoices/
Returns a list of all invoices within all events of a given organizer (with sufficient access permissions).
Supported query parameters and output format of this endpoint are identical to the list endpoint within an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"number": "SAMPLECONF-00001",
"event": "sampleconf",
"order": "ABC12",
...
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual invoices
----------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/
Returns information on one invoice, identified by its invoice number.
@@ -243,6 +293,7 @@ Endpoints
{
"number": "SAMPLECONF-00001",
"event": "sampleconf",
"order": "ABC12",
"is_cancellation": false,
"invoice_from_name": "Big Events LLC",
@@ -337,6 +388,12 @@ Endpoints
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Modifying invoices
------------------
Invoices cannot be edited directly, but the following actions can be triggered:
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/reissue/
Cancels the invoice and creates a new one.
+49
View File
@@ -20,6 +20,7 @@ The order resource contains the following public fields:
Field Type Description
===================================== ========================== =======================================================
code string Order code
event string The slug of the parent event
status string Order status, one of:
* ``n`` pending
@@ -130,6 +131,10 @@ last_modified datetime Last modificati
The ``valid_if_pending`` attribute has been added.
.. versionchanged:: 2023.8
The ``event`` attribute has been added. The organizer-level endpoint has been added.
.. _order-position-resource:
@@ -289,6 +294,7 @@ List of all orders
"results": [
{
"code": "ABC12",
"event": "sampleconf",
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
@@ -441,6 +447,48 @@ List of all orders
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/orders/
Returns a list of all orders within all events of a given organizer (with sufficient access permissions).
Supported query parameters and output format of this endpoint are identical to the list endpoint within an event,
with the exception that the ``pdf_data`` parameter is not supported here.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/orders/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
X-Page-Generated: 2017-12-01T10:00:00Z
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"code": "ABC12",
"event": "sampleconf",
...
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual orders
--------------------------
@@ -466,6 +514,7 @@ Fetching individual orders
{
"code": "ABC12",
"event": "sampleconf",
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
+11 -2
View File
@@ -23,10 +23,14 @@ limit_products list of integers List of product
restrict_to_status list List of order states to restrict recipients to. Valid
entries are ``p`` for paid, ``e`` for expired, ``c`` for canceled,
``n__pending_approval`` for pending approval,
``n__not_pending_approval_and_not_valid_if_pending`` for payment pending,
``n__valid_if_pending`` for payment pending but already confirmed,
``n__not_pending_approval_and_not_valid_if_pending`` for payment
pending, ``n__valid_if_pending`` for payment pending but already confirmed,
and ``n__pending_overdue`` for pending with payment overdue.
The default is ``["p", "n__valid_if_pending"]``.
checked_in_status string Check-in status to restrict recipients to. Valid strings are:
``null`` for no filtering (default), ``checked_in`` for
limiting to attendees that are or have been checked in, and
``no_checkin`` for limiting to attendees who have not checked in.
date_is_absolute boolean If ``true``, the email is set at a specific point in time.
send_date datetime If ``date_is_absolute`` is set: Date and time to send the email.
send_offset_days integer If ``date_is_absolute`` is not set, this is the number of days
@@ -89,6 +93,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": null,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -139,6 +144,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": null,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -180,6 +186,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": "checked_in",
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -209,6 +216,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": "checked_in",
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -266,6 +274,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": "checked_in",
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
+3
View File
@@ -67,6 +67,9 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.live.deactivated``
* ``pretix.event.testmode.activated``
* ``pretix.event.testmode.deactivated``
* ``pretix.customer.created``
* ``pretix.customer.changed``
* ``pretix.customer.anonymized``
Installed plugins might register more valid values.
+1 -1
View File
@@ -37,7 +37,7 @@ you to execute a piece of code with a different locale:
This is very useful e.g. when sending an email to a user that has a different language than the user performing the
action that causes the mail to be sent.
.. _translation features: https://docs.djangoproject.com/en/1.9/topics/i18n/translation/
.. _translation features: https://docs.djangoproject.com/en/4.2/topics/i18n/translation/
.. _GNU gettext: https://www.gnu.org/software/gettext/
.. _strings: https://django-i18nfield.readthedocs.io/en/latest/strings.html
.. _database fields: https://django-i18nfield.readthedocs.io/en/latest/quickstart.html
+23 -10
View File
@@ -15,25 +15,33 @@ and the admin panel is available at ``https://pretix.eu/control/event/bigorg/awe
If the organizer now configures a custom domain like ``tickets.bigorg.com``, his event will
from now on be available on ``https://tickets.bigorg.com/awesomecon/``. The former URL at
``pretix.eu`` will redirect there. However, the admin panel will still only be available
on ``pretix.eu`` for convenience and security reasons.
``pretix.eu`` will redirect there. It's also possible to do this for just an event, in which
case the event will be available on ``https://tickets.awesomecon.org/``.
However, the admin panel will still only be available on ``pretix.eu`` for convenience and security reasons.
URL routing
-----------
The hard part about implementing this URL routing in Django is that
``https://pretix.eu/bigorg/awesomecon/`` contains two parameters of nearly arbitrary content
and ``https://tickets.bigorg.com/awesomecon/`` contains only one. The only robust way to do
this is by having *separate* URL configuration for those two cases. In pretix, we call the
former our ``maindomain`` config and the latter our ``subdomain`` config. For pretix's core
modules we do some magic to avoid duplicate configuration, but for a fairly simple plugin with
only a handful of routes, we recommend just configuring the two URL sets separately.
and ``https://tickets.bigorg.com/awesomecon/`` contains only one and ``https://tickets.awesomecon.org/`` does not contain any.
The only robust way to do this is by having *separate* URL configuration for those three cases.
In pretix, we therefore do not have a global URL configuration, but three, living in the following modules:
- ``pretix.multidomain.maindomain_urlconf``
- ``pretix.multidomain.organizer_domain_urlconf``
- ``pretix.multidomain.event_domain_urlconf``
We provide some helper utilities to work with these to avoid duplicate configuration of the individual URLs.
The file ``urls.py`` inside your plugin package will be loaded and scanned for URL configuration
automatically and should be provided by any plugin that provides any view.
However, unlike plain Django, we look not only for a ``urlpatterns`` attribute on the module but support other
attributes like ``event_patterns`` and ``organizer_patterns`` as well.
A very basic example that provides one view in the admin panel and one view in the frontend
could look like this::
For example, for a simple plugin that adds one URL to the backend and one event-level URL to the frontend, you can
create the following configuration in your ``urls.py``::
from django.urls import re_path
@@ -52,7 +60,7 @@ could look like this::
As you can see, the view in the frontend is not included in the standard Django ``urlpatterns``
setting but in a separate list with the name ``event_patterns``. This will automatically prepend
the appropriate parameters to the regex (e.g. the event or the event and the organizer, depending
on the called domain).
on the called domain). For organizer-level views, ``organizer_patterns`` works the same way.
If you only provide URLs in the admin area, you do not need to provide a ``event_patterns`` attribute.
@@ -71,11 +79,16 @@ is a python method that emulates a behavior similar to ``reverse``:
.. autofunction:: pretix.multidomain.urlreverse.eventreverse
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
.. autofunction:: pretix.multidomain.urlreverse.build_absolute_uri
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
as its first argument and can be used like this::
{% load eventurl %}
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
Implementation details
+14
View File
@@ -96,6 +96,20 @@ http://localhost:8000/control/ for the admin view.
port (for example because you develop on `pretixdroid`_), you can check
`Django's documentation`_ for more options.
When running the local development webserver, ensure Celery is not configured
in ``pretix.cfg``. i.e., you should remove anything such as::
[celery]
backend=redis://redis:6379/2
broker=redis://redis:6379/2
If you choose to use Celery for development, you must also start a Celery worker
process::
celery -A pretix.celery_app worker -l info
However, beware that code changes will not auto-reload within Celery.
.. _`checksandtests`:
Code checks and unit tests
+3 -2
View File
@@ -36,7 +36,7 @@ dependencies = [
"css-inline==0.8.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django==4.1.*",
"Django==4.2.*",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
@@ -90,7 +90,7 @@ dependencies = [
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==7.4.*",
"redis==4.5.*,>=4.5.4",
"redis==4.6.*",
"reportlab==4.0.*",
"requests==2.31.*",
"sentry-sdk==1.15.*",
@@ -112,6 +112,7 @@ memcached = ["pylibmc"]
dev = [
"coverage",
"coveralls",
"fakeredis==2.18.*",
"flake8==6.0.*",
"freezegun",
"isort==5.12.*",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2023.7.0"
__version__ = "2023.8.0.dev0"
+8 -1
View File
@@ -196,7 +196,14 @@ STATICFILES_DIRS = [
STATICI18N_ROOT = os.path.join(BASE_DIR, "pretix/static")
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
},
}
# if os.path.exists(os.path.join(DATA_DIR, 'static')):
# STATICFILES_DIRS.insert(0, os.path.join(DATA_DIR, 'static'))
+3 -1
View File
@@ -32,11 +32,13 @@ class DiscountSerializer(I18nAwareModelSerializer):
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
'condition_ignore_voucher_discounted')
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
def validate(self, data):
data = super().validate(data)
+94 -10
View File
@@ -27,6 +27,7 @@ from decimal import Decimal
import pycountry
from django.conf import settings
from django.core.files import File
from django.db import models
from django.db.models import F, Q
from django.utils.encoding import force_str
from django.utils.timezone import now
@@ -283,11 +284,12 @@ class FailedCheckinSerializer(I18nAwareModelSerializer):
raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
raw_subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
nonce = serializers.CharField(required=False, allow_null=True)
class Meta:
model = Checkin
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
'raw_subevent', 'datetime', 'type', 'position')
'raw_subevent', 'nonce', 'datetime', 'type', 'position')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -372,11 +374,15 @@ class PdfDataSerializer(serializers.Field):
self.context['vars_images'] = get_images(self.context['event'])
for k, f in self.context['vars'].items():
try:
res[k] = f['evaluate'](instance, instance.order, ev)
except:
logger.exception('Evaluating PDF variable failed')
res[k] = '(error)'
if 'evaluate_bulk' in f:
# Will be evaluated later by our list serializers
res[k] = (f['evaluate_bulk'], instance)
else:
try:
res[k] = f['evaluate'](instance, instance.order, ev)
except:
logger.exception('Evaluating PDF variable failed')
res[k] = '(error)'
if not hasattr(ev, '_cached_meta_data'):
ev._cached_meta_data = ev.meta_data
@@ -429,6 +435,38 @@ class PdfDataSerializer(serializers.Field):
return res
class OrderPositionListSerializer(serializers.ListSerializer):
def to_representation(self, data):
# We have a custom implementation of this method because PdfDataSerializer() might keep some elements unevaluated
# with a (callable, input) tuple. We'll loop over these entries and evaluate them bulk-wise to save on SQL queries.
if isinstance(self.parent, OrderSerializer) and isinstance(self.parent.parent, OrderListSerializer):
# Do not execute our custom code because it will be executed by OrderListSerializer later for the
# full result set.
return super().to_representation(data)
iterable = data.all() if isinstance(data, models.Manager) else data
data = []
evaluate_queue = defaultdict(list)
for item in iterable:
entry = self.child.to_representation(item)
if "pdf_data" in entry:
for k, v in entry["pdf_data"].items():
if isinstance(v, tuple) and callable(v[0]):
evaluate_queue[v[0]].append((v[1], entry, k))
data.append(entry)
for func, entries in evaluate_queue.items():
results = func([item for (item, entry, k) in entries])
for (item, entry, k), result in zip(entries, results):
entry["pdf_data"][k] = result
return data
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True, read_only=True)
answers = AnswerSerializer(many=True)
@@ -440,6 +478,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
attendee_name = serializers.CharField(required=False)
class Meta:
list_serializer_class = OrderPositionListSerializer
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
@@ -468,6 +507,20 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
def validate(self, data):
raise TypeError("this serializer is readonly")
def to_representation(self, data):
if isinstance(self.parent, (OrderListSerializer, OrderPositionListSerializer)):
# Do not execute our custom code because it will be executed by OrderListSerializer later for the
# full result set.
return super().to_representation(data)
entry = super().to_representation(data)
if "pdf_data" in entry:
for k, v in entry["pdf_data"].items():
if isinstance(v, tuple) and callable(v[0]):
entry["pdf_data"][k] = v[0]([v[1]])[0]
return entry
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
@@ -562,7 +615,7 @@ class PaymentURLField(serializers.URLField):
def to_representation(self, instance: OrderPayment):
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
return None
return build_absolute_uri(self.context['event'], 'presale:event.order.pay', kwargs={
return build_absolute_uri(instance.order.event, 'presale:event.order.pay', kwargs={
'order': instance.order.code,
'secret': instance.order.secret,
'payment': instance.pk,
@@ -607,13 +660,42 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
class OrderURLField(serializers.URLField):
def to_representation(self, instance: Order):
return build_absolute_uri(self.context['event'], 'presale:event.order', kwargs={
return build_absolute_uri(instance.event, 'presale:event.order', kwargs={
'order': instance.code,
'secret': instance.secret,
})
class OrderListSerializer(serializers.ListSerializer):
def to_representation(self, data):
# We have a custom implementation of this method because PdfDataSerializer() might keep some elements
# unevaluated with a (callable, input) tuple. We'll loop over these entries and evaluate them bulk-wise to
# save on SQL queries.
iterable = data.all() if isinstance(data, models.Manager) else data
data = []
evaluate_queue = defaultdict(list)
for item in iterable:
entry = self.child.to_representation(item)
for p in entry.get("positions", []):
if "pdf_data" in p:
for k, v in p["pdf_data"].items():
if isinstance(v, tuple) and callable(v[0]):
evaluate_queue[v[0]].append((v[1], p, k))
data.append(entry)
for func, entries in evaluate_queue.items():
results = func([item for (item, entry, k) in entries])
for (item, entry, k), result in zip(entries, results):
entry["pdf_data"][k] = result
return data
class OrderSerializer(I18nAwareModelSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
invoice_address = InvoiceAddressSerializer(allow_null=True)
positions = OrderPositionSerializer(many=True, read_only=True)
fees = OrderFeeSerializer(many=True, read_only=True)
@@ -627,8 +709,9 @@ class OrderSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
list_serializer_class = OrderListSerializer
fields = (
'code', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url', 'customer', 'valid_if_pending'
@@ -1512,6 +1595,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class InvoiceSerializer(I18nAwareModelSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
refers = serializers.SlugRelatedField(slug_field='full_invoice_no', read_only=True)
lines = InlineInvoiceLineSerializer(many=True)
@@ -1520,7 +1604,7 @@ class InvoiceSerializer(I18nAwareModelSerializer):
class Meta:
model = Invoice
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
fields = ('event', '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',
+8
View File
@@ -94,6 +94,14 @@ class CustomerSerializer(I18nAwareModelSerializer):
data['name_parts']['_scheme'] = self.context['request'].organizer.settings.name_scheme
return data
def validate_email(self, value):
qs = Customer.objects.filter(organizer=self.context['organizer'], email__iexact=value)
if self.instance and self.instance.pk:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError(_("An account with this email address is already registered."))
return value
class CustomerCreateSerializer(CustomerSerializer):
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)
+3 -1
View File
@@ -61,6 +61,8 @@ orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
orga_router.register(r'teams', organizer.TeamViewSet)
orga_router.register(r'devices', organizer.DeviceViewSet)
orga_router.register(r'orders', order.OrganizerOrderViewSet)
orga_router.register(r'invoices', order.InvoiceViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
team_router = routers.DefaultRouter()
@@ -77,7 +79,7 @@ event_router.register(r'questions', item.QuestionViewSet)
event_router.register(r'discounts', discount.DiscountViewSet)
event_router.register(r'quotas', item.QuotaViewSet)
event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.OrderViewSet)
event_router.register(r'orders', order.EventOrderViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
+14 -1
View File
@@ -164,8 +164,21 @@ class CheckinListViewSet(viewsets.ModelViewSet):
secret=serializer.validated_data['raw_barcode']
).first()
clist = self.get_object()
if serializer.validated_data.get('nonce'):
if kwargs.get('position'):
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
else:
prev = clist.checkins.filter(
nonce=serializer.validated_data['nonce'],
raw_barcode=serializer.validated_data['raw_barcode'],
).first()
if prev:
# Ignore because nonce is already handled
return Response(serializer.data, status=201)
c = serializer.save(
list=self.get_object(),
list=clist,
successful=False,
forced=True,
force_sent=True,
-1
View File
@@ -166,7 +166,6 @@ class InitializeView(APIView):
device.software_brand = serializer.validated_data.get('software_brand')
device.software_version = serializer.validated_data.get('software_version')
device.info = serializer.validated_data.get('info')
print(serializer.validated_data, request.data)
device.rsa_pubkey = serializer.validated_data.get('rsa_pubkey')
device.api_token = generate_api_token()
device.save()
+1
View File
@@ -415,6 +415,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
'subeventitem_set',
'subeventitemvariation_set',
'meta_values',
'meta_values__property',
Prefetch(
'seat_category_mappings',
to_attr='_seat_category_mappings',
+71 -25
View File
@@ -44,6 +44,7 @@ from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError,
)
from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
@@ -185,7 +186,7 @@ with scopes_disabled():
)
class OrderViewSet(viewsets.ModelViewSet):
class OrderViewSetMixin:
serializer_class = OrderSerializer
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
@@ -193,19 +194,12 @@ class OrderViewSet(viewsets.ModelViewSet):
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
filterset_class = OrderFilter
lookup_field = 'code'
permission = 'can_view_orders'
write_permission = 'can_change_orders'
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
ctx['exclude'] = self.request.query_params.getlist('exclude')
ctx['include'] = self.request.query_params.getlist('include')
return ctx
def get_base_queryset(self):
raise NotImplementedError()
def get_queryset(self):
qs = self.request.event.orders
qs = self.get_base_queryset()
if 'fees' not in self.request.GET.getlist('exclude'):
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
fqs = OrderFee.all
@@ -227,11 +221,12 @@ class OrderViewSet(viewsets.ModelViewSet):
opq = OrderPosition.all
else:
opq = OrderPosition.objects
if request.query_params.get('pdf_data', 'false') == 'true':
if request.query_params.get('pdf_data', 'false') == 'true' and getattr(request, 'event', None):
prefetch_related_objects([request.organizer], 'meta_properties')
prefetch_related_objects(
[request.event],
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'), to_attr='meta_values_cached'),
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'),
'questions',
'item_meta_properties',
)
@@ -266,13 +261,12 @@ class OrderViewSet(viewsets.ModelViewSet):
)
)
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
for receiver, response in responses:
prov = response(self.request.event)
if prov.identifier == identifier:
return prov
raise NotFound('Unknown output provider.')
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['exclude'] = self.request.query_params.getlist('exclude')
ctx['include'] = self.request.query_params.getlist('include')
ctx['pdf_data'] = False
return ctx
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
def list(self, request, **kwargs):
@@ -289,6 +283,45 @@ class OrderViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, headers={'X-Page-Generated': date})
class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
def get_base_queryset(self):
perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
if isinstance(self.request.auth, (TeamAPIToken, Device)):
return Order.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.auth.get_events_with_permission(perm)
)
elif self.request.user.is_authenticated:
return Order.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.user.get_events_with_permission(perm)
)
else:
raise PermissionDenied()
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
permission = 'can_view_orders'
write_permission = 'can_change_orders'
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
return ctx
def get_base_queryset(self):
return self.request.event.orders
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
for receiver, response in responses:
prov = response(self.request.event)
if prov.identifier == identifier:
return prov
raise NotFound('Unknown output provider.')
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)
@@ -1782,11 +1815,24 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
write_permission = 'can_change_orders'
def get_queryset(self):
return self.request.event.invoices.prefetch_related('lines').select_related('order', 'refers').annotate(
perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
if getattr(self.request, 'event', None):
qs = self.request.event.invoices
elif isinstance(self.request.auth, (TeamAPIToken, Device)):
qs = Invoice.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.auth.get_events_with_permission(perm)
)
elif self.request.user.is_authenticated:
qs = Invoice.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.user.get_events_with_permission(perm)
)
return qs.prefetch_related('lines').select_related('order', 'refers').annotate(
nr=Concat('prefix', 'invoice_no')
)
@action(detail=True, )
@action(detail=True)
def download(self, request, **kwargs):
invoice = self.get_object()
@@ -1805,7 +1851,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
return resp
@action(detail=True, methods=['POST'])
def regenerate(self, request, **kwarts):
def regenerate(self, request, **kwargs):
inv = self.get_object()
if inv.canceled:
raise ValidationError('The invoice has already been canceled.')
@@ -1815,7 +1861,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
raise PermissionDenied('The invoice file is no longer stored on the server.')
elif inv.sent_to_organizer:
raise PermissionDenied('The invoice file has already been exported.')
elif now().astimezone(self.request.event.timezone).date() - inv.date > datetime.timedelta(days=1):
elif now().astimezone(inv.event.timezone).date() - inv.date > datetime.timedelta(days=1):
raise PermissionDenied('The invoice file is too old to be regenerated.')
else:
inv = regenerate_invoice(inv)
@@ -1830,7 +1876,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
return Response(status=204)
@action(detail=True, methods=['POST'])
def reissue(self, request, **kwarts):
def reissue(self, request, **kwargs):
inv = self.get_object()
if inv.canceled:
raise ValidationError('The invoice has already been canceled.')
+27
View File
@@ -202,6 +202,21 @@ class ParametrizedWaitingListEntryWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedCustomerWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
customer = logentry.content_object
if not customer:
return None
return {
'notification_id': logentry.pk,
'organizer': customer.organizer.slug,
'customer': customer.identifier,
'action': logentry.action_type,
}
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
def register_default_webhook_events(sender, **kwargs):
return (
@@ -350,6 +365,18 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.orders.waitinglist.voucher_assigned',
_('Waiting list entry received voucher'),
),
ParametrizedCustomerWebhookEvent(
'pretix.customer.created',
_('Customer account created'),
),
ParametrizedCustomerWebhookEvent(
'pretix.customer.changed',
_('Customer account changed'),
),
ParametrizedCustomerWebhookEvent(
'pretix.customer.anonymized',
_('Customer account anonymized'),
),
)
+5 -5
View File
@@ -62,27 +62,27 @@ class NamespacedCache:
prefix = int(time.time())
self.cache.set(self.prefixkey, prefix)
def set(self, key: str, value: str, timeout: int=300):
def set(self, key: str, value: any, timeout: int=300):
return self.cache.set(self._prefix_key(key), value, timeout)
def get(self, key: str) -> str:
def get(self, key: str) -> any:
return self.cache.get(self._prefix_key(key, known_prefix=self._last_prefix))
def get_or_set(self, key: str, default: Callable, timeout=300) -> str:
def get_or_set(self, key: str, default: Callable, timeout=300) -> any:
return self.cache.get_or_set(
self._prefix_key(key, known_prefix=self._last_prefix),
default=default,
timeout=timeout
)
def get_many(self, keys: List[str]) -> Dict[str, str]:
def get_many(self, keys: List[str]) -> Dict[str, any]:
values = self.cache.get_many([self._prefix_key(key) for key in keys])
newvalues = {}
for k, v in values.items():
newvalues[self._strip_prefix(k)] = v
return newvalues
def set_many(self, values: Dict[str, str], timeout=300):
def set_many(self, values: Dict[str, any], timeout=300):
newvalues = {}
for k, v in values.items():
newvalues[self._prefix_key(k)] = v
+5 -2
View File
@@ -134,8 +134,11 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def compile_markdown(self, plaintext):
return markdown_compile_email(plaintext)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
body_md = markdown_compile_email(plain_body)
body_md = self.compile_markdown(plain_body)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
@@ -153,7 +156,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
if plain_signature:
signature_md = plain_signature.replace('\n', '<br>\n')
signature_md = markdown_compile_email(signature_md)
signature_md = self.compile_markdown(signature_md)
htmlctx['signature'] = signature_md
if order:
+4
View File
@@ -549,7 +549,9 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('End date'))
headers += [
_('Product'),
_('Product ID'),
_('Variation'),
_('Variation ID'),
_('Price'),
_('Tax rate'),
_('Tax rule'),
@@ -656,7 +658,9 @@ class OrderListExporter(MultiSheetListExporter):
row.append('')
row += [
str(op.item),
str(op.item_id),
str(op.variation) if op.variation else '',
str(op.variation_id) if op.variation_id else '',
op.price,
op.tax_rate,
str(op.tax_rule) if op.tax_rule else '',
+2
View File
@@ -271,6 +271,8 @@ class SecurityMiddleware(MiddlewareMixin):
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment")
):
h['script-src'].append('https://pay.google.com')
h['frame-src'].append('https://pay.google.com')
h['connect-src'].append('https://google.com/pay')
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:
@@ -0,0 +1,34 @@
# Generated by Django 4.2.4 on 2023-08-28 12:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0244_mediumkeyset"),
]
operations = [
migrations.AddField(
model_name="discount",
name="benefit_apply_to_addons",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="discount",
name="benefit_ignore_voucher_discounted",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="discount",
name="benefit_limit_products",
field=models.ManyToManyField(
related_name="benefit_discounts", to="pretixbase.item"
),
),
migrations.AddField(
model_name="discount",
name="benefit_same_products",
field=models.BooleanField(default=True),
),
]
+1 -1
View File
@@ -97,7 +97,7 @@ def _transactions_mark_order_dirty(order_id, using=None):
if getattr(dirty_transactions, 'order_ids', None) is None:
dirty_transactions.order_ids = set()
if _check_for_dirty_orders not in [func for savepoint_id, func in conn.run_on_commit]:
if _check_for_dirty_orders not in [func for (savepoint_id, func, *__) in conn.run_on_commit]:
transaction.on_commit(_check_for_dirty_orders, using)
dirty_transactions.order_ids.clear() # This is necessary to clean up after old threads with rollbacked transactions
+74 -23
View File
@@ -99,7 +99,7 @@ class Discount(LoggedModel):
)
condition_apply_to_addons = models.BooleanField(
default=True,
verbose_name=_("Apply to add-on products"),
verbose_name=_("Count add-on products"),
help_text=_("Discounts never apply to bundled products"),
)
condition_ignore_voucher_discounted = models.BooleanField(
@@ -107,7 +107,7 @@ class Discount(LoggedModel):
verbose_name=_("Ignore products discounted by a voucher"),
help_text=_("If this option is checked, products that already received a discount through a voucher will not "
"be considered for this discount. However, products that use a voucher only to e.g. unlock a "
"hidden product or gain access to sold-out quota will still receive the discount."),
"hidden product or gain access to sold-out quota will still be considered."),
)
condition_min_count = models.PositiveIntegerField(
verbose_name=_('Minimum number of matching products'),
@@ -120,6 +120,19 @@ class Discount(LoggedModel):
default=Decimal('0.00'),
)
benefit_same_products = models.BooleanField(
default=True,
verbose_name=_("Apply discount to same set of products"),
help_text=_("By default, the discount is applied across the same selection of products than the condition for "
"the discount given above. If you want, you can however also select a different selection of "
"products.")
)
benefit_limit_products = models.ManyToManyField(
'Item',
verbose_name=_("Apply discount to specific products"),
related_name='benefit_discounts',
blank=True
)
benefit_discount_matching_percent = models.DecimalField(
verbose_name=_('Percentual discount on matching products'),
decimal_places=2,
@@ -139,6 +152,18 @@ class Discount(LoggedModel):
blank=True,
validators=[MinValueValidator(1)],
)
benefit_apply_to_addons = models.BooleanField(
default=True,
verbose_name=_("Apply to add-on products"),
help_text=_("Discounts never apply to bundled products"),
)
benefit_ignore_voucher_discounted = models.BooleanField(
default=False,
verbose_name=_("Ignore products discounted by a voucher"),
help_text=_("If this option is checked, products that already received a discount through a voucher will not "
"be discounted. However, products that use a voucher only to e.g. unlock a hidden product or gain "
"access to sold-out quota will still receive the discount."),
)
# more feature ideas:
# - max_usages_per_order
@@ -187,6 +212,14 @@ class Discount(LoggedModel):
'on a minimum value.')
)
if data.get('subevent_mode') == cls.SUBEVENT_MODE_DISTINCT and not data.get('benefit_same_products'):
raise ValidationError(
{'benefit_same_products': [
_('You cannot apply the discount to a different set of products if the discount is only valid '
'for bookings of different dates.')
]}
)
def allow_delete(self):
return not self.orderposition_set.exists()
@@ -197,6 +230,7 @@ class Discount(LoggedModel):
'condition_min_value': self.condition_min_value,
'benefit_only_apply_to_cheapest_n_matches': self.benefit_only_apply_to_cheapest_n_matches,
'subevent_mode': self.subevent_mode,
'benefit_same_products': self.benefit_same_products,
})
def is_available_by_time(self, now_dt=None) -> bool:
@@ -207,14 +241,14 @@ class Discount(LoggedModel):
return False
return True
def _apply_min_value(self, positions, idx_group, result):
if self.condition_min_value and sum(positions[idx][2] for idx in idx_group) < self.condition_min_value:
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result):
if self.condition_min_value and sum(positions[idx][2] for idx in condition_idx_group) < self.condition_min_value:
return
if self.condition_min_count or self.benefit_only_apply_to_cheapest_n_matches:
raise ValueError('Validation invariant violated.')
for idx in idx_group:
for idx in benefit_idx_group:
previous_price = positions[idx][2]
new_price = round_decimal(
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
@@ -222,8 +256,8 @@ class Discount(LoggedModel):
)
result[idx] = new_price
def _apply_min_count(self, positions, idx_group, result):
if len(idx_group) < self.condition_min_count:
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result):
if len(condition_idx_group) < self.condition_min_count:
return
if not self.condition_min_count or self.condition_min_value:
@@ -233,15 +267,17 @@ class Discount(LoggedModel):
if not self.condition_min_count:
raise ValueError('Validation invariant violated.')
idx_group = sorted(idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
# want to match multiples of 3
consume_idx = idx_group[:len(idx_group) // self.condition_min_count * self.condition_min_count]
benefit_idx = idx_group[:len(idx_group) // self.condition_min_count * self.benefit_only_apply_to_cheapest_n_matches]
n_groups = min(len(condition_idx_group) // self.condition_min_count, len(benefit_idx_group))
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
else:
consume_idx = idx_group
benefit_idx = idx_group
consume_idx = condition_idx_group
benefit_idx = benefit_idx_group
for idx in benefit_idx:
previous_price = positions[idx][2]
@@ -276,7 +312,7 @@ class Discount(LoggedModel):
limit_products = {p.pk for p in self.condition_limit_products.all()}
# First, filter out everything not even covered by our product scope
initial_candidates = [
condition_candidates = [
idx
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount) in positions.items()
if (
@@ -286,11 +322,25 @@ class Discount(LoggedModel):
)
]
if self.benefit_same_products:
benefit_candidates = list(condition_candidates)
else:
benefit_products = {p.pk for p in self.benefit_limit_products.all()}
benefit_candidates = [
idx
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount) in positions.items()
if (
item_id in benefit_products and
(self.benefit_apply_to_addons or not is_addon_to) and
(not self.benefit_ignore_voucher_discounted or voucher_discount is None or voucher_discount == Decimal('0.00'))
)
]
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
if self.condition_min_count:
self._apply_min_count(positions, initial_candidates, result)
self._apply_min_count(positions, condition_candidates, benefit_candidates, result)
else:
self._apply_min_value(positions, initial_candidates, result)
self._apply_min_value(positions, condition_candidates, benefit_candidates, result)
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
def key(idx):
@@ -299,17 +349,18 @@ class Discount(LoggedModel):
# Build groups of candidates with the same subevent, then apply our regular algorithm
# to each group
_groups = groupby(sorted(initial_candidates, key=key), key=key)
candidate_groups = [list(g) for k, g in _groups]
_groups = groupby(sorted(condition_candidates, key=key), key=key)
candidate_groups = [(k, list(g)) for k, g in _groups]
for g in candidate_groups:
for subevent_id, g in candidate_groups:
benefit_g = [idx for idx in benefit_candidates if positions[idx][1] == subevent_id]
if self.condition_min_count:
self._apply_min_count(positions, g, result)
self._apply_min_count(positions, g, benefit_g, result)
else:
self._apply_min_value(positions, g, result)
self._apply_min_value(positions, g, benefit_g, result)
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
if self.condition_min_value:
if self.condition_min_value or not self.benefit_same_products:
raise ValueError('Validation invariant violated.')
# Build optimal groups of candidates with distinct subevents, then apply our regular algorithm
@@ -336,7 +387,7 @@ class Discount(LoggedModel):
candidates = []
cardinality = None
for se, l in subevent_to_idx.items():
l = [ll for ll in l if ll in initial_candidates and ll not in current_group]
l = [ll for ll in l if ll in condition_candidates and ll not in current_group]
if cardinality and len(l) != cardinality:
continue
if se not in {positions[idx][1] for idx in current_group}:
@@ -373,5 +424,5 @@ class Discount(LoggedModel):
break
for g in candidate_groups:
self._apply_min_count(positions, g, result)
self._apply_min_count(positions, g, g, result)
return result
+6 -2
View File
@@ -907,14 +907,18 @@ class Event(EventMixin, LoggedModel):
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
for d in Discount.objects.filter(event=other).prefetch_related('condition_limit_products'):
items = list(d.condition_limit_products.all())
c_items = list(d.condition_limit_products.all())
b_items = list(d.benefit_limit_products.all())
d.pk = None
d.event = self
d.save(force_insert=True)
d.log_action('pretix.object.cloned')
for i in items:
for i in c_items:
if i.pk in item_map:
d.condition_limit_products.add(item_map[i.pk])
for i in b_items:
if i.pk in item_map:
d.benefit_limit_products.add(item_map[i.pk])
question_map = {}
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
+8 -3
View File
@@ -43,6 +43,7 @@ from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import dateutil.parser
import django_redis
from dateutil.tz import datetime_exists
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -57,7 +58,6 @@ from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country
from django_redis import get_redis_connection
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
@@ -1910,8 +1910,13 @@ class Quota(LoggedModel):
def rebuild_cache(self, now_dt=None):
if settings.HAS_REDIS:
rc = get_redis_connection("redis")
rc.hdel(f'quotas:{self.event_id}:availabilitycache', str(self.pk))
rc = django_redis.get_redis_connection("redis")
p = rc.pipeline()
p.hdel(f'quotas:{self.event_id}:availabilitycache', str(self.pk))
p.hdel(f'quotas:{self.event_id}:availabilitycache:nocw', str(self.pk))
p.hdel(f'quotas:{self.event_id}:availabilitycache:igcl', str(self.pk))
p.hdel(f'quotas:{self.event_id}:availabilitycache:nocw:igcl', str(self.pk))
p.execute()
self.availability(now_dt=now_dt)
def availability(
+1 -3
View File
@@ -88,9 +88,7 @@ class LogEntry(models.Model):
class Meta:
ordering = ('-datetime', '-id')
index_together = [
['datetime', 'id']
]
indexes = [models.Index(fields=["datetime", "id"])]
def display(self):
from ..signals import logentry_display
+4 -1
View File
@@ -121,7 +121,10 @@ class ReusableMedium(LoggedModel):
class Meta:
unique_together = (("identifier", "type", "organizer"),)
index_together = (("identifier", "type", "organizer"), ("updated", "id"))
indexes = [
models.Index(fields=("identifier", "type", "organizer")),
models.Index(fields=("updated", "id")),
]
ordering = "identifier", "type", "organizer"
+6 -6
View File
@@ -270,9 +270,9 @@ class Order(LockModel, LoggedModel):
verbose_name = _("Order")
verbose_name_plural = _("Orders")
ordering = ("-datetime", "-pk")
index_together = [
["datetime", "id"],
["last_modified", "id"],
indexes = [
models.Index(fields=["datetime", "id"]),
models.Index(fields=["last_modified", "id"]),
]
def __str__(self):
@@ -1676,7 +1676,7 @@ class OrderPayment(models.Model):
"""
Marks the order as failed and sets info to ``info``, but only if the order is in ``created`` or ``pending``
state. This is equivalent to setting ``state`` to ``OrderPayment.PAYMENT_STATE_FAILED`` and logging a failure,
but it adds strong database logging since we do not want to report a failure for an order that has just
but it adds strong database locking since we do not want to report a failure for an order that has just
been marked as paid.
:param send_mail: Whether an email should be sent to the user about this event (default: ``True``).
"""
@@ -2756,8 +2756,8 @@ class Transaction(models.Model):
class Meta:
ordering = 'datetime', 'pk'
index_together = [
['datetime', 'id']
indexes = [
models.Index(fields=['datetime', 'id'])
]
def save(self, *args, **kwargs):
+11 -4
View File
@@ -340,10 +340,17 @@ class TaxRule(LoggedModel):
rules = self._custom_rules
if invoice_address:
for r in rules:
if r['country'] == 'EU' and not is_eu_country(invoice_address.country):
continue
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
continue
if r['country'] == 'ZZ': # Rule: Any country
pass
elif r['country'] == 'EU': # Rule: Any EU country
if not is_eu_country(invoice_address.country):
continue
elif '-' in r['country']: # Rule: Specific country and state
if r['country'] != str(invoice_address.country) + '-' + str(invoice_address.state):
continue
else: # Rule: Specific country
if r['country'] != str(invoice_address.country):
continue
if r['address_type'] == 'individual' and invoice_address.is_business:
continue
if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business:
+1 -1
View File
@@ -805,7 +805,7 @@ class QuestionColumn(ImportColumn):
return self.q.clean_answer(value)
def assign(self, value, order, position, invoice_address, **kwargs):
if value:
if value is not None:
if not hasattr(order, '_answers'):
order._answers = []
if isinstance(value, QuestionOption):
+6 -3
View File
@@ -108,7 +108,10 @@ DEFAULT_VARIABLES = OrderedDict((
("positionid", {
"label": _("Order position number"),
"editor_sample": "1",
"evaluate": lambda orderposition, order, event: str(orderposition.positionid)
"evaluate": lambda orderposition, order, event: str(orderposition.positionid),
# There is no performance gain in using evaluate_bulk here, but we want to make sure it is used somewhere
# in core to make sure we notice if the implementation of the API breaks.
"evaluate_bulk": lambda orderpositions: [str(p.positionid) for p in orderpositions],
}),
("order_positionid", {
"label": _("Order code and position number"),
@@ -699,10 +702,10 @@ def get_seat(op: OrderPosition):
def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int)
addons = (
addons = [p for p in (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
)
) if not p.canceled]
for pos in addons:
itemcount[pos.item, pos.variation] += 1
+20 -8
View File
@@ -1078,6 +1078,7 @@ class CartManager:
quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt)
err = None
new_cart_positions = []
deleted_positions = set()
err = err or self._check_min_max_per_product()
@@ -1089,7 +1090,10 @@ class CartManager:
if op.position.expires > self.now_dt:
for q in op.position.quotas:
quotas_ok[q] += 1
op.position.addons.all().delete()
addons = op.position.addons.all()
deleted_positions |= {a.pk for a in addons}
addons.delete()
deleted_positions.add(op.position.pk)
op.position.delete()
elif isinstance(op, (self.AddOperation, self.ExtendOperation)):
@@ -1239,20 +1243,28 @@ class CartManager:
if op.seat and not op.seat.is_available(ignore_cart=op.position, sales_channel=self._sales_channel,
ignore_voucher_id=op.position.voucher_id):
err = err or error_messages['seat_unavailable']
op.position.addons.all().delete()
addons = op.position.addons.all()
deleted_positions |= {a.pk for a in addons}
deleted_positions.add(op.position.pk)
addons.delete()
op.position.delete()
elif available_count == 1:
op.position.expires = self._expiry
op.position.listed_price = op.listed_price
op.position.price_after_voucher = op.price_after_voucher
# op.position.price will be updated by recompute_final_prices_and_taxes()
try:
op.position.save(force_update=True, update_fields=['expires', 'listed_price', 'price_after_voucher'])
except DatabaseError:
# Best effort... The position might have been deleted in the meantime!
pass
if op.position.pk not in deleted_positions:
try:
op.position.save(force_update=True, update_fields=['expires', 'listed_price', 'price_after_voucher'])
except DatabaseError:
# Best effort... The position might have been deleted in the meantime!
pass
elif available_count == 0:
op.position.addons.all().delete()
addons = op.position.addons.all()
deleted_positions |= {a.pk for a in addons}
deleted_positions.add(op.position.pk)
addons.delete()
op.position.delete()
else:
raise AssertionError("ExtendOperation cannot affect more than one item")
+1 -1
View File
@@ -886,7 +886,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if isinstance(auth, Device):
device = auth
last_cis = list(op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce'))
last_cis = list(op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce', 'position_id'))
entry_allowed = (
type == Checkin.TYPE_EXIT or
clist.allow_multiple_entries or
+5
View File
@@ -2476,6 +2476,11 @@ class OrderChangeManager:
split_order.status = Order.STATUS_PAID
else:
split_order.status = Order.STATUS_PENDING
if self.order.status == Order.STATUS_PAID:
split_order.set_expires(
now(),
list(set(p.subevent_id for p in split_positions))
)
split_order.save()
if offset_amount > Decimal('0.00'):
+1 -1
View File
@@ -171,7 +171,7 @@ def apply_discounts(event: Event, sales_channel: str,
Q(available_until__isnull=True) | Q(available_until__gte=now()),
sales_channels__contains=sales_channel,
active=True,
).prefetch_related('condition_limit_products').order_by('position', 'pk')
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs:
result = discount.apply({
idx: (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
+15 -12
View File
@@ -24,13 +24,13 @@ import time
from collections import Counter, defaultdict
from itertools import zip_longest
import django_redis
from django.conf import settings
from django.db import models
from django.db.models import (
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
)
from django.utils.timezone import now
from django_redis import get_redis_connection
from pretix.base.models import (
CartPosition, Checkin, Order, OrderPosition, Quota, Voucher,
@@ -102,6 +102,12 @@ class QuotaAvailability:
self.count_waitinglist = defaultdict(int)
self.count_cart = defaultdict(int)
self._cache_key_suffix = ""
if not self._count_waitinglist:
self._cache_key_suffix += ":nocw"
if self._ignore_closed:
self._cache_key_suffix += ":igcl"
self.sizes = {}
def queue(self, *quota):
@@ -121,17 +127,14 @@ class QuotaAvailability:
if self._full_results:
raise ValueError("You cannot combine full_results and allow_cache.")
elif not self._count_waitinglist:
raise ValueError("If you set allow_cache, you need to set count_waitinglist.")
elif settings.HAS_REDIS:
rc = get_redis_connection("redis")
rc = django_redis.get_redis_connection("redis")
quotas_by_event = defaultdict(list)
for q in [_q for _q in self._queue if _q.id in quota_ids_set]:
quotas_by_event[q.event_id].append(q)
for eventid, evquotas in quotas_by_event.items():
d = rc.hmget(f'quotas:{eventid}:availabilitycache', [str(q.pk) for q in evquotas])
d = rc.hmget(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', [str(q.pk) for q in evquotas])
for redisval, q in zip(d, evquotas):
if redisval is not None:
data = [rv for rv in redisval.decode().split(',')]
@@ -164,12 +167,12 @@ class QuotaAvailability:
if not settings.HAS_REDIS or not quotas:
return
rc = get_redis_connection("redis")
rc = django_redis.get_redis_connection("redis")
# We write the computed availability to redis in a per-event hash as
#
# quota_id -> (availability_state, availability_number, timestamp).
#
# We store this in a hash instead of inidividual values to avoid making two many redis requests
# We store this in a hash instead of individual values to avoid making too many redis requests
# which would introduce latency.
# The individual entries in the hash are "valid" for 120 seconds. This means in a typical peak scenario with
@@ -179,16 +182,16 @@ class QuotaAvailability:
# these quotas. We choose 10 seconds since that should be well above the duration of a write.
lock_name = '_'.join([str(p) for p in sorted([q.pk for q in quotas])])
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}'):
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}'):
return
rc.setex(f'quotas:availabilitycachewrite:{lock_name}', '1', 10)
rc.setex(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}', '1', 10)
update = defaultdict(list)
for q in quotas:
update[q.event_id].append(q)
for eventid, quotas in update.items():
rc.hmset(f'quotas:{eventid}:availabilitycache', {
rc.hmset(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', {
str(q.id): ",".join(
[str(i) for i in self.results[q]] +
[str(int(time.time()))]
@@ -197,7 +200,7 @@ class QuotaAvailability:
# To make sure old events do not fill up our redis instance, we set an expiry on the cache. However, we set it
# on 7 days even though we mostly ignore values older than 2 monites. The reasoning is that we have some places
# where we set allow_cache_stale and use the old entries anyways to save on performance.
rc.expire(f'quotas:{eventid}:availabilitycache', 3600 * 24 * 7)
rc.expire(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', 3600 * 24 * 7)
# We used to also delete item_quota_cache:* from the event cache here, but as the cache
# gets more complex, this does not seem worth it. The cache is only present for up to
+19 -4
View File
@@ -22,13 +22,15 @@
import sys
from datetime import timedelta
from django.db.models import Exists, F, OuterRef, Q, Sum
from django.db.models import (
Exists, F, OuterRef, Prefetch, Q, Sum, prefetch_related_objects,
)
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import (
Event, SeatCategoryMapping, User, WaitingListEntry,
Event, EventMetaValue, SeatCategoryMapping, User, WaitingListEntry,
)
from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.tasks import EventTask
@@ -59,8 +61,21 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
seats_available[(m.product_id, m.subevent_id)] = num_free_seets_for_product - num_valid_vouchers_for_product
qs = WaitingListEntry.objects.filter(
event=event, voucher__isnull=True
prefetch_related_objects(
[event.organizer],
'meta_properties'
)
prefetch_related_objects(
[event],
Prefetch(
'meta_values',
EventMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'
)
)
qs = event.waitinglistentries.filter(
voucher__isnull=True
).select_related('item', 'variation', 'subevent').prefetch_related(
'item__quotas', 'variation__quotas'
).order_by('-priority', 'created')
+2
View File
@@ -210,6 +210,8 @@ def slow_delete(qs, batch_size=1000, sleep_time=.5, progress_callback=None, prog
break
if total_deleted >= 0.8 * batch_size:
time.sleep(sleep_time)
if progress_callback and progress_total:
progress_callback((progress_offset + total_deleted) / progress_total)
return total_deleted
+5 -1
View File
@@ -683,12 +683,16 @@ dictionaries as values that contain keys like in the following example::
"product": {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item)
"evaluate": lambda orderposition, order, event: str(orderposition.item),
"evaluate_bulk": lambda orderpositions: [str(op.item) for op in orderpositions],
}
}
The ``evaluate`` member will be called with the order position, order and event as arguments. The event might
also be a subevent, if applicable.
The ``evaluate_bulk`` member is optional but can significantly improve performance in some situations because you
can perform database fetches in bulk instead of single queries for every position.
"""
+3 -3
View File
@@ -292,7 +292,7 @@ class LinkifyAndCleanExtension(Extension):
)
def markdown_compile_email(source):
def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes=ALLOWED_ATTRIBUTES):
linker = bleach.Linker(
url_re=URL_RE,
email_re=EMAIL_RE,
@@ -306,8 +306,8 @@ def markdown_compile_email(source):
EmailNl2BrExtension(),
LinkifyAndCleanExtension(
linker,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
tags=allowed_tags,
attributes=allowed_attributes,
protocols=ALLOWED_PROTOCOLS,
strip=False,
)
+10 -1
View File
@@ -50,11 +50,16 @@ class DiscountForm(I18nModelForm):
'condition_ignore_voucher_discounted',
'benefit_discount_matching_percent',
'benefit_only_apply_to_cheapest_n_matches',
'benefit_same_products',
'benefit_limit_products',
'benefit_apply_to_addons',
'benefit_ignore_voucher_discounted',
]
field_classes = {
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
'condition_limit_products': ItemMultipleChoiceField,
'benefit_limit_products': ItemMultipleChoiceField,
}
widgets = {
'subevent_mode': forms.RadioSelect,
@@ -64,11 +69,14 @@ class DiscountForm(I18nModelForm):
'data-inverse-dependency': '<[name$=all_products]',
'class': 'scrolling-multiple-choice',
}),
'benefit_limit_products': forms.CheckboxSelectMultiple(attrs={
'class': 'scrolling-multiple-choice',
}),
'benefit_only_apply_to_cheapest_n_matches': forms.NumberInput(
attrs={
'data-display-dependency': '#id_condition_min_count',
}
)
),
}
def __init__(self, *args, **kwargs):
@@ -85,6 +93,7 @@ class DiscountForm(I18nModelForm):
widget=forms.CheckboxSelectMultiple,
)
self.fields['condition_limit_products'].queryset = self.event.items.all()
self.fields['benefit_limit_products'].queryset = self.event.items.all()
self.fields['condition_min_count'].required = False
self.fields['condition_min_count'].widget.is_required = False
self.fields['condition_min_value'].required = False
+15 -2
View File
@@ -38,6 +38,7 @@ from decimal import Decimal
from urllib.parse import urlencode, urlparse
from zoneinfo import ZoneInfo
import pycountry
from django import forms
from django.conf import settings
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
@@ -65,7 +66,8 @@ from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
)
from pretix.base.validators import multimail_validate
from pretix.control.forms import (
@@ -1428,9 +1430,20 @@ class CountriesAndEU(CachedCountries):
cache_subkey = 'with_any_or_eu'
class CountriesAndEUAndStates(CountriesAndEU):
def __iter__(self):
for country_code, country_name in super().__iter__():
yield country_code, country_name
if country_code in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[country_code]
yield from sorted(((state.code, country_name + " - " + state.name)
for state in pycountry.subdivisions.get(country_code=country_code)
if state.type in types), key=lambda s: s[1])
class TaxRuleLineForm(I18nForm):
country = LazyTypedChoiceField(
choices=CountriesAndEU(),
choices=CountriesAndEUAndStates(),
required=False
)
address_type = forms.ChoiceField(
+4 -4
View File
@@ -1732,8 +1732,8 @@ class CheckinListAttendeeFilterForm(FilterForm):
'-timestamp': (OrderBy(F('last_entry'), nulls_last=True, descending=True), '-order__code'),
'item': ('item__name', 'variation__value', 'order__code'),
'-item': ('-item__name', '-variation__value', '-order__code'),
'seat': ('seat__sorting_rank', 'seat__guid'),
'-seat': ('-seat__sorting_rank', '-seat__guid'),
'seat': ('seat__sorting_rank', 'seat__seat_guid'),
'-seat': ('-seat__sorting_rank', '-seat__seat_guid'),
'date': ('subevent__date_from', 'subevent__id', 'order__code'),
'-date': ('-subevent__date_from', 'subevent__id', '-order__code'),
'name': {'_order': F('display_name').asc(nulls_first=True),
@@ -1940,7 +1940,7 @@ class VoucherFilterForm(FilterForm):
'item__category__position',
'item__category',
'item__position',
'item__variation__position',
'variation__position',
'quota__name',
),
'subevent': 'subevent__date_from',
@@ -1950,7 +1950,7 @@ class VoucherFilterForm(FilterForm):
'-item__category__position',
'-item__category',
'-item__position',
'-item__variation__position',
'-variation__position',
'-quota__name',
)
}
+4 -2
View File
@@ -86,12 +86,14 @@ class GlobalSettingsForm(SettingsForm):
('leaflet_tiles', forms.CharField(
required=False,
label=_("Leaflet tiles URL pattern"),
help_text=_("e.g. {sample}").format(sample="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png")
help_text=_("e.g. {sample}").format(sample="https://tile.openstreetmap.org/{z}/{x}/{y}.png")
)),
('leaflet_tiles_attribution', forms.CharField(
required=False,
label=_("Leaflet tiles attribution"),
help_text=_("e.g. {sample}").format(sample='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors')
help_text=_("e.g. {sample}").format(
sample='&copy; &lt;a href=&quot;https://www.openstreetmap.org/copyright&quot;&gt;OpenStreetMap&lt;/a&gt; contributors'
)
)),
])
responses = register_global_settings.send(self)
-5
View File
@@ -461,11 +461,6 @@ class ItemCreateForm(I18nModelForm):
)
if self.cleaned_data.get('copy_from'):
for mv in self.cleaned_data['copy_from'].meta_values.all():
mv.pk = None
mv.item = instance
mv.save(force_insert=True)
for question in self.cleaned_data['copy_from'].questions.all():
question.items.add(instance)
question.log_action('pretix.event.question.changed', user=self.user, data={
+3
View File
@@ -340,6 +340,9 @@ class VoucherBulkForm(VoucherForm):
def clean_send_recipients(self):
raw = self.cleaned_data['send_recipients']
if self.cleaned_data.get('send', None) is False:
# No need to validate addresses if the section was turned off
return []
if not raw:
return []
r = raw.split('\n')
+1
View File
@@ -341,6 +341,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
'pretix.giftcards.acceptance.acceptor.removed': _('A gift card acceptor has been removed.'),
'pretix.giftcards.acceptance.issuer.removed': _('A gift card issuer has been removed or declined.'),
'pretix.giftcards.acceptance.issuer.accepted': _('A new gift card issuer has been accepted.'),
'pretix.webhook.created': _('The webhook has been created.'),
@@ -0,0 +1,27 @@
{% load i18n %}
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="png" %}{% if url %}?url={{ url|urlencode }}{% endif %}"
target="_blank" download>
{% blocktrans with filetype="PNG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="svg" %}{% if url %}?url={{ url|urlencode }}{% endif %}"
target="_blank" download>
{% blocktrans with filetype="SVG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="jpeg" %}{% if url %}?url={{ url|urlencode }}{% endif %}"
target="_blank" download>
{% blocktrans with filetype="JPG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="gif" %}{% if url %}?url={{ url|urlencode }}{% endif %}"
target="_blank" download>
{% blocktrans with filetype="GIF" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
</ul>
@@ -27,28 +27,7 @@
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-qrcode" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="png" %}" target="_blank" download>
{% blocktrans with filetype="PNG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="svg" %}" target="_blank" download>
{% blocktrans with filetype="SVG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="jpeg" %}" target="_blank" download>
{% blocktrans with filetype="JPG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="gif" %}" target="_blank" download>
{% blocktrans with filetype="GIF" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
</ul>
{% include "pretixcontrol/event/fragment_qr_dropdown.html" with url=0 %}
</div>
<div class="clearfix"></div>
</div>
@@ -337,7 +337,7 @@
<div class="alert alert-info">
{% blocktrans trimmed %}
The waiting list currently is not compatible with some advanced features of pretix such as
add-on products or product bundles.
hidden products, add-on products or product bundles.
{% endblocktrans %}
</div>
<div class="alert alert-info">
@@ -48,6 +48,12 @@
</fieldset>
<fieldset>
<legend>{% trans "Benefit" context "discount" %}</legend>
{% bootstrap_field form.benefit_same_products layout="control" %}
<div data-display-dependency="#id_benefit_same_products" data-inverse>
{% bootstrap_field form.benefit_limit_products layout="control" %}
{% bootstrap_field form.benefit_apply_to_addons layout="control" %}
{% bootstrap_field form.benefit_ignore_voucher_discounted layout="control" %}
</div>
{% bootstrap_field form.benefit_discount_matching_percent layout="control" addon_after="%" %}
{% bootstrap_field form.benefit_only_apply_to_cheapest_n_matches layout="control" %}
</fieldset>
@@ -69,7 +69,7 @@
<td></td>
<td class="text-right flip">
<strong>
{{ sums.count }}
{{ sums.sum_count }}
</strong>
</td>
<td></td>
@@ -295,6 +295,11 @@
{% bootstrap_field sform.invoice_regenerate_allowed layout="control" %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</div>
<div class="col-xs-12 col-lg-2">
<div class="panel panel-default">
@@ -307,10 +312,5 @@
</div>
</div>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -42,10 +42,18 @@
<div class="form-group">
<label class="col-md-3 control-label" for="id_url">{% trans "Voucher link" %}</label>
<div class="col-md-9">
<input type="text" name="url"
value="{% abseventurl request.event "presale:event.redeem" %}?voucher={{ voucher.code|urlencode }}{% if voucher.subevent_id %}&subevent={{ voucher.subevent_id }}{% endif %}"
class="form-control"
id="id_url" readonly>
<div class="input-group">
<input type="text" name="url"
value="{{ url }}"
class="form-control"
id="id_url" readonly>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-qrcode" aria-hidden="true"></i>
</button>
{% include "pretixcontrol/event/fragment_qr_dropdown.html" with url=url %}
</div>
</div>
</div>
</div>
{% endif %}
@@ -96,7 +96,9 @@
<tr>
<th>
{% if "can_change_vouchers" in request.eventpermset %}
<input type="checkbox" data-toggle-table />
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label">
<input type="checkbox" data-toggle-table />
</label>
{% endif %}
</th>
<th>
@@ -139,7 +141,9 @@
<tr>
<td>
{% if "can_change_vouchers" in request.eventpermset %}
<input type="checkbox" name="voucher" class="" value="{{ v.pk }}"/>
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label">
<input type="checkbox" name="voucher" class="batch-select-checkbox" value="{{ v.pk }}"/>
</label>
{% endif %}
</td>
<td>
@@ -194,9 +198,12 @@
</table>
</div>
{% if "can_change_vouchers" in request.eventpermset %}
<button type="submit" class="btn btn-default btn-save" name="action" value="delete">
{% trans "Delete selected" %}
</button>
<div class="batch-select-actions">
<button type="submit" class="btn btn-danger" name="action" value="delete">
<i class="fa fa-trash" aria-hidden="true"></i>
{% trans "Delete selected" %}
</button>
</div>
{% endif %}
</form>
{% include "pretixcontrol/pagination.html" %}
+3 -3
View File
@@ -198,12 +198,12 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
else item.check_quotas(subevent=subevent, count_waitinglist=False, _cache=quota_cache)
)
if row[1] is None:
happy += 1
happy += wlt['cnt']
elif row[1] > 0:
happy += 1
happy += min(wlt['cnt'], row[1])
for q in quotas:
if q.size is not None:
quota_cache[q.pk] = (quota_cache[q.pk][0], quota_cache[q.pk][1] - 1)
quota_cache[q.pk] = (quota_cache[q.pk][0], quota_cache[q.pk][1] - min(wlt['cnt'], row[1]))
widgets.append({
'content': None if lazy else NUM_WIDGET.format(
+11 -3
View File
@@ -40,7 +40,7 @@ from collections import OrderedDict
from decimal import Decimal
from io import BytesIO
from itertools import groupby
from urllib.parse import urlsplit
from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo
import bleach
@@ -50,6 +50,7 @@ from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.db import transaction
from django.db.models import ProtectedError
@@ -61,6 +62,7 @@ from django.http import (
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
from django.views.generic import FormView, ListView
@@ -783,8 +785,8 @@ class MailSettingsRendererPreview(MailSettingsPreview):
return ctx
def get(self, request, *args, **kwargs):
v = str(request.event.settings.mail_text_order_payment_failed)
v = format_map(v, self.placeholders('mail_text_order_payment_failed'))
v = str(request.event.settings.mail_text_order_placed)
v = format_map(v, self.placeholders('mail_text_order_placed'))
renderers = request.event.get_html_mail_renderers()
if request.GET.get('renderer') in renderers:
with rolledback_transaction():
@@ -1530,6 +1532,12 @@ class EventQRCode(EventPermissionRequiredMixin, View):
def get(self, request, *args, filetype, **kwargs):
url = build_absolute_uri(request.event, 'presale:event.index')
if "url" in request.GET:
if url_has_allowed_host_and_scheme(request.GET["url"], allowed_hosts=[urlparse(url).netloc]):
url = request.GET["url"]
else:
raise PermissionDenied("Untrusted URL")
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_M,
+29 -4
View File
@@ -1188,30 +1188,46 @@ class MetaDataEditorMixin:
@cached_property
def meta_forms(self):
if hasattr(self, 'object') and self.object:
if getattr(self, 'object', None):
val_instances = {
v.property_id: v for v in self.object.meta_values.all()
}
else:
val_instances = {}
if getattr(self, 'copy_from', None):
defaults = {
v.property_id: v.value for v in self.copy_from.meta_values.all()
}
else:
defaults = {}
formlist = []
for p in self.request.event.item_meta_properties.all():
formlist.append(self._make_meta_form(p, val_instances))
formlist.append(self._make_meta_form(p, val_instances, defaults))
return formlist
def _make_meta_form(self, p, val_instances):
def _make_meta_form(self, p, val_instances, defaults):
return self.meta_form(
prefix='prop-{}'.format(p.pk),
property=p,
instance=val_instances.get(p.pk, self.meta_model(property=p, item=self.object)),
instance=val_instances.get(
p.pk,
self.meta_model(
property=p,
item=self.object if getattr(self, 'object', None) else None,
value=defaults.get(p.pk, None)
)
),
data=(self.request.POST if self.request.method == "POST" else None)
)
def save_meta(self):
for f in self.meta_forms:
if f.cleaned_data.get('value'):
if not f.instance.item_id:
f.instance.item = self.object
f.save()
elif f.instance and f.instance.pk:
f.instance.delete()
@@ -1257,6 +1273,7 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
messages.success(self.request, _('Your changes have been saved.'))
ret = super().form_valid(form)
self.save_meta()
form.instance.log_action('pretix.event.item.added', user=self.request.user, data={
k: (form.cleaned_data.get(k).name
if isinstance(form.cleaned_data.get(k), File)
@@ -1283,6 +1300,14 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
ctx['meta_forms'] = self.meta_forms
return ctx
def post(self, request, *args, **kwargs):
self.object = None
form = self.get_form()
if form.is_valid() and all([f.is_valid() for f in self.meta_forms]):
return self.form_valid(form)
else:
return self.form_invalid(form)
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
form_class = ItemUpdateForm
+1 -1
View File
@@ -418,7 +418,7 @@ class OrderTransactions(OrderView):
'item', 'variation', 'subevent'
).order_by('datetime')
ctx['sums'] = self.order.transactions.aggregate(
count=Sum('count'),
sum_count=Sum('count'),
full_price=Sum(F('count') * F('price')),
full_tax_value=Sum(F('count') * F('tax_value')),
)
+2 -2
View File
@@ -1054,8 +1054,8 @@ class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, Organizer
limit_events_list=Subquery(
Device.limit_events.through.objects.filter(
device_id=OuterRef('pk')
).order_by('device_id', 'event_id').values('device_id').annotate(
g=GroupConcat('event_id', separator=',')
).order_by().values('device_id').annotate(
g=GroupConcat('event_id', separator=',', ordered=True)
).values('g')
)
)
+3 -3
View File
@@ -546,7 +546,7 @@ def variations_select2(request, **kwargs):
F('item__category__position').asc(nulls_first=True),
'item__category_id',
'item__position',
'item__pk'
'item__pk',
'position',
'value'
).select_related('item')
@@ -718,7 +718,7 @@ def itemvarquota_select2(request, **kwargs):
itemqs = request.event.items.prefetch_related('variations').filter(
Q(name__icontains=i18ncomp(query)) | Q(internal_name__icontains=query)
)
quotaqs = request.event.quotas.filter(quotaf).select_related('subevent')
quotaqs = request.event.quotas.filter(quotaf).select_related('subevent').order_by('-subevent__date_from', 'name')
more = False
else:
if page == 1:
@@ -727,7 +727,7 @@ def itemvarquota_select2(request, **kwargs):
)
else:
itemqs = request.event.items.none()
quotaqs = request.event.quotas.filter(name__icontains=query).select_related('subevent')
quotaqs = request.event.quotas.filter(name__icontains=query).select_related('subevent').order_by('-subevent__date_from', 'name')
total = quotaqs.count()
pagesize = 20
offset = (page - 1) * pagesize
+9
View File
@@ -34,6 +34,7 @@
# License for the specific language governing permissions and limitations under the License.
import io
from urllib.parse import urlencode
import bleach
from defusedcsv import csv
@@ -75,6 +76,7 @@ from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import format_map
from pretix.helpers.models import modelcopy
from pretix.multidomain.urlreverse import build_absolute_uri
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
@@ -315,6 +317,13 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
expires__gte=now()
).count()
ctx['redeemed_in_carts'] = redeemed_in_carts
url_params = {
'voucher': self.object.code
}
if self.object.subevent_id:
url_params['subevent'] = self.object.subevent_id
ctx['url'] = build_absolute_uri(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params)
return ctx
+14 -6
View File
@@ -66,18 +66,26 @@ class GroupConcat(Aggregate):
function = 'group_concat'
template = '%(function)s(%(field)s, "%(separator)s")'
def __init__(self, *expressions, **extra):
def __init__(self, *expressions, ordered=False, **extra):
self.ordered = ordered
if 'separator' not in extra:
# For PostgreSQL separator is an obligatory
extra.update({'separator': ','})
super().__init__(*expressions, **extra)
def as_postgresql(self, compiler, connection):
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s')",
)
if self.ordered:
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s' ORDER BY %(field)s ASC)",
)
else:
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s')",
)
class ReplicaRouter:
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
"PO-Revision-Date: 2023-07-27 11:58+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"PO-Revision-Date: 2023-08-16 22:00+0000\n"
"Last-Translator: Felix Hartnagel <felix@fhcom.de>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix/de_Informal/>\n"
"Language: de_Informal\n"
@@ -16172,7 +16172,7 @@ msgstr "Kontoeinstellungen"
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:13
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:14
msgid "Log out"
msgstr "Anmelden"
msgstr "Abmelden"
#: pretix/control/templates/pretixcontrol/base.html:245
msgid "Organizer account"
@@ -31019,7 +31019,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html:43
msgid "Please note that we still await your payment to complete the process."
msgstr ""
"Bitte beachten Sie, dass wir noch deine Zahlung erwarten, um den Prozess "
"Bitte beachte, dass wir noch deine Zahlung erwarten, um den Prozess "
"abzuschließen."
#: pretix/presale/templates/pretixpresale/event/order.html:55
File diff suppressed because it is too large Load Diff
+103 -111
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
"PO-Revision-Date: 2023-07-13 07:22+0000\n"
"Last-Translator: Martin Gross <gross@rami.io>\n"
"PO-Revision-Date: 2023-08-02 02:00+0000\n"
"Last-Translator: Patrizia Cotza <str.cotza@gmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
"Language: es\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
"X-Generator: Weblate 4.18.2\n"
#: pretix/_base_settings.py:78
msgid "English"
@@ -2435,6 +2435,8 @@ msgstr "Disponibilidad de cuotas"
msgid ""
"Download a spreadsheet of all quotas including their current availability."
msgstr ""
"Descargar un archivo Excel con todas las cuotas incluyendo su disponibilidad "
"actual."
#: pretix/base/exporters/orderlist.py:1082
#: pretix/control/templates/pretixcontrol/items/quotas.html:45
@@ -2499,6 +2501,7 @@ msgstr "Tarjeta de regalo"
#: pretix/base/exporters/orderlist.py:1132
msgid "Download a spreadsheet of all gift card transactions."
msgstr ""
"Descargar una hoja de cálculo con todas las transacciones de tarjeta regalo."
#: pretix/base/exporters/orderlist.py:1160
#: pretix/base/exporters/orderlist.py:1207
@@ -2571,6 +2574,8 @@ msgstr "Redenciones de tarjetas de regalo"
msgid ""
"Download a spreadsheet of all payments or refunds that involve gift cards."
msgstr ""
"Descargar una hoja de cálculo con todos los pagos y devoluciones que "
"contienen tarjetas regalo."
#: pretix/base/exporters/orderlist.py:1207
#: pretix/control/templates/pretixcontrol/giftcards/payment.html:16
@@ -2777,6 +2782,8 @@ msgid ""
"Due to technical reasons you cannot set inputs, that need to be masked (e.g. "
"passwords), to %(value)s."
msgstr ""
"Por razones técnicas, no puedes introducir datos que deben estar ocultos ("
"ej. contraseñas) en el campo %(value)s."
#: pretix/base/forms/auth.py:57 pretix/base/forms/auth.py:168
msgid "Keep me logged in"
@@ -2848,6 +2855,8 @@ msgid ""
"You uploaded an image in landscape orientation. Please upload an image in "
"portrait orientation."
msgstr ""
"Has cargado una imagen con formato horizontal. Por favor sube una imagen en "
"vertical."
#: pretix/base/forms/questions.py:471
msgid "Please upload an image where the width is 3/4 of the height."
@@ -2870,6 +2879,8 @@ msgid ""
"If you keep this empty, the ticket will be valid starting at the time of "
"purchase."
msgstr ""
"Si mantienes este campo vacío, la entrada será válida empezando en el "
"momento de la compra."
#: pretix/base/forms/questions.py:664 pretix/base/forms/questions.py:992
msgid "Street and Number"
@@ -2885,6 +2896,8 @@ msgid ""
"Optional, but depending on the country you reside in we might need to charge "
"you additional taxes if you do not enter it."
msgstr ""
"Opcional, pero dependiendo de tu país de residencia, es posible que haya que "
"aplicar cargos adicionales si no nos facilitas tu dirección."
#: pretix/base/forms/questions.py:1033 pretix/base/forms/questions.py:1039
msgid "If you are registered in Switzerland, you can enter your UID instead."
@@ -2895,6 +2908,8 @@ msgid ""
"Optional, but it might be required for you to claim tax benefits on your "
"invoice depending on your and the sellers country of residence."
msgstr ""
"Opcional, pero puede que sea necesario si aplican beneficios fiscales en tu "
"factura dependiendo del país de residencia del vendedor."
#: pretix/base/forms/questions.py:1129
msgid "You need to provide a company name."
@@ -2927,7 +2942,7 @@ msgstr "La contraseña actual que ingresó no es correcta."
#: pretix/base/forms/user.py:58
msgid "Please choose a password different to your current one."
msgstr ""
msgstr "Elige una contraseña diferente a la actual."
#: pretix/base/forms/user.py:63 pretix/presale/forms/customer.py:373
#: pretix/presale/forms/customer.py:442
@@ -3139,7 +3154,7 @@ msgstr "Monto"
#, python-brace-format
msgctxt "invoice"
msgid "Single price: {net_price} net / {gross_price} gross"
msgstr ""
msgstr "Precio único: {net_price} neto / {gross_price} bruto"
#: pretix/base/invoice.py:659
#, fuzzy, python-brace-format
@@ -3236,7 +3251,7 @@ msgstr "Por favor, seleccione una cuota."
#: pretix/base/media.py:61
msgid "Barcode / QR-Code"
msgstr ""
msgstr "Código de barras / Código QR"
#: pretix/base/media.py:77
#: pretix/control/templates/pretixcontrol/organizers/edit.html:237
@@ -3435,7 +3450,7 @@ msgstr "Tipo de ticket no está permitido"
#: pretix/base/models/checkin.py:351
msgid "Ticket code is ambiguous on list"
msgstr ""
msgstr "El código de la entrada es ambiguo en la lista"
#: pretix/base/models/checkin.py:352
#, fuzzy
@@ -3486,6 +3501,8 @@ msgid ""
"The identifier may only contain letters, numbers, dots, dashes, and "
"underscores. It must start and end with a letter or number."
msgstr ""
"El identificador solo puede contener letras, números, puntos, y barras "
"bajas. Tiene que empezar y terminar con una letra o un número."
#: pretix/base/models/customers.py:299 pretix/base/models/orders.py:1392
#: pretix/base/models/orders.py:2971 pretix/base/settings.py:1093
@@ -3501,7 +3518,7 @@ msgstr "Seleccione país"
#: pretix/base/models/customers.py:370
msgctxt "openidconnect"
msgid "Confidential"
msgstr ""
msgstr "Confidencial"
#: pretix/base/models/customers.py:371
#, fuzzy
@@ -3520,7 +3537,7 @@ msgstr "Código de transacción"
#: pretix/base/models/customers.py:378
msgctxt "openidconnect"
msgid "Implicit"
msgstr ""
msgstr "Implícito"
#: pretix/base/models/customers.py:382
msgid "OpenID Connect access (required)"
@@ -3572,7 +3589,7 @@ msgstr "Este identificador ya se utiliza para una pregunta diferente."
#: pretix/control/templates/pretixcontrol/organizers/gates.html:16
#: pretix/plugins/checkinlists/exporters.py:671
msgid "Gate"
msgstr ""
msgstr "Puerta"
#: pretix/base/models/devices.py:132
#: pretix/control/templates/pretixcontrol/organizers/devices.html:83
@@ -3964,7 +3981,7 @@ msgstr "Parametrizaciones adicionales"
#: pretix/base/models/exports.py:61 pretix/base/models/exports.py:66
#: pretix/base/models/exports.py:71
msgid "You can specify multiple recipients separated by commas."
msgstr ""
msgstr "Puedes especificar múltiples destinatarios separados por comas."
#: pretix/base/models/exports.py:64
#, fuzzy
@@ -4006,6 +4023,7 @@ msgstr "Hora de inicio del evento"
#: pretix/base/models/exports.py:86
msgid "The actual start time might be delayed depending on system load."
msgstr ""
"La hora de inicio real puede atrasarse dependiendo de la carga del sistema."
#: pretix/base/models/fields.py:33
msgid "No value can contain the delimiter character."
@@ -4417,11 +4435,11 @@ msgstr "minutos"
#: pretix/base/models/items.py:626
msgid "Hours"
msgstr ""
msgstr "Horas"
#: pretix/base/models/items.py:630
msgid "Days"
msgstr ""
msgstr "Días"
#: pretix/base/models/items.py:634
#, fuzzy
@@ -4453,7 +4471,7 @@ msgstr "El elemento seleccionado no pertenece a este evento."
#: pretix/base/models/items.py:650
msgid "Reusable media policy"
msgstr ""
msgstr "Condiciones de utilización de imágenes"
#: pretix/base/models/items.py:652
msgid ""
@@ -5142,7 +5160,7 @@ msgstr "Tarjeta de crédito"
#: pretix/base/models/memberships.py:44
msgid "Membership is transferable"
msgstr ""
msgstr "La suscripción es transferible"
#: pretix/base/models/memberships.py:45
msgid ""
@@ -5152,7 +5170,7 @@ msgstr ""
#: pretix/base/models/memberships.py:50
msgid "Parallel usage is allowed"
msgstr ""
msgstr "El uso paralelo está permitido"
#: pretix/base/models/memberships.py:51
msgid ""
@@ -5235,7 +5253,7 @@ msgstr ""
#: pretix/base/models/orders.py:234
msgid "We'll show you this order to be due for a follow-up on this day."
msgstr ""
msgstr "Te mostraremos esta compra en el seguimiento de ese día."
#: pretix/base/models/orders.py:240
msgid ""
@@ -5414,7 +5432,7 @@ msgstr "Tarifa de cancelación"
#: pretix/base/models/orders.py:2132
msgid "Insurance fee"
msgstr ""
msgstr "Prima de seguro"
#: pretix/base/models/orders.py:2133
msgid "Other fees"
@@ -5615,7 +5633,7 @@ msgstr ""
#: pretix/base/models/tax.py:168
msgid "Official name"
msgstr ""
msgstr "Nombre oficial"
#: pretix/base/models/tax.py:169
msgid "Should be short, e.g. \"VAT\""
@@ -5698,7 +5716,7 @@ msgstr ""
#: pretix/base/models/tax.py:372
msgctxt "invoice"
msgid "VAT liability rests with the service recipient."
msgstr ""
msgstr "La responsabilidad del IVA es del destinatario del servicio."
#: pretix/base/models/vouchers.py:171
msgid "No effect"
@@ -5985,7 +6003,7 @@ msgstr "Debe elegir el producto \"{prod}\" para este asiento."
#: pretix/base/models/vouchers.py:500
#, python-brace-format
msgid "The seat \"{id}\" is already sold or currently blocked."
msgstr ""
msgstr "El puesto\"{id}\" ya se ha vendido o está bloqueado."
#: pretix/base/models/waitinglist.py:64
msgid "On waiting list since"
@@ -6507,7 +6525,7 @@ msgstr "Habilitar método de pago"
#: pretix/base/payment.py:441
msgid "Share this link with customers who should use this payment method."
msgstr ""
msgstr "Comparte este link con clientes que deben usar este método de pago."
#: pretix/base/payment.py:487
msgctxt "invoice"
@@ -6948,7 +6966,7 @@ msgstr "Dirección de facturación empresa"
#: pretix/base/pdf.py:339
msgid "Sesame Street 42"
msgstr ""
msgstr "Calle Sésamo 42"
#: pretix/base/pdf.py:344
#, fuzzy
@@ -7331,6 +7349,8 @@ msgid ""
"All payments for this event need to be confirmed already, so no new orders "
"can be created."
msgstr ""
"Todos los pagos de este evento tienen que estar ya confirmados, por lo que "
"no se pueden crear nuevos pedidos."
#: pretix/base/services/cart.py:136
msgid ""
@@ -7579,12 +7599,12 @@ msgstr "Razón desconocida"
#: pretix/base/services/checkin.py:246
#, python-brace-format
msgid "Only allowed before {datetime}"
msgstr ""
msgstr "Solo está permitido antes del {datetime}"
#: pretix/base/services/checkin.py:248
#, python-brace-format
msgid "Only allowed after {datetime}"
msgstr ""
msgstr "Solo está permitido después de {datetime}"
#: pretix/base/services/checkin.py:251
msgid "Ticket type not allowed"
@@ -7653,22 +7673,22 @@ msgstr "Domingo"
#: pretix/base/services/checkin.py:307
#, python-brace-format
msgid "{variable} is not {value}"
msgstr ""
msgstr "{variable} no es {value}"
#: pretix/base/services/checkin.py:309
#, python-brace-format
msgid "Maximum {variable} exceeded"
msgstr ""
msgstr "Máximo {variable} superado"
#: pretix/base/services/checkin.py:311
#, python-brace-format
msgid "Minimum {variable} exceeded"
msgstr ""
msgstr "Mínimo {variable} superado"
#: pretix/base/services/checkin.py:313
#, python-brace-format
msgid "{variable} is {value}"
msgstr ""
msgstr "{variable} es {value}"
#: pretix/base/services/checkin.py:763
msgid "This order position has been canceled."
@@ -7831,7 +7851,7 @@ msgstr "Product de Ejemplo A"
#: pretix/base/services/invoices.py:519
#, python-brace-format
msgid "New invoice: {number}"
msgstr ""
msgstr "Nueva factura: {number}"
#: pretix/base/services/invoices.py:521
#, python-brace-format
@@ -7843,6 +7863,13 @@ msgid ""
"We are sending this email because you configured us to do so in your event "
"settings."
msgstr ""
"Hola,\n"
"\n"
"Una nueva factura para tu pedido {order} de {event} se ha creado, la puedes "
"encontrar adjunta.\n"
"\n"
"Te estamos enviando este email porque lo has configurado así en la página de "
"configuración del evento."
#: pretix/base/services/mail.py:266
#, fuzzy, python-brace-format
@@ -10051,17 +10078,7 @@ msgstr ""
"su equipo {event}"
#: pretix/base/settings.py:2108
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "your order {code} for {event} has been canceled.\n"
#| "\n"
#| "You can view the details of your order at\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
#, python-brace-format
msgid ""
"Hello {attendee_name},\n"
"\n"
@@ -10073,15 +10090,15 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"Hola, \n"
"Hola, {attendee_name}: \n"
"\n"
"su pedido {code} para {event} ha sido cancelado. \n"
"Te has registrado correctamente para {event}. \n"
"\n"
"Puede ver los detalles de su pedido en\n"
"Puedes ver el estado y los detalles de tu entrada aquí:\n"
"{url}. \n"
"\n"
"Saludos cordiales, \n"
"su equipo {event}"
"El equipo {event}"
#: pretix/base/settings.py:2128
#, python-brace-format
@@ -10621,19 +10638,7 @@ msgid "Order approved and confirmed: {code}"
msgstr "Pedido aprobado y confirmado: {code}"
#: pretix/base/settings.py:2462
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "your order for {event} was successful. As you only ordered free "
#| "products,\n"
#| "no payment is required.\n"
#| "\n"
#| "You can change your order details and view the status of your order at\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
#, python-brace-format
msgid ""
"Hello,\n"
"\n"
@@ -10648,15 +10653,15 @@ msgid ""
msgstr ""
"Hola, \n"
"\n"
"su pedido para {event} fue un exitoso. Como sólo ha pedido productos "
"gratuitos , \n"
"no se requiere ningún pago. \n"
"se ha aprobado tu pedido para {event} y te damos la bienvenida a nuestro "
"evento. Como solo ha pedido productos gratuitos, no se requiere ningún pago. "
"\n"
"\n"
"Puede cambiar los detalles de su pedido y ver el estado del mismo en\n"
"{url}\n"
"\n"
"Saludos cordiales, \n"
"su equipo {event}"
"El equipo de {event}"
#: pretix/base/settings.py:2495
#, python-brace-format
@@ -10717,17 +10722,7 @@ msgid "Your ticket is ready for download: {code}"
msgstr "Su ticket está listo para descargar: {code}"
#: pretix/base/settings.py:2536
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello {attendee_name},\n"
#| "\n"
#| "you are registered for {event}.\n"
#| "\n"
#| "If you did not do so already, you can download your ticket here:\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
#, python-brace-format
msgid ""
"Hello {attendee_name},\n"
"\n"
@@ -10739,11 +10734,11 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"Hola {attendee_name},\n"
"Hola, {attendee_name}:\n"
"\n"
"está registrado en {event}.\n"
"\n"
"Si aún no lo ha hecho, puede descargar su entrada aquí :\n"
"Si aún no lo ha hecho, puede descargar su entrada aquí:\n"
"{url} \n"
"\n"
"Saludos cordiales, \n"
@@ -12453,10 +12448,8 @@ msgid "Ask for {fields}, display like {example}"
msgstr "Pregunta por {fields}, despliega como {example}"
#: pretix/control/forms/event.py:634 pretix/control/forms/organizer.py:454
#, fuzzy
#| msgid "Free price input"
msgid "Free text input"
msgstr "Entrada de precio gratuita"
msgstr "Entrada de texto libre"
#: pretix/control/forms/event.py:666
#, fuzzy
@@ -12733,10 +12726,8 @@ msgid "Subject for approved free order"
msgstr "Pedido aprobado"
#: pretix/control/forms/event.py:1233
#, fuzzy
#| msgid "Approved order"
msgid "Text for approved free order"
msgstr "Pedido aprobado"
msgstr "Texto aprobado"
#: pretix/control/forms/event.py:1236 pretix/control/forms/event.py:1254
#, fuzzy
@@ -14259,7 +14250,7 @@ msgstr "ticket secreto:"
#: pretix/control/forms/orders.py:458
msgid "Validity start"
msgstr ""
msgstr "Incio de validez"
#: pretix/control/forms/orders.py:463
#, fuzzy
@@ -14410,10 +14401,8 @@ msgid "Keep a fixed cancellation fee per ticket"
msgstr "Mantener una tarifa de cancelación fija"
#: pretix/control/forms/orders.py:828
#, fuzzy
#| msgid "Generate tickets for non-admission products"
msgid "Free tickets and add-on products are not counted"
msgstr "Generar tickets para productos no admitidos"
msgstr "Las entradas gratuitas en productos complementarios no contabilizan"
#: pretix/control/forms/orders.py:838
#, fuzzy
@@ -14607,29 +14596,23 @@ msgid "Gift card value"
msgstr "Tarjeta de regalo"
#: pretix/control/forms/organizer.py:700
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "An medium with this type and identifier is already registered."
msgstr "Este ticket ya ha sido canjeado."
msgstr "Un medio con este tipo y este identificador ya ha sido registrado."
#: pretix/control/forms/organizer.py:801
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "An account with this customer ID is already registered."
msgstr "Este ticket ya ha sido canjeado."
msgstr "Una cuenta con este identificador de usuario ya está registrado."
#: pretix/control/forms/organizer.py:802 pretix/presale/forms/customer.py:439
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "An account with this email address is already registered."
msgstr "Este ticket ya ha sido canjeado."
msgstr "Una cuenta con esta dirección de correo ya está registrada."
#: pretix/control/forms/organizer.py:818
#: pretix/control/templates/pretixcontrol/organizers/customer.html:60
#: pretix/presale/forms/customer.py:156 pretix/presale/forms/customer.py:472
#: pretix/presale/templates/pretixpresale/organizers/customer_profile.html:32
msgid "Phone"
msgstr ""
msgstr "Teléfono"
#: pretix/control/forms/organizer.py:925
#, fuzzy
@@ -14655,7 +14638,7 @@ msgstr "Clave Secreta"
#: pretix/control/forms/organizer.py:937
msgctxt "sso_oidc"
msgid "Scope"
msgstr ""
msgstr "Alcance"
#: pretix/control/forms/organizer.py:938
msgctxt "sso_oidc"
@@ -14665,7 +14648,7 @@ msgstr ""
#: pretix/control/forms/organizer.py:942
msgctxt "sso_oidc"
msgid "User ID field"
msgstr ""
msgstr "Campo identificador del usuario"
#: pretix/control/forms/organizer.py:943
msgctxt "sso_oidc"
@@ -17064,6 +17047,8 @@ msgid ""
"We've detected that you are using <strong>Microsoft Internet Explorer</"
"strong>."
msgstr ""
"Hemos detectado que estás usando <strong> Microsoft Internet Explorer "
"</strong>."
#: pretix/control/templates/pretixcontrol/base.html:328
msgid ""
@@ -17141,6 +17126,8 @@ msgid ""
"For security reasons, please change your password before you continue. "
"Afterwards you will be redirected to your original destination."
msgstr ""
"Por motivos de seguridad, por favor cambia tu contraseña antes de continuar. "
"Serás redirigido después a la página de origen."
#: pretix/control/templates/pretixcontrol/base.html:446
#, python-format
@@ -17329,7 +17316,7 @@ msgstr "Resultado"
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:108
msgid "Cash"
msgstr ""
msgstr "Efectivo"
#: pretix/control/templates/pretixcontrol/checkin/checkins.html:9
#: pretix/control/templates/pretixcontrol/checkin/checkins.html:41
@@ -20839,7 +20826,7 @@ msgstr "No hay solicitudes registradas todavía."
#: pretix/control/templates/pretixcontrol/oauth/app_register.html:4
#: pretix/control/templates/pretixcontrol/oauth/app_register.html:6
msgid "Register a new application"
msgstr "Registrar una nueva aplicación"
msgstr "Hacer un nuevo registro"
#: pretix/control/templates/pretixcontrol/oauth/app_rollkeys.html:4
#: pretix/control/templates/pretixcontrol/oauth/app_rollkeys.html:6
@@ -22706,7 +22693,7 @@ msgstr "Crear varias fechas"
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:13
#, python-format
msgid "%(number)s selected"
msgstr ""
msgstr "%(number)s selecionado"
#: pretix/control/templates/pretixcontrol/organizers/device_bulk_edit.html:36
#: pretix/control/templates/pretixcontrol/organizers/device_edit.html:24
@@ -26928,10 +26915,8 @@ msgid "The device has been removed."
msgstr "El dispositivo ha sido retirado."
#: pretix/control/views/user.py:449
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "This security device is already registered."
msgstr "Este ticket ya ha sido canjeado."
msgstr "Este dispositivo ya está registrado."
#: pretix/control/views/user.py:471 pretix/control/views/user.py:532
msgid "A new two-factor authentication device has been added to your account."
@@ -28815,6 +28800,8 @@ msgid ""
"After placing your order, you will be able to select your desired payment "
"method, including PayPal."
msgstr ""
"Después de hacer el pedido, podrás elegir el método de pago preferido, "
"incluyendo PayPal."
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/checkout_payment_form.html:5
msgid ""
@@ -29843,10 +29830,8 @@ msgid "Blocked Seats"
msgstr ""
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:86
#, fuzzy
#| msgid "Free order"
msgid "Free Seats"
msgstr "Pedido gratuito"
msgstr "Asiento gratuito"
#: pretix/plugins/statistics/templates/pretixplugins/statistics/index.html:94
#, fuzzy
@@ -31066,6 +31051,8 @@ msgid ""
"An account with this email address is already registered. Please try to log "
"in or reset your password instead."
msgstr ""
"Una cuenta con esta dirección de correo ya está registrada. Puedes entrar o "
"recuperar la contraseña."
#: pretix/presale/forms/customer.py:189
#, python-brace-format
@@ -31707,7 +31694,7 @@ msgstr "cantidad mínima a pedir: %(num)s"
#: pretix/presale/templates/pretixpresale/event/voucher.html:354
msgctxt "price"
msgid "free"
msgstr ""
msgstr "gratis"
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:77
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:51
@@ -32400,7 +32387,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html:22
#: pretix/presale/templates/pretixpresale/organizers/calendar.html:24
msgid "Select month to show"
msgstr ""
msgstr "Seleccione un mes a mostrar"
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html:32
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:36
@@ -32423,7 +32410,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:22
#: pretix/presale/templates/pretixpresale/organizers/calendar_week.html:26
msgid "Select week to show"
msgstr ""
msgstr "Selecciona semana a mostrar"
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:43
#, python-format
@@ -32779,6 +32766,7 @@ msgstr "Cambiar detalles"
msgid ""
"You need to select a payment method above before you can request an invoice."
msgstr ""
"Es necesario seleccionar un método de pago antes de solicitar una factura."
#: pretix/presale/templates/pretixpresale/event/order.html:267
#: pretix/presale/templates/pretixpresale/event/order.html:274
@@ -33141,6 +33129,8 @@ msgid ""
"Please select the desired changes to your ticket. Note that you can only "
"perform changes that do not change the total price of the ticket."
msgstr ""
"Selecciona los cambios que quieres hacer en tu entrada. Ten en cuenta que "
"solo puedes hacer cambios que no cambien el valor total de la entrada."
#: pretix/presale/templates/pretixpresale/event/position_change_confirm.html:19
#, fuzzy
@@ -33702,7 +33692,7 @@ msgstr "No se ha encontrado el organizador seleccionado."
msgid ""
"Your selected payment method can only be used for a payment of at least "
"{amount}."
msgstr ""
msgstr "El método de pago solo se puede usar para un pago de mínimo {amount}."
#: pretix/presale/views/cart.py:183
msgid "Please enter positive numbers only."
@@ -33981,6 +33971,8 @@ msgid ""
"Thank you very much! We will assign your spot on the waiting list to someone "
"else."
msgstr ""
"¡Muchas gracias! Le asignaremos tu puesto en la lista de espera a otra "
"persona."
#: pretix/presale/views/widget.py:341
#, fuzzy
@@ -34001,7 +33993,7 @@ msgstr "de %(start_date)s"
#: pretix/settings.py:710
msgid "User profile only"
msgstr ""
msgstr "Solo perfil de usuario"
#: pretix/settings.py:711
msgid "Read access"
+90 -157
View File
@@ -4,8 +4,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
"PO-Revision-Date: 2023-07-22 21:00+0000\n"
"Last-Translator: Ronan LE MEILLAT <ronan.le_meillat@highcanfly.club>\n"
"PO-Revision-Date: 2023-08-16 22:00+0000\n"
"Last-Translator: Maurice Kaag <maurice@kaag.me>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"Language: fr\n"
@@ -13,7 +13,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.17\n"
"X-Generator: Weblate 4.18.2\n"
#: pretix/_base_settings.py:78
msgid "English"
@@ -247,9 +247,7 @@ msgstr ""
#: pretix/api/serializers/item.py:185 pretix/control/forms/item.py:1084
msgid "The bundled item must not have bundles on its own."
msgstr ""
"Un forfait ne doit pas contenir des produits, qui sont eux-mêmes des "
"forfaits."
msgstr "Un produit groupé ne doit pas contenir des produits groupés."
#: pretix/api/serializers/item.py:262
msgid ""
@@ -1666,7 +1664,7 @@ msgstr "Nécessite une attention particulière"
#: pretix/base/exporters/items.py:91 pretix/base/models/items.py:553
#: pretix/base/models/items.py:1018
msgid "Original price"
msgstr "Facture originale"
msgstr "Prix d'origine"
#: pretix/base/exporters/items.py:92 pretix/base/models/items.py:565
msgid "This product is a gift card"
@@ -3097,7 +3095,7 @@ msgstr "Annulation"
#: pretix/base/invoice.py:620 pretix/base/invoice.py:628
msgctxt "invoice"
msgid "Description"
msgstr "Déscription"
msgstr "Description"
#: pretix/base/invoice.py:621 pretix/base/invoice.py:629
msgctxt "invoice"
@@ -6227,24 +6225,18 @@ msgid "Ambiguous option selected."
msgstr "Option ambiguë sélectionnée."
#: pretix/base/orderimport.py:845
#, fuzzy
#| msgid "No matching seat was found."
msgid "No matching customer was found."
msgstr "Aucun siège correspondant na été trouvé."
msgstr "Aucun client correspondant na été trouvé."
#: pretix/base/payment.py:86
#, fuzzy
#| msgid "Apply"
msgctxt "payment"
msgid "Apple Pay"
msgstr "Appliquer"
msgstr "Apple Pay"
#: pretix/base/payment.py:87
#, fuzzy
#| msgid "Android (Google Play)"
msgctxt "payment"
msgid "Google Pay"
msgstr "Android (Google Play)"
msgstr "Google Pay"
#: pretix/base/payment.py:256
#: pretix/presale/templates/pretixpresale/event/order.html:115
@@ -6811,16 +6803,12 @@ msgid "List of Add-Ons"
msgstr "Liste des Addons"
#: pretix/base/pdf.py:364
#, fuzzy
#| msgid ""
#| "Add-on 1\n"
#| "Add-on 2"
msgid ""
"Add-on 1\n"
"2x Add-on 2"
msgstr ""
"Add-on 1\n"
"Add-on 2"
"2x Add-on 2"
#: pretix/base/pdf.py:370 pretix/control/forms/filter.py:1275
#: pretix/control/forms/filter.py:1277
@@ -8229,16 +8217,14 @@ msgid "Gift card currency"
msgstr "Devise de la carte-cadeau"
#: pretix/base/settings.py:277
#, fuzzy
#| msgid ""
#| "Automatically create a new gift card if a previously unknown chip is seen"
msgid "Automatically create a new gift card if a new chip is encoded"
msgstr ""
"Créer automatiquement une nouvelle carte-cadeau si une puce inconnue est vue"
"Créer automatiquement une nouvelle carte-cadeau si une nouvelle puce est "
"encodée"
#: pretix/base/settings.py:299
msgid "Use UID protection feature of NFC chip"
msgstr ""
msgstr "Utiliser la fonction de protection UID de la puce NFC"
#: pretix/base/settings.py:313
msgid "Maximum number of items per order"
@@ -8753,10 +8739,8 @@ msgstr ""
"commandés par d'autres personnes."
#: pretix/base/settings.py:942
#, fuzzy
#| msgid "Expiration date"
msgid "Expiration delay"
msgstr "Date d'expiration"
msgstr "Délai dexpiration"
#: pretix/base/settings.py:943
msgid ""
@@ -8766,6 +8750,11 @@ msgid ""
"beyond the \"last date of payments\" configured above, which is always "
"enforced."
msgstr ""
"La commande nexpirera réellement que ce nombre de jours après la date d"
"expiration communiquée au client. Si vous sélectionnez « Ne terminez les "
"conditions de paiement que les jours de semaine » ci-dessus, cela sera "
"également respecté. Cependant, cela ne retardera pas au-delà de la « "
"dernière date de paiement » configurée ci-dessus, qui est toujours appliquée."
#: pretix/base/settings.py:964
msgid "Hide \"payment pending\" state on customer-facing pages"
@@ -10186,25 +10175,12 @@ msgstr ""
"Votre équipe {event}"
#: pretix/base/settings.py:2349
#, fuzzy, python-brace-format
#| msgid "Payment received for your order: {code}"
#, python-brace-format
msgid "Payment failed for your order: {code}"
msgstr "Paiement reçu pour votre commande: {code}"
msgstr "Paiement échoué pour votre commande: {code}"
#: pretix/base/settings.py:2353
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "we did not yet receive a full payment for your order for {event}.\n"
#| "Please keep in mind that we only guarantee your order if we receive\n"
#| "your payment before {expire_date}.\n"
#| "\n"
#| "You can view the payment information and the status of your order at\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
#, python-brace-format
msgid ""
"Hello,\n"
"\n"
@@ -10222,18 +10198,18 @@ msgid ""
msgstr ""
"Bonjour\n"
"\n"
"Nous navons pas encore reçu le paiement intégral de votre commande de "
"{event}.\n"
"Veuillez garder à lesprit que nous ne garantissons votre commande que si "
"nous recevons\n"
"votre paiement avant {expire_date}.\n"
"Votre tentative de paiement pour votre commande pour {event} a échoué.\n"
"\n"
"Vous pouvez consulter les informations de paiement et l’état de votre "
"commande à ladresse\n"
"Votre commande est toujours valide et vous pouvez essayer de payer à nouveau "
"en utilisant le même mode de paiement ou un mode de paiement différent. "
"Veuillez effectuer votre paiement avant {expire_date}.\n"
"\n"
"Vous pouvez réessayer le paiement et consulter l’état de votre commande à l"
"adresse\n"
"{url}\n"
"\n"
"Sinceres salutations \n"
"Votre {event} équipe"
"Votre équipe {event}"
#: pretix/base/settings.py:2367
#, python-brace-format
@@ -11154,7 +11130,7 @@ msgstr "Degré (après le nom)"
#: pretix/base/settings.py:3577
msgctxt "person_name_sample"
msgid "MA"
msgstr ""
msgstr "MA"
#: pretix/base/settings.py:3684 pretix/control/forms/event.py:217
msgid ""
@@ -11195,7 +11171,7 @@ msgstr ""
#: pretix/base/settings.py:3720
msgid "This needs to be disabled if other NFC-based types are active."
msgstr ""
msgstr "Cela doit être désactivé si dautres types NFC sont actifs."
#: pretix/base/shredder.py:74 pretix/base/shredder.py:77
msgid "Your event needs to be over to use this feature."
@@ -14920,10 +14896,8 @@ msgid "The medium has been connected to a new ticket."
msgstr "Le média a été connecté à un nouveau ticket."
#: pretix/control/logdisplay.py:371
#, fuzzy
#| msgid "The medium has been connected to a new ticket."
msgid "The medium has been connected to a new gift card."
msgstr "Le média a été connecté à un nouveau ticket."
msgstr "Le média a été connecté à une nouvelle carte cadeau."
#: pretix/control/logdisplay.py:372 pretix/control/logdisplay.py:413
msgid "Sending of an email has failed."
@@ -15193,12 +15167,9 @@ msgstr ""
"l'utilisateur."
#: pretix/control/logdisplay.py:436
#, fuzzy
#| msgid ""
#| "An email has been sent to notify the user that payment has been received."
msgid "An email has been sent to notify the user that the payment failed."
msgstr ""
"Un mail a été envoyé pour informer l'utilisateur que le paiement a été reçu."
"Un mail a été envoyé pour informer l'utilisateur que le paiement a échoué."
#: pretix/control/logdisplay.py:437
#, python-brace-format
@@ -18023,7 +17994,7 @@ msgstr "Contenu de l' e-mail"
#: pretix/control/templates/pretixcontrol/event/mail.html:90
msgid "Placed order"
msgstr "Ordre placé"
msgstr "Commande placée"
#: pretix/control/templates/pretixcontrol/event/mail.html:93
msgid "Paid order"
@@ -18044,10 +18015,8 @@ msgid "Payment reminder"
msgstr "Rappel de paiement"
#: pretix/control/templates/pretixcontrol/event/mail.html:108
#, fuzzy
#| msgid "Payment fee"
msgid "Payment failed"
msgstr "Frais de paiement"
msgstr "Paiement échoué"
#: pretix/control/templates/pretixcontrol/event/mail.html:111
msgid "Waiting list notification"
@@ -18102,8 +18071,6 @@ msgid "Deadlines"
msgstr "Échéances"
#: pretix/control/templates/pretixcontrol/event/payment.html:68
#, fuzzy
#| msgid "days"
msgctxt "unit"
msgid "days"
msgstr "jours"
@@ -22176,6 +22143,10 @@ msgid ""
"made by NXP. This provides a higher level of security than other approaches, "
"but requires all chips to be encoded prior to use."
msgstr ""
"Ce type de support ne fonctionne quavec des puces NFC du type Mifare "
"Ultralight AES fabriquées par NXP. Cela fournit un niveau de sécurité plus "
"élevé que les autres approches, mais nécessite que toutes les puces soient "
"encodées avant utilisation."
#: pretix/control/templates/pretixcontrol/organizers/export.html:64
msgid "Run export now and download result"
@@ -24255,19 +24226,15 @@ msgid ""
"For safety reasons, the waiting list does not run if the quota is set to "
"unlimited."
msgstr ""
"Pour des raisons de sécurité, la liste dattente ne fonctionne pas si le "
"quota est fixé sur illimité."
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:219
#, fuzzy
#| msgid "Quota name"
msgid "Quota unlimited"
msgstr "Nom du quota"
msgstr "Quota illimité"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:225
#, fuzzy, python-format
#| msgid ""
#| "\n"
#| " Waiting, product %(num)sx available\n"
#| " "
#, python-format
msgid ""
"\n"
" Waiting, product %(num)sx "
@@ -24275,8 +24242,9 @@ msgid ""
" "
msgstr ""
"\n"
" En attente, produit %(num)sx disponible\n"
" "
" En attente, produit %(num)sx "
"disponible\n"
" "
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:231
msgid "Waiting, product unavailable"
@@ -25047,13 +25015,6 @@ msgid "The selected product has been deactivated."
msgstr "Le produit sélectionné a été désactivé."
#: pretix/control/views/mailsetup.py:195
#, fuzzy
#| msgid ""
#| "We could not find an SPF record set for the domain you are trying to use. "
#| "You can still proceed, but it will increase the chance of emails going to "
#| "spam or being rejected. We strongly recommend setting an SPF record on "
#| "the domain. You can do so through the DNS settings at the provider you "
#| "registered your domain with."
msgid ""
"We could not find an SPF record set for the domain you are trying to use. "
"This means that there is a very high change most of the emails will be "
@@ -25062,11 +25023,11 @@ msgid ""
"registered your domain with."
msgstr ""
"Nous navons pas pu trouver de jeu denregistrements SPF pour le domaine que "
"vous essayez dutiliser. Vous pouvez toujours continuer, mais cela "
"augmentera les chances que les e-mails soient envoyés au spam ou rejetés. "
"Nous vous recommandons vivement de définir un enregistrement SPF sur le "
"domaine. Vous pouvez le faire via les paramètres DNS du fournisseur auprès "
"duquel vous avez enregistré votre domaine."
"vous essayez dutiliser. Cela signifie quil y a un changement très élevé, "
"la plupart des e-mails seront rejetés ou marqués comme spam. Nous vous "
"recommandons vivement de définir un enregistrement SPF sur le domaine. Vous "
"pouvez le faire via les paramètres DNS du fournisseur auprès duquel vous "
"avez enregistré votre domaine."
#: pretix/control/views/mailsetup.py:202
msgid ""
@@ -28160,9 +28121,6 @@ msgid "Restrict to event dates starting before"
msgstr "Limiter aux dates d’événements commençant avant"
#: pretix/plugins/sendmail/forms.py:170
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Send to"
msgctxt "sendmail_form"
msgid "Send to"
msgstr "Envoyer à"
@@ -28177,9 +28135,6 @@ msgid "Filter check-in status"
msgstr "Filtrer le statut d'enregistrement"
#: pretix/plugins/sendmail/forms.py:189
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Restrict to recipients without check-in"
msgctxt "sendmail_form"
msgid "Restrict to recipients without check-in"
msgstr "Restreindre aux destinataires sans enregistrement"
@@ -28233,17 +28188,11 @@ msgid "pending with payment overdue"
msgstr "en attente avec retard"
#: pretix/plugins/sendmail/forms.py:258
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Restrict to orders with status"
msgctxt "sendmail_form"
msgid "Restrict to orders with status"
msgstr "Restreindre aux commandes avec statut"
#: pretix/plugins/sendmail/forms.py:283 pretix/plugins/sendmail/forms.py:287
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Restrict to recipients with check-in on list"
msgctxt "sendmail_form"
msgid "Restrict to recipients with check-in on list"
msgstr "Restreindre aux destinataires avec enregistrement sur la liste"
@@ -28314,9 +28263,6 @@ msgid "Limit products"
msgstr "Limiter les produits"
#: pretix/plugins/sendmail/models.py:218
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Restrict to orders with status"
msgid "Restrict to orders with status"
msgstr "Restreindre aux commandes avec statut"
@@ -28880,7 +28826,7 @@ msgstr ""
#: pretix/plugins/stripe/payment.py:296
msgid "Check for Apple Pay/Google Pay"
msgstr ""
msgstr "Vérifier Apple Pay/Google Pay"
#: pretix/plugins/stripe/payment.py:300
msgid ""
@@ -28890,6 +28836,11 @@ msgid ""
"take into consideration if Google Pay/Apple Pay has been disabled in the "
"Stripe Dashboard."
msgstr ""
"pretix tentera de vérifier si le navigateur Web du client prend en charge "
"les méthodes de paiement basées sur le portefeuille comme Apple Pay ou "
"Google Pay et les affichera bien en évidence avec le mode de paiement par "
"carte de crédit. Cette détection ne prend pas en compte si Google Pay/Apple "
"Pay a été désactivé dans le tableau de bord Stripe."
#: pretix/plugins/stripe/payment.py:309
msgid "Statement descriptor postfix"
@@ -28939,38 +28890,32 @@ msgid "Bancontact"
msgstr "Bancontact"
#: pretix/plugins/stripe/payment.py:357
#, fuzzy
#| msgid "Disable SEPA Direct Debit"
msgid "SEPA Direct Debit"
msgstr "Désactiver le prélèvement SEPA"
msgstr "Prélèvement SEPA"
#: pretix/plugins/stripe/payment.py:362
#, fuzzy
#| msgid ""
#| "Despite the name, Sofort payments via Stripe are <strong>not</strong> "
#| "processed instantly but might take up to <strong>14 days</strong> to be "
#| "confirmed in some cases. Please only activate this payment method if your "
#| "payment term allows for this lag."
msgid ""
"SEPA Direct Debit payments via Stripe are <strong>not</strong> processed "
"instantly but might take up to <strong>14 days</strong> to be confirmed in "
"some cases. Please only activate this payment method if your payment term "
"allows for this lag."
msgstr ""
"Malgré leur nom, les paiements Sofort via Stripe <strong>ne sont pas</"
"strong> traités instantanément, mais peuvent prendre jusqu’à <strong>14 "
"jours</strong> pour être confirmés dans certains cas. Veuillez nactiver ce "
"mode de paiement que si votre délai de paiement le permet."
"Les paiements par prélèvement SEPA via Stripe ne sont <strong>pas</strong> "
"traités instantanément, mais peuvent prendre jusqu’à <strong>14 jours</"
"strong> pour être confirmés dans certains cas. Veuillez nactiver ce mode de "
"paiement que si votre délai de paiement le permet."
#: pretix/plugins/stripe/payment.py:370
msgid "SEPA Creditor Mandate Name"
msgstr ""
msgstr "Nom du mandat du créancier SEPA"
#: pretix/plugins/stripe/payment.py:372
msgid ""
"Please provide your SEPA Creditor Mandate Name, that will be displayed to "
"the user."
msgstr ""
"Veuillez fournir votre nom de mandat de créancier SEPA, qui sera affiché à "
"lutilisateur."
#: pretix/plugins/stripe/payment.py:383
msgid "SOFORT"
@@ -29081,44 +29026,32 @@ msgid "Credit card"
msgstr "Carte de crédit"
#: pretix/plugins/stripe/payment.py:1157
#, fuzzy
#| msgid "EPS via Stripe"
msgid "SEPA Debit via Stripe"
msgstr "EPS via Stripe"
msgstr "Prélèvement SEPA via Stripe"
#: pretix/plugins/stripe/payment.py:1158
msgid "SEPA Debit"
msgstr ""
msgstr "Débit SEPA"
#: pretix/plugins/stripe/payment.py:1197
#, fuzzy
#| msgid "Account holder"
msgid "Account Holder Name"
msgstr "Titulaire du compte"
#: pretix/plugins/stripe/payment.py:1202
#, fuzzy
#| msgid "Account holder"
msgid "Account Holder Street"
msgstr "Titulaire du compte"
msgstr "Rue du titulaire du compte"
#: pretix/plugins/stripe/payment.py:1214
#, fuzzy
#| msgid "Account holder"
msgid "Account Holder Postal Code"
msgstr "Titulaire du compte"
msgstr "Code postal du titulaire du compte"
#: pretix/plugins/stripe/payment.py:1226
#, fuzzy
#| msgid "Account holder"
msgid "Account Holder City"
msgstr "Titulaire du compte"
msgstr "Ville du titulaire du compte"
#: pretix/plugins/stripe/payment.py:1238
#, fuzzy
#| msgid "Account holder"
msgid "Account Holder Country"
msgstr "Titulaire du compte"
msgstr "Pays du titulaire du compte"
#: pretix/plugins/stripe/payment.py:1282
msgid "giropay via Stripe"
@@ -29289,22 +29222,18 @@ msgid "Card type"
msgstr "Type de carte"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:14
#, fuzzy
#| msgid "The total amount will be withdrawn from your credit card."
msgid "The total amount will be withdrawn from your bank account."
msgstr "Le montant total sera prélevé sur votre carte de crédit."
msgstr "Le montant total sera prélevé sur votre compte bancaire."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:18
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:20
msgid "Banking Institution"
msgstr ""
msgstr "Établissement bancaire"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:20
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:22
#, fuzzy
#| msgid "Account holder"
msgid "Account number"
msgstr "Titulaire du compte"
msgstr "Numéro de compte"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:24
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_simple.html:4
@@ -29353,28 +29282,20 @@ msgstr ""
"serveurs."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:10
#, fuzzy
#| msgid "For a credit card payment, please turn on JavaScript."
msgid "For a SEPA Debit payment, please turn on JavaScript."
msgstr "Pour un paiement par carte de crédit, veuillez activer JavaScript."
msgstr "Pour un paiement par prélèvement SEPA, veuillez activer JavaScript."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:16
#, fuzzy
#| msgid ""
#| "You already entered a card number that we will use to charge the payment "
#| "amount."
msgid ""
"You already entered a bank account that we will use to charge the payment "
"amount."
msgstr ""
"Vous avez déjà entré un numéro de carte que nous utiliserons pour débiter le "
"Vous avez déjà saisi un compte bancaire que nous utiliserons pour débiter le "
"montant du paiement."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:27
#, fuzzy
#| msgid "Use a different card"
msgid "Use a different account"
msgstr "Utiliser une autre carte"
msgstr "Utiliser un autre compte"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:51
#, python-format
@@ -29390,6 +29311,18 @@ msgid ""
"statement that you can obtain from your bank. You agree to receive "
"notifications for future debits up to 2 days before they occur."
msgstr ""
"En fournissant vos informations de paiement et en confirmant ce paiement, "
"vous autorisez (A) %(sepa_creditor_name)s et Stripe, notre prestataire de "
"services de paiement et/ou PPRO, son prestataire de services local, à "
"envoyer des instructions à votre banque pour débiter votre compte et (B) "
"votre banque à débiter votre compte conformément à ces instructions. Dans le "
"cadre de vos droits, vous avez droit à un remboursement de votre banque "
"selon les termes et conditions de votre accord avec votre banque. Un "
"remboursement doit être demandé dans un délai de 8 semaines à compter de la "
"date à laquelle votre compte a été débité. Vos droits sont expliqués dans "
"une déclaration que vous pouvez obtenir auprès de votre banque. Vous "
"acceptez de recevoir des notifications pour les débits futurs jusqu’à 2 "
"jours avant quils ne se produisent."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/control.html:6
msgid "Charge ID"
+4 -6
View File
@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
"PO-Revision-Date: 2023-07-19 17:00+0000\n"
"PO-Revision-Date: 2023-08-02 02:00+0000\n"
"Last-Translator: Ronan LE MEILLAT <ronan.le_meillat@highcanfly.club>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
"fr/>\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.17\n"
"X-Generator: Weblate 4.18.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -63,7 +63,7 @@ msgstr "iDEAL"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit"
msgstr "Débit direct SEPA"
msgstr "Prélèvement SEPA"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
msgid "Bancontact"
@@ -679,10 +679,8 @@ msgid "Your local time:"
msgstr "Votre heure locale:"
#: pretix/static/pretixpresale/js/walletdetection.js:39
#, fuzzy
#| msgid "Apple Pay"
msgid "Google Pay"
msgstr "Apple Pay"
msgstr "Google Pay"
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
+57 -121
View File
@@ -7,16 +7,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
"PO-Revision-Date: 2023-07-16 22:00+0000\n"
"Last-Translator: Freek Engelbarts <freekengelbarts@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
">\n"
"PO-Revision-Date: 2023-08-25 04:00+0000\n"
"Last-Translator: Alain <alain@waag.org>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
"X-Generator: Weblate 4.18.2\n"
#: pretix/_base_settings.py:78
msgid "English"
@@ -411,7 +411,7 @@ msgstr "Bestelling is verlopen"
#: pretix/api/webhooks.py:234
msgid "Order expiry date changed"
msgstr "Verloopdatum aangepast."
msgstr "Verloopdatum aangepast"
#: pretix/api/webhooks.py:238 pretix/base/notifications.py:269
msgid "Order information changed"
@@ -534,10 +534,8 @@ msgid "Waiting list entry deleted"
msgstr "Wachtlijstitem verwijderd"
#: pretix/api/webhooks.py:351
#, fuzzy
#| msgid "Waiting list entries"
msgid "Waiting list entry received voucher"
msgstr "Wachtlijstitems"
msgstr "Wachtlijstitem heeft voucher ontvangen"
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:938
@@ -553,11 +551,11 @@ msgstr "Dit veld is verplicht."
#: pretix/base/addressvalidation.py:213
msgid "Enter a postal code in the format XXX."
msgstr "Voer een postcode in in het formaat XXX."
msgstr "Postcode in het formaat XXX invoeren."
#: pretix/base/addressvalidation.py:222 pretix/base/addressvalidation.py:224
msgid "Enter a postal code in the format XXXX."
msgstr "Voer een postcode in in het format XXXX."
msgstr "Postcode in het format XXXX invoeren."
#: pretix/base/auth.py:143
#, python-brace-format
@@ -2311,7 +2309,7 @@ msgstr ""
#: pretix/base/exporters/orderlist.py:887
msgid "Converted from legacy version"
msgstr ""
msgstr "Vanuit oudere versie geconverteerd"
#: pretix/base/exporters/orderlist.py:949
msgid "Payments and refunds"
@@ -4380,7 +4378,7 @@ msgstr ""
#: pretix/base/models/items.py:662
msgid "Reusable media type"
msgstr ""
msgstr "Mediatype"
#: pretix/base/models/items.py:664
msgid ""
@@ -6146,7 +6144,7 @@ msgstr "Vul een geldige taalcode in."
#: pretix/base/orderimport.py:669 pretix/base/orderimport.py:692
#, python-brace-format
msgid "Could not parse {value} as a date and time."
msgstr ""
msgstr "Kon {value} niet als datum en tijd herkennen."
#: pretix/base/orderimport.py:711
msgid "Please enter a valid sales channel."
@@ -6847,7 +6845,7 @@ msgstr "Geldig tot"
#: pretix/base/pdf.py:457
msgid "Reusable Medium ID"
msgstr ""
msgstr "Media-ID"
#: pretix/base/pdf.py:462
msgid "Seat: Full name"
@@ -7047,38 +7045,30 @@ msgstr ""
"door u gekozen hoeveelheid. Zie hieronder voor de details."
#: pretix/base/services/cart.py:114
#, fuzzy, python-format
#| msgid "You cannot select more than %s items per order."
#, python-format
msgid "You cannot select more than %s item per order."
msgid_plural "You cannot select more than %s items per order."
msgstr[0] "U kunt niet meer dan %s items per bestelling kiezen."
msgstr[0] "U kunt niet meer dan %s item per bestelling kiezen."
msgstr[1] "U kunt niet meer dan %s items per bestelling kiezen."
#: pretix/base/services/cart.py:118 pretix/base/services/orders.py:1468
#, fuzzy, python-format
#| msgid ""
#| "You cannot select more than %(max)s items of the product %(product)s."
#, python-format
msgid "You cannot select more than %(max)s item of the product %(product)s."
msgid_plural ""
"You cannot select more than %(max)s items of the product %(product)s."
msgstr[0] "U kunt niet meer dan %(max)s items van product %(product)s kiezen."
msgstr[0] "U kunt niet meer dan %(max)s item van product %(product)s kiezen."
msgstr[1] "U kunt niet meer dan %(max)s items van product %(product)s kiezen."
#: pretix/base/services/cart.py:123 pretix/base/services/orders.py:1473
#, fuzzy, python-format
#| msgid ""
#| "You need to select at least %(min)s items of the product %(product)s."
#, python-format
msgid "You need to select at least %(min)s item of the product %(product)s."
msgid_plural ""
"You need to select at least %(min)s items of the product %(product)s."
msgstr[0] "U moet ten minste %(min)s items van product %(product)s kiezen."
msgstr[0] "U moet ten minste %(min)s item van product %(product)s kiezen."
msgstr[1] "U moet ten minste %(min)s items van product %(product)s kiezen."
#: pretix/base/services/cart.py:128
#, fuzzy, python-format
#| msgid ""
#| "We removed %(product)s from your cart as you can not buy less than "
#| "%(min)s items of it."
#, python-format
msgid ""
"We removed %(product)s from your cart as you can not buy less than %(min)s "
"item of it."
@@ -7087,10 +7077,10 @@ msgid_plural ""
"items of it."
msgstr[0] ""
"We hebben %(product)s uit uw winkelwagen verwijderd, omdat u niet minder dan "
"%(min)s ervan kunt kopen."
"%(min)s item ervan kunt kopen."
msgstr[1] ""
"We hebben %(product)s uit uw winkelwagen verwijderd, omdat u niet minder dan "
"%(min)s ervan kunt kopen."
"%(min)s items ervan kunt kopen."
#: pretix/base/services/cart.py:132 pretix/base/services/orders.py:146
#: pretix/presale/templates/pretixpresale/event/index.html:157
@@ -7252,10 +7242,7 @@ msgid "You can not select two variations of the same add-on product."
msgstr "U kunt niet twee varianten van hetzelfde add-on-product selecteren."
#: pretix/base/services/cart.py:185 pretix/base/services/orders.py:184
#, fuzzy, python-format
#| msgid ""
#| "You can select at most %(max)s add-ons from the category %(cat)s for the "
#| "product %(base)s."
#, python-format
msgid ""
"You can select at most %(max)s add-on from the category %(cat)s for the "
"product %(base)s."
@@ -7263,17 +7250,14 @@ msgid_plural ""
"You can select at most %(max)s add-ons from the category %(cat)s for the "
"product %(base)s."
msgstr[0] ""
"U kunt maximaal %(max)s add-ons van de categorie %(cat)s selecteren voor het "
"U kunt maximaal %(max)s add-on van de categorie %(cat)s selecteren voor het "
"product %(base)s."
msgstr[1] ""
"U kunt maximaal %(max)s add-ons van de categorie %(cat)s selecteren voor het "
"product %(base)s."
#: pretix/base/services/cart.py:190 pretix/base/services/orders.py:189
#, fuzzy, python-format
#| msgid ""
#| "You need to select at least %(min)s add-ons from the category %(cat)s for "
#| "the product %(base)s."
#, python-format
msgid ""
"You need to select at least %(min)s add-on from the category %(cat)s for the "
"product %(base)s."
@@ -7281,7 +7265,7 @@ msgid_plural ""
"You need to select at least %(min)s add-ons from the category %(cat)s for "
"the product %(base)s."
msgstr[0] ""
"U moet minimaal %(min)s add-ons van de categorie %(cat)s selecteren voor het "
"U moet minimaal %(min)s add-on van de categorie %(cat)s selecteren voor het "
"product %(base)s."
msgstr[1] ""
"U moet minimaal %(min)s add-ons van de categorie %(cat)s selecteren voor het "
@@ -7446,16 +7430,14 @@ msgid "This ticket has been blocked."
msgstr "Dit ticket werd reeds eenmaal gebruikt."
#: pretix/base/services/checkin.py:781 pretix/base/services/checkin.py:785
#, fuzzy, python-brace-format
#| msgid "Only allowed after {datetime}"
#, python-brace-format
msgid "This ticket is only valid after {datetime}."
msgstr "Alleen toegestaan vanaf {datetime}"
msgstr "Dit ticket is geldig vanaf {datetime}."
#: pretix/base/services/checkin.py:795 pretix/base/services/checkin.py:799
#, fuzzy, python-brace-format
#| msgid "This ticket has already been redeemed."
#, python-brace-format
msgid "This ticket was only valid before {datetime}."
msgstr "Dit ticket is al gebruikt."
msgstr "Dit ticket was geldig vòòr {datetime}."
#: pretix/base/services/checkin.py:830
msgid "This order position has an invalid product for this check-in list."
@@ -7516,14 +7498,14 @@ msgid "Export failed"
msgstr "Geëxporteerde bestanden"
#: pretix/base/services/export.py:206
#, fuzzy
#| msgid "Permission denied"
msgid "Permission denied."
msgstr "Geen toestemming"
msgstr "Geen toestemming."
#: pretix/base/services/export.py:221
msgid "Your exported data exceeded the size limit for scheduled exports."
msgstr ""
"De door u geëxporteerde data overschrijdt de grootte-limiet voor geplande "
"exports."
#: pretix/base/services/invoices.py:103
#, python-brace-format
@@ -7548,11 +7530,10 @@ msgstr ""
"{country}"
#: pretix/base/services/invoices.py:220 pretix/base/services/invoices.py:257
#, fuzzy, python-brace-format
#| msgid "Event location"
#, python-brace-format
msgctxt "invoice"
msgid "Event location: {location}"
msgstr "Evenementlocatie"
msgstr "Evenementlocatie: {location}"
#: pretix/base/services/invoices.py:236
#, python-brace-format
@@ -7766,10 +7747,7 @@ msgid "Your cart is empty."
msgstr "Uw winkelwagen is leeg."
#: pretix/base/services/orders.py:138
#, fuzzy, python-format
#| msgid ""
#| "You cannot select more than %(max)s items of the product %(product)s. We "
#| "removed the surplus items from your cart."
#, python-format
msgid ""
"You cannot select more than %(max)s item of the product %(product)s. We "
"removed the surplus items from your cart."
@@ -7777,11 +7755,11 @@ msgid_plural ""
"You cannot select more than %(max)s items of the product %(product)s. We "
"removed the surplus items from your cart."
msgstr[0] ""
"U kunt niet meer dan %(max)s kopieën van het product %(product)s kiezen. We "
"hebben het overschot uit uw winkelwagen verwijderd."
"U kunt van het product %(product)s niet meer dan %(max)s per bestelling "
"kiezen. We hebben de overtallige producten uit uw winkelwagen verwijderd."
msgstr[1] ""
"U kunt niet meer dan %(max)s kopieën van het product %(product)s kiezen. We "
"hebben het overschot uit uw winkelwagen verwijderd."
"U kunt van het product %(product)s niet meer dan %(max)s per bestelling "
"kiezen. We hebben de overtallige producten uit uw winkelwagen verwijderd."
#: pretix/base/services/orders.py:147
msgid "The booking period has ended."
@@ -7831,10 +7809,9 @@ msgstr ""
"niet geldig voor dit item. We hebben dit item uit uw winkelwagen verwijderd."
#: pretix/base/services/orders.py:168
#, fuzzy
#| msgid "You need a valid voucher code to order this product."
msgid "You need a valid voucher code to order one of the products."
msgstr "U heeft een geldige vouchercode nodig om dit product te bestellen."
msgstr ""
"U heeft een geldige vouchercode nodig om een van de producten te bestellen."
#: pretix/base/services/orders.py:170
msgid ""
@@ -7873,10 +7850,8 @@ msgstr ""
"is besteld."
#: pretix/base/services/orders.py:210
#, fuzzy
#| msgid "The order has been canceled."
msgid "The order was not canceled."
msgstr "De bestelling is geannuleerd."
msgstr "De bestelling is niet geannuleerd."
#: pretix/base/services/orders.py:265 pretix/control/forms/orders.py:120
msgid "The new expiry date needs to be in the future."
@@ -7912,10 +7887,8 @@ msgstr ""
"bestelling is betaald."
#: pretix/base/services/orders.py:918
#, fuzzy
#| msgid "This payment method does not support automatic refunds."
msgid "The selected payment methods do not cover the total balance."
msgstr "Deze betalingsmethode ondersteunt geen automatische terugbetalingen."
msgstr "Deze betalingsmethode dekt het volledige bedrag niet."
#: pretix/base/services/orders.py:990
msgid ""
@@ -8070,10 +8043,8 @@ msgid "Something happened in your event after the export, please try again."
msgstr "Er is iets gebeurd in uw evenement na de export, probeer het opnieuw."
#: pretix/base/services/shredder.py:177
#, fuzzy
#| msgid "Payment completed."
msgid "Data shredding completed"
msgstr "Betaling voltooid."
msgstr "Verwijderen van data voltooid."
#: pretix/base/services/stats.py:210
msgid "Uncategorized"
@@ -10100,19 +10071,7 @@ msgid "Your order is pending payment: {code}"
msgstr "Uw bestelling wacht op betaling: {code}"
#: pretix/base/settings.py:2316
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "we did not yet receive a full payment for your order for {event}.\n"
#| "Please keep in mind that we only guarantee your order if we receive\n"
#| "your payment before {expire_date}.\n"
#| "\n"
#| "You can view the payment information and the status of your order at\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
#, python-brace-format
msgid ""
"Hello,\n"
"\n"
@@ -10135,7 +10094,7 @@ msgstr ""
"U kunt de betalingsinformatie en de status van uw bestelling inzien op\n"
"{url}.\n"
"\n"
"Met vriendelijke groet,\n"
"Met vriendelijke groet, \n"
"De organisatoren van {event}"
#: pretix/base/settings.py:2329
@@ -10145,19 +10104,7 @@ msgid "Incomplete payment received: {code}"
msgstr "Betaling ontvangen voor uw bestelling: {code}"
#: pretix/base/settings.py:2333
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "we did not yet receive a full payment for your order for {event}.\n"
#| "Please keep in mind that we only guarantee your order if we receive\n"
#| "your payment before {expire_date}.\n"
#| "\n"
#| "You can view the payment information and the status of your order at\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
#, python-brace-format
msgid ""
"Hello,\n"
"\n"
@@ -10175,10 +10122,11 @@ msgid ""
msgstr ""
"Hallo,\n"
"\n"
"We hebben nog geen volledige betaling ontvangen voor uw bestelling voor "
"{event}.\n"
"We kunnen uw bestelling alleen garanderen als we uw betaling ontvangen\n"
"voor {expire_date}.\n"
"We hebben een betaling ontvangen voor {event}\n"
"\n"
"Helaas is het ontvangen bedrag minder dan het volledige verschuldigde "
"bedrag. Graag nog het bedrag van **{pending_sum}** voldoen om de bestelling "
"te voltooien.\n"
"\n"
"U kunt de betalingsinformatie en de status van uw bestelling inzien op\n"
"{url}.\n"
@@ -10367,17 +10315,7 @@ msgstr ""
"Organisatie van {event}"
#: pretix/base/settings.py:2446 pretix/base/settings.py:2483
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello {attendee_name},\n"
#| "\n"
#| "a ticket for {event} has been ordered for you.\n"
#| "\n"
#| "You can view the details and status of your ticket here:\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
#, python-brace-format
msgid ""
"Hello,\n"
"\n"
@@ -10389,9 +10327,9 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"Beste {attendee_name},\n"
"Beste,\n"
"\n"
"Er is een ticket voor {event} voor u besteld.\n"
"Uw ticket voor {event} is geaccordeerd.\n"
"\n"
"U kunt de details en status van uw ticket hier bekijken:\n"
"{url}\n"
@@ -17295,10 +17233,8 @@ msgid "Valid check-in"
msgstr "Alle check-ins"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:67
#, fuzzy
#| msgid "Additional information"
msgid "Additional information required"
msgstr "Extra informatie"
msgstr "Extra informatie vereist"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:69
msgid ""
+4 -4
View File
@@ -7,8 +7,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
"PO-Revision-Date: 2021-10-29 02:00+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"PO-Revision-Date: 2023-08-24 04:00+0000\n"
"Last-Translator: Alain <alain@waag.org>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
"nl/>\n"
"Language: nl\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.8\n"
"X-Generator: Weblate 4.18.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -252,7 +252,7 @@ msgstr "Dit ticket is nog niet betaald. Wilt u toch doorgaan?"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
msgid "Additional information required"
msgstr "Extra informatie nodig"
msgstr "Extra informatie vereist"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket"
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
"PO-Revision-Date: 2023-07-16 22:00+0000\n"
"Last-Translator: Freek Engelbarts <freekengelbarts@gmail.com>\n"
"PO-Revision-Date: 2023-08-24 04:00+0000\n"
"Last-Translator: Alain <alain@waag.org>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_Informal/>\n"
"Language: nl_Informal\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
"X-Generator: Weblate 4.18.2\n"
#: pretix/_base_settings.py:78
msgid "English"
@@ -555,10 +555,8 @@ msgid "Waiting list entry deleted"
msgstr "Wachtlijstitem"
#: pretix/api/webhooks.py:351
#, fuzzy
#| msgid "Waiting list entries"
msgid "Waiting list entry received voucher"
msgstr "Wachtlijstitems"
msgstr "Wachtlijstitem heeft voucher ontvangen"
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:938
+42 -116
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
"PO-Revision-Date: 2023-06-28 06:00+0000\n"
"Last-Translator: Yucheng Lin <yuchenglinedu@gmail.com>\n"
"PO-Revision-Date: 2023-08-30 07:00+0000\n"
"Last-Translator: Ash So <ashs@vankaifong.com>\n"
"Language-Team: Chinese (Traditional) <https://translate.pretix.eu/projects/"
"pretix/pretix/zh_Hant/>\n"
"Language: zh_Hant\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.17\n"
"X-Generator: Weblate 4.18.2\n"
#: pretix/_base_settings.py:78
msgid "English"
@@ -5876,24 +5876,18 @@ msgid "Ambiguous option selected."
msgstr "選擇不明確的選項。"
#: pretix/base/orderimport.py:845
#, fuzzy
#| msgid "No matching seat was found."
msgid "No matching customer was found."
msgstr "未找到符合的座位。"
msgstr "未找到符合的客戶。"
#: pretix/base/payment.py:86
#, fuzzy
#| msgid "Apply"
msgctxt "payment"
msgid "Apple Pay"
msgstr "應用"
msgstr "Apple Pay"
#: pretix/base/payment.py:87
#, fuzzy
#| msgid "Android (Google Play)"
msgctxt "payment"
msgid "Google Pay"
msgstr "安卓(Google Play"
msgstr "安卓(Google Pay"
#: pretix/base/payment.py:256
#: pretix/presale/templates/pretixpresale/event/order.html:115
@@ -6424,16 +6418,12 @@ msgid "List of Add-Ons"
msgstr "附加組件清單"
#: pretix/base/pdf.py:364
#, fuzzy
#| msgid ""
#| "Add-on 1\n"
#| "Add-on 2"
msgid ""
"Add-on 1\n"
"2x Add-on 2"
msgstr ""
"附加1\n"
"附加2"
"2x附加2"
#: pretix/base/pdf.py:370 pretix/control/forms/filter.py:1275
#: pretix/control/forms/filter.py:1277
@@ -9410,25 +9400,12 @@ msgstr ""
"你的{event} 團隊"
#: pretix/base/settings.py:2349
#, fuzzy, python-brace-format
#| msgid "Payment received for your order: {code}"
#, python-brace-format
msgid "Payment failed for your order: {code}"
msgstr "收到的訂單付款:{code}"
msgstr "訂單付款失敗{code}"
#: pretix/base/settings.py:2353
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "we did not yet receive a full payment for your order for {event}.\n"
#| "Please keep in mind that we only guarantee your order if we receive\n"
#| "your payment before {expire_date}.\n"
#| "\n"
#| "You can view the payment information and the status of your order at\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
#, python-brace-format
msgid ""
"Hello,\n"
"\n"
@@ -9446,11 +9423,12 @@ msgid ""
msgstr ""
"你好\n"
"\n"
"我們尚未收到你的 {event} 訂單的全額付款。\n"
"請記住,我們僅在收到時保證你的訂單\n"
"你在 {expire_date} 之前的付款。\n"
"你在 {event} 訂單付款未能成功。\n"
"\n"
"你可以在以下位置查看付款資訊與訂單狀態:\n"
"您的訂單仍然有效,您可以嘗試使用相同或不同的付款方式再次進行支付,唯請在 "
"{expire_date} 前完成付款程序。\n"
"\n"
"您可以重新嘗試付款,並在以下網址檢視您的訂單狀態:\n"
"{url}\n"
"\n"
"敬此\n"
@@ -13568,7 +13546,7 @@ msgstr "已為位置 #{posid} 生成一個新密鑰。"
#, python-brace-format
msgid ""
"The validity start date for position #{posid} has been changed to {value}."
msgstr "位置 #{posid} 的有效開始日期已更改為{value}"
msgstr "位置 #{posid} 的有效開始日期已更改為{value}"
#: pretix/control/logdisplay.py:171
#, python-brace-format
@@ -13846,10 +13824,8 @@ msgid "The medium has been connected to a new ticket."
msgstr "媒體已連接到新票證。"
#: pretix/control/logdisplay.py:371
#, fuzzy
#| msgid "The medium has been connected to a new ticket."
msgid "The medium has been connected to a new gift card."
msgstr "媒體已連接到新票證。"
msgstr "媒體已連接到新的禮品卡。"
#: pretix/control/logdisplay.py:372 pretix/control/logdisplay.py:413
msgid "Sending of an email has failed."
@@ -14085,11 +14061,8 @@ msgid ""
msgstr "包含訂單詳細資訊頁面連結的電子郵件已重新發送給使用者。"
#: pretix/control/logdisplay.py:436
#, fuzzy
#| msgid ""
#| "An email has been sent to notify the user that payment has been received."
msgid "An email has been sent to notify the user that the payment failed."
msgstr "已發送一封電子郵件通知使用者已收到付款。"
msgstr "已發送一封電子郵件通知使用者未能成功付款。"
#: pretix/control/logdisplay.py:437
#, python-brace-format
@@ -16788,10 +16761,8 @@ msgid "Payment reminder"
msgstr "付款提醒"
#: pretix/control/templates/pretixcontrol/event/mail.html:108
#, fuzzy
#| msgid "Payment fee"
msgid "Payment failed"
msgstr "支付費用"
msgstr "支付失敗"
#: pretix/control/templates/pretixcontrol/event/mail.html:111
msgid "Waiting list notification"
@@ -16845,8 +16816,6 @@ msgid "Deadlines"
msgstr "期限"
#: pretix/control/templates/pretixcontrol/event/payment.html:68
#, fuzzy
#| msgid "days"
msgctxt "unit"
msgid "days"
msgstr "日"
@@ -21981,11 +21950,11 @@ msgstr "兩步驟狀態"
#: pretix/control/templates/pretixcontrol/user/2fa_main.html:44
msgid "Two-factor authentication is currently enabled."
msgstr "兩步驟驗證目前啟用"
msgstr "兩步驟驗證目前啟用"
#: pretix/control/templates/pretixcontrol/user/2fa_main.html:60
msgid "Two-factor authentication is currently disabled."
msgstr "兩步驟驗證目前停用"
msgstr "兩步驟驗證目前停用"
#: pretix/control/templates/pretixcontrol/user/2fa_main.html:63
msgid "To enable it, you need to configure at least one device below."
@@ -22529,20 +22498,14 @@ msgstr "此條目的優先順序已修改。此數字越高,此人將越早獲
msgid ""
"For safety reasons, the waiting list does not run if the quota is set to "
"unlimited."
msgstr ""
msgstr "出於安全考慮,如果額度設定為無限制,將不設等候名單。"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:219
#, fuzzy
#| msgid "Quota name"
msgid "Quota unlimited"
msgstr "額度名稱"
msgstr "無限額度"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:225
#, fuzzy, python-format
#| msgid ""
#| "\n"
#| " Waiting, product %(num)sx available\n"
#| " "
#, python-format
msgid ""
"\n"
" Waiting, product %(num)sx "
@@ -22550,8 +22513,8 @@ msgid ""
" "
msgstr ""
"\n"
" 等待中,產品 %(num)s可用\n"
" "
" 等待中,產品 %(num)s可用\n"
" "
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:231
msgid "Waiting, product unavailable"
@@ -23678,7 +23641,7 @@ msgstr "訂單已更改,使用者已收到通知。"
#: pretix/control/views/orders.py:1828 pretix/control/views/orders.py:1962
#: pretix/control/views/orders.py:1999 pretix/presale/views/order.py:1538
msgid "The order has been changed."
msgstr "訂單順序已更改"
msgstr "訂單順序已更改"
#: pretix/control/views/orders.py:1855 pretix/presale/checkoutflow.py:881
#: pretix/presale/views/order.py:799
@@ -25274,7 +25237,7 @@ msgstr "值機清單 PDF"
#: pretix/plugins/checkinlists/exporters.py:648
msgctxt "export_category"
msgid "Check-in"
msgstr "Check-in"
msgstr "簽到"
#: pretix/plugins/checkinlists/exporters.py:286
msgid ""
@@ -26107,9 +26070,6 @@ msgid "Restrict to event dates starting before"
msgstr "限制為早於之前開始的活動日期"
#: pretix/plugins/sendmail/forms.py:170
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Send to"
msgctxt "sendmail_form"
msgid "Send to"
msgstr "傳送到"
@@ -26124,9 +26084,6 @@ msgid "Filter check-in status"
msgstr "篩選簽到狀態"
#: pretix/plugins/sendmail/forms.py:189
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Restrict to recipients without check-in"
msgctxt "sendmail_form"
msgid "Restrict to recipients without check-in"
msgstr "僅限未簽到的收件者"
@@ -26176,17 +26133,11 @@ msgid "pending with payment overdue"
msgstr "待處理,付款逾期"
#: pretix/plugins/sendmail/forms.py:258
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Restrict to orders with status"
msgctxt "sendmail_form"
msgid "Restrict to orders with status"
msgstr "限制為具有狀態的訂單"
msgstr "限具有狀態的訂單"
#: pretix/plugins/sendmail/forms.py:283 pretix/plugins/sendmail/forms.py:287
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Restrict to recipients with check-in on list"
msgctxt "sendmail_form"
msgid "Restrict to recipients with check-in on list"
msgstr "僅限在清單中簽到的收件者"
@@ -26257,11 +26208,8 @@ msgid "Limit products"
msgstr "限制商品"
#: pretix/plugins/sendmail/models.py:218
#, fuzzy
#| msgctxt "sendmail_from"
#| msgid "Restrict to orders with status"
msgid "Restrict to orders with status"
msgstr "限制為具有狀態的訂單"
msgstr "限具有狀態的訂單"
#: pretix/plugins/sendmail/models.py:228
msgid "Send date"
@@ -26840,10 +26788,8 @@ msgid "Bancontact"
msgstr "Bancontact"
#: pretix/plugins/stripe/payment.py:357
#, fuzzy
#| msgid "Disable SEPA Direct Debit"
msgid "SEPA Direct Debit"
msgstr "禁用 SEPA 直接扣款"
msgstr "SEPA 直接扣款"
#: pretix/plugins/stripe/payment.py:362
#, fuzzy
@@ -26975,26 +26921,20 @@ msgid "Credit card"
msgstr "信用卡"
#: pretix/plugins/stripe/payment.py:1157
#, fuzzy
#| msgid "EPS via Stripe"
msgid "SEPA Debit via Stripe"
msgstr "EPS透過Stripe"
msgstr "透過Stripe進行SEPA直接扣款"
#: pretix/plugins/stripe/payment.py:1158
msgid "SEPA Debit"
msgstr ""
msgstr "SEPA扣款"
#: pretix/plugins/stripe/payment.py:1197
#, fuzzy
#| msgid "Account holder"
msgid "Account Holder Name"
msgstr "帳戶持有人"
msgstr "帳戶持有人名稱"
#: pretix/plugins/stripe/payment.py:1202
#, fuzzy
#| msgid "Account holder"
msgid "Account Holder Street"
msgstr "帳戶持有人"
msgstr "帳戶持有人街道"
#: pretix/plugins/stripe/payment.py:1214
#, fuzzy
@@ -27003,16 +26943,12 @@ msgid "Account Holder Postal Code"
msgstr "帳戶持有人"
#: pretix/plugins/stripe/payment.py:1226
#, fuzzy
#| msgid "Account holder"
msgid "Account Holder City"
msgstr "帳戶持有人"
msgstr "帳戶持有人城市"
#: pretix/plugins/stripe/payment.py:1238
#, fuzzy
#| msgid "Account holder"
msgid "Account Holder Country"
msgstr "帳戶持有人"
msgstr "帳戶持有人國家"
#: pretix/plugins/stripe/payment.py:1282
msgid "giropay via Stripe"
@@ -27183,10 +27119,8 @@ msgid "Card type"
msgstr "卡片類型"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:14
#, fuzzy
#| msgid "The total amount will be withdrawn from your credit card."
msgid "The total amount will be withdrawn from your bank account."
msgstr "總金額將從你的信用卡中提取。"
msgstr "總金額將從你的銀行戶口中提取。"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:18
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:20
@@ -27195,10 +27129,8 @@ msgstr ""
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:20
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:22
#, fuzzy
#| msgid "Account holder"
msgid "Account number"
msgstr "帳戶持有人"
msgstr "帳戶號碼"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:24
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_simple.html:4
@@ -27246,20 +27178,14 @@ msgid "For a SEPA Debit payment, please turn on JavaScript."
msgstr "對於信用卡付款,請打開JavaScript。"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:16
#, fuzzy
#| msgid ""
#| "You already entered a card number that we will use to charge the payment "
#| "amount."
msgid ""
"You already entered a bank account that we will use to charge the payment "
"amount."
msgstr "已經輸入了一個卡號,我們將使用該卡號來收取付款金額。"
msgstr "已經輸入了一個我們將用來扣除支付金額的銀行帳戶。"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:27
#, fuzzy
#| msgid "Use a different card"
msgid "Use a different account"
msgstr "使用不同卡片"
msgstr "使用不同帳戶"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html:51
#, python-format
@@ -27539,7 +27465,7 @@ msgstr "網上check-in"
#: pretix/plugins/webcheckin/templates/pretixplugins/webcheckin/index.html:10
msgid "Check-in"
msgstr "Check-in"
msgstr "簽到"
#: pretix/presale/checkoutflow.py:107
msgctxt "checkoutflow"
+14 -1
View File
@@ -77,6 +77,8 @@ def get_event_domain(event, fallback=False, return_info=False):
def get_organizer_domain(organizer):
assert isinstance(organizer, Organizer)
if not organizer.pk:
return None
domain = getattr(organizer, '_cached_domain', None) or organizer.cache.get('domain')
if domain is None:
domains = organizer.domains.filter(event__isnull=True)
@@ -126,7 +128,7 @@ def eventreverse(obj, name, kwargs=None):
:param kwargs: A dictionary of additional keyword arguments that should be used. You do not
need to provide the organizer or event slug here, it will be added automatically as
needed.
:returns: An absolute URL (including scheme and host) as a string
:returns: An absolute or relative URL as a string
"""
from pretix.multidomain import (
event_domain_urlconf, maindomain_urlconf, organizer_domain_urlconf,
@@ -175,6 +177,17 @@ def eventreverse(obj, name, kwargs=None):
def build_absolute_uri(obj, urlname, kwargs=None):
"""
Works similar to ``eventreverse`` but always returns an absolute URL.
:param obj: An ``Event`` or ``Organizer`` object
:param name: The name of the URL route
:type name: str
:param kwargs: A dictionary of additional keyword arguments that should be used. You do not
need to provide the organizer or event slug here, it will be added automatically as
needed.
:returns: An absolute URL (including scheme and host) as a string
"""
reversedurl = eventreverse(obj, urlname, kwargs)
if '://' in reversedurl:
return reversedurl
+3 -1
View File
@@ -535,9 +535,11 @@ class BankTransfer(BasePaymentProvider):
'eu_barcodes': self.event.currency == 'EUR',
'pending_description': self.settings.get('pending_description', as_type=LazyI18nString),
'details': self.settings.get('bank_details', as_type=LazyI18nString),
'has_invoices': payment.order.invoices.exists(),
'invoice_email_enabled': self.settings.get('invoice_email', as_type=bool),
}
ctx['any_barcodes'] = ctx['swiss_qrbill'] or ctx['eu_barcodes']
return template.render(ctx)
return template.render(ctx, request=request)
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
warning = None
@@ -11,10 +11,10 @@
<span class="icon icon-upload"></span> {% trans "Continue" %}
</button>
<div class="flipped-scroll-wrapper clearfix">
<table class="table table-condensed flipped-scroll-inner">
<table class="table table-condensed table-th-sticky-horizontal flipped-scroll-inner">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th scope="row">{% trans "Date" %}</th>
{% for col in rows.0 %}
<th>
<input type="radio" name="date" value="{{ forloop.counter0 }}"/>
@@ -22,7 +22,7 @@
{% endfor %}
</tr>
<tr>
<th>{% trans "Amount" %}</th>
<th scope="row">{% trans "Amount" %}</th>
{% for col in rows.0 %}
<th>
<input type="radio" name="amount" value="{{ forloop.counter0 }}" required="required"/>
@@ -30,7 +30,7 @@
{% endfor %}
</tr>
<tr>
<th>{% trans "Reference" %}</th>
<th scope="row">{% trans "Reference" %}</th>
{% for col in rows.0 %}
<th>
<input type="checkbox" name="reference" value="{{ forloop.counter0 }}"/>
@@ -38,7 +38,7 @@
{% endfor %}
</tr>
<tr>
<th>{% trans "Payer" %}</th>
<th scope="row">{% trans "Payer" %}</th>
{% for col in rows.0 %}
<th>
<input type="checkbox" name="payer" value="{{ forloop.counter0 }}"/>
@@ -46,7 +46,7 @@
{% endfor %}
</tr>
<tr>
<th>
<th scope="row">
{% trans "IBAN" %}
<label for="id_iban_clear">
<span class="btn btn-default btn-sm fa fa-close"></span>
@@ -62,7 +62,7 @@
{% endfor %}
</tr>
<tr>
<th>
<th scope="row">
{% trans "BIC" %}
<label for="id_bic_clear">
<span class="btn btn-default btn-sm fa fa-close"></span>
@@ -7,6 +7,7 @@
{% load money %}
{% load unidecode %}
{% load rich_text %}
{% load eventurl %}
{% if pending_description %}
{{ pending_description|rich_text }}
@@ -103,3 +104,28 @@ SCT
{% if swiss_qrbill %}
<link rel="stylesheet" href="{% static "pretixplugins/banktransfer/swisscross.css" %}">
{% endif %}
{% if invoice_email_enabled and has_invoices %}
<form method="post" action="{% eventurl event "plugins:banktransfer:mail_invoice" order=order.code secret=order.secret %}">
{% csrf_token %}
<p>
{% blocktrans trimmed %}
To send the invoice directly to your accounting department, please enter their email address:
{% endblocktrans %}
</p>
<div class="row">
<div class="col-md-9 col-xs-12">
<label for="mail_invoice_email" class="sr-only">{% trans "Invoice recipient email" %}:</label>
<input type="email" name="email" id="mail_invoice_email" class="form-control" value="" required
placeholder="{% trans "Email address" %}" />
</div>
<div class="col-md-3 col-xs-12">
<button class="btn btn-default btn-block">
<span class="fa fa-envelope-o" aria-hidden="true"></span>
{% trans "Send invoice via email" %}
</button>
</div>
</div>
</form>
<hr>
{% endif %}
+7 -1
View File
@@ -19,13 +19,19 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.urls import re_path
from django.urls import include, re_path
from pretix.api.urls import orga_router
from pretix.plugins.banktransfer.api import BankImportJobViewSet
from . import views
event_patterns = [
re_path(r'^banktransfer/', include([
re_path(r'^(?P<order>[^/][^w]+)/(?P<secret>[A-Za-z0-9]+)/mail-invoice/$', views.SendInvoiceMailView.as_view(), name='mail_invoice'),
])),
]
urlpatterns = [
re_path(r'^control/organizer/(?P<organizer>[^/]+)/banktransfer/import/',
views.OrganizerImportView.as_view(),
+40 -1
View File
@@ -44,14 +44,18 @@ from typing import Set
from django import forms
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.db import transaction
from django.db.models import Count, Q, QuerySet
from django.http import FileResponse, JsonResponse
from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import DetailView, FormView, ListView, View
from django.views.generic.detail import SingleObjectMixin
from localflavor.generic.forms import BICFormField, IBANFormField
@@ -75,6 +79,8 @@ from pretix.plugins.banktransfer.refund_export import (
build_sepa_xml, get_refund_export_csv,
)
from pretix.plugins.banktransfer.tasks import process_banktransfers
from pretix.presale.views import EventViewMixin
from pretix.presale.views.order import OrderDetailMixin
logger = logging.getLogger('pretix.plugins.banktransfer')
@@ -886,3 +892,36 @@ class OrganizerSepaXMLExportView(OrganizerPermissionRequiredMixin, OrganizerDeta
organizer=self.request.organizer,
pk=self.kwargs.get('id')
)
@method_decorator(xframe_options_exempt, 'dispatch')
class SendInvoiceMailView(EventViewMixin, OrderDetailMixin, View):
def post(self, request, *args, **kwargs):
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
try:
validate_email(request.POST['email'])
except ValidationError:
messages.error(request, _('Please enter a valid email address.'))
return redirect(self.get_order_url())
last_payment = self.order.payments.last()
if (not last_payment
or last_payment.provider != BankTransfer.identifier
or last_payment.state != OrderPayment.PAYMENT_STATE_CREATED):
messages.error(request, _('No pending bank transfer payment found. Maybe the order has been paid already?'))
return redirect(self.get_order_url())
if not last_payment.payment_provider.settings.get('invoice_email', as_type=bool):
messages.error(request, _('Sending invoices via email is disabled by the event organizer.'))
return redirect(self.get_order_url())
last_invoice = self.order.invoices.last()
if not last_invoice:
messages.error(request, _('No invoice found, please request an invoice first.'))
return redirect(self.get_order_url())
provider = last_payment.payment_provider
provider.send_invoice_to_alternate_email(self.order, last_invoice, request.POST['email'])
messages.success(request, _('Sending the latest invoice via e-mail to {email}.').format(email=request.POST['email']))
return redirect(self.get_order_url())
+19 -1
View File
@@ -607,6 +607,12 @@ class PaypalMethod(BasePaymentProvider):
response = self.client.execute(req)
except IOError as e:
logger.exception('PayPal OrdersGetRequest: {}'.format(str(e)))
payment.fail(info={
"error": {
"name": "IOError",
"message": str(e),
}
})
raise PaymentException(_('We had trouble communicating with PayPal'))
else:
pp_captured_order = response.result
@@ -615,9 +621,15 @@ class PaypalMethod(BasePaymentProvider):
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_captured_order.id)
except ReferencedPayPalObject.MultipleObjectsReturned:
pass
if str(pp_captured_order.purchase_units[0].amount.value) != str(payment.amount) or \
if Decimal(pp_captured_order.purchase_units[0].amount.value) != payment.amount or \
pp_captured_order.purchase_units[0].amount.currency_code != self.event.currency:
logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_captured_order.dict())))
payment.fail(info={
"error": {
"name": "ValidationError",
"message": "Value mismatch",
}
})
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
'proceed.'))
@@ -660,6 +672,12 @@ class PaypalMethod(BasePaymentProvider):
self.client.execute(patchreq)
except IOError as e:
messages.error(request, _('We had trouble communicating with PayPal'))
payment.fail(info={
"error": {
"name": "IOError",
"message": str(e),
}
})
logger.exception('PayPal OrdersPatchRequest: {}'.format(str(e)))
return
+5 -1
View File
@@ -34,7 +34,7 @@ class RuleSerializer(I18nAwareModelSerializer):
class Meta:
model = Rule
fields = ['id', 'subject', 'template', 'all_products', 'limit_products', 'restrict_to_status',
'send_date', 'send_offset_days', 'send_offset_time', 'date_is_absolute',
'checked_in_status', 'send_date', 'send_offset_days', 'send_offset_time', 'date_is_absolute',
'offset_to_event_end', 'offset_is_after', 'send_to', 'enabled']
read_only_fields = ['id']
@@ -88,6 +88,10 @@ class RuleSerializer(I18nAwareModelSerializer):
]:
raise ValidationError(f'status {s} not allowed: restrict_to_status may only include valid states')
if full_data.get('checked_in_status') == "":
# even though "blank" is not allowed on this field, "" gets accepted without this check
raise ValidationError('empty string not allowed: use null to disable check-in based filtering')
return full_data
def save(self, **kwargs):
+2 -1
View File
@@ -312,7 +312,7 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
fields = ['subject', 'template', 'attach_ical',
'send_date', 'send_offset_days', 'send_offset_time',
'all_products', 'limit_products', 'restrict_to_status',
'send_to', 'enabled']
'checked_in_status', 'send_to', 'enabled']
field_classes = {
'subevent': SafeModelMultipleChoiceField,
@@ -337,6 +337,7 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
'data-inverse-dependency': '#id_all_products'},
),
'send_to': forms.RadioSelect,
'checked_in_status': forms.RadioSelect,
}
def __init__(self, *args, **kwargs):
@@ -0,0 +1,18 @@
# Generated by Django 3.2.19 on 2023-08-09 11:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sendmail', '0004_rule_restrict_to_status'),
]
operations = [
migrations.AddField(
model_name='rule',
name='checked_in_status',
field=models.CharField(max_length=10, null=True),
),
]
+37 -8
View File
@@ -34,7 +34,8 @@ from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent, fields,
Checkin, Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent,
fields,
)
from pretix.base.models.base import LoggingMixin
from pretix.base.services.mail import SendMailException
@@ -112,19 +113,30 @@ class ScheduledMail(models.Model):
e = self.event
orders = e.orders.all()
limit_products = self.rule.limit_products.values_list('pk', flat=True) if not self.rule.all_products else None
filter_orders_by_op = False
op_qs = OrderPosition.objects.filter(
order__event=self.event,
canceled=False,
)
if self.subevent:
orders = orders.filter(
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), subevent=self.subevent))
)
filter_orders_by_op = True
op_qs = op_qs.filter(subevent=self.subevent)
elif e.has_subevents:
return # This rule should not even exist
if not self.rule.all_products:
orders = orders.filter(
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), item_id__in=limit_products))
)
filter_orders_by_op = True
limit_products = self.rule.limit_products.values_list('pk', flat=True)
op_qs = op_qs.filter(item_id__in=limit_products)
if self.rule.checked_in_status == "no_checkin":
filter_orders_by_op = True
op_qs = op_qs.filter(~Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
elif self.rule.checked_in_status == "checked_in":
filter_orders_by_op = True
op_qs = op_qs.filter(Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
status_q = Q(status__in=self.rule.restrict_to_status)
if 'n__pending_approval' in self.rule.restrict_to_status:
@@ -142,6 +154,8 @@ class ScheduledMail(models.Model):
pk__gt=self.last_successful_order_id
)
if filter_orders_by_op:
orders = orders.filter(pk__in=op_qs.values_list('order_id', flat=True))
orders = orders.filter(
status_q,
).order_by('pk').select_related('invoice_address').prefetch_related('positions')
@@ -205,6 +219,12 @@ class Rule(models.Model, LoggingMixin):
(BOTH, _('Both (all order contact addresses and all attendee email addresses)'))
]
CHECK_IN_STATUS_CHOICES = [
(None, _("Everyone")),
("checked_in", _("Anyone who is or was checked in")),
("no_checkin", _("Anyone who never checked in before"))
]
id = models.BigAutoField(primary_key=True)
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='sendmail_rules')
@@ -219,6 +239,15 @@ class Rule(models.Model, LoggingMixin):
default=['p', 'n__valid_if_pending'],
)
checked_in_status = models.CharField(
verbose_name=_("Restrict to check-in status"),
default=None,
choices=CHECK_IN_STATUS_CHOICES,
max_length=10,
null=True,
blank=True,
)
attach_ical = models.BooleanField(
default=False,
verbose_name=_("Attach calendar files"),
@@ -28,6 +28,8 @@
<legend>{% trans "Recipients" %}</legend>
{% bootstrap_field form.send_to layout='control' %}
{% bootstrap_field form.restrict_to_status layout='control' %}
{% bootstrap_field form.checked_in_status layout='control' %}
<hr>
{% bootstrap_field form.all_products layout='control' %}
{% bootstrap_field form.limit_products layout='horizontal' %}
</fieldset>
@@ -42,6 +42,8 @@
<legend>{% trans "Recipients" %}</legend>
{% bootstrap_field form.send_to layout='control' %}
{% bootstrap_field form.restrict_to_status layout='control' %}
{% bootstrap_field form.checked_in_status layout='control' %}
<hr>
{% bootstrap_field form.all_products layout='control' %}
{% bootstrap_field form.limit_products layout='horizontal' %}
</fieldset>
+56 -3
View File
@@ -39,10 +39,13 @@ from decimal import Decimal
from django.conf import settings
from django.contrib import messages
from django.core.cache import caches
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.signing import BadSignature, loads
from django.core.validators import EmailValidator
from django.db.models import F, Q
from django.db import models
from django.db.models import Count, F, Q, Sum
from django.db.models.functions import Cast
from django.http import HttpResponseNotAllowed, JsonResponse
from django.shortcuts import redirect
from django.utils import translation
@@ -62,12 +65,14 @@ from pretix.base.services.cart import (
)
from pretix.base.services.memberships import validate_memberships_in_order
from pretix.base.services.orders import perform_order
from pretix.base.services.tasks import EventTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import validate_cart_addons
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format
from pretix.base.templatetags.rich_text import rich_text_snippet
from pretix.base.views.tasks import AsyncAction
from pretix.celery_app import app
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.checkout import (
ContactForm, InvoiceAddressForm, InvoiceNameForm, MembershipForm,
@@ -802,7 +807,9 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
@cached_property
def invoice_form(self):
wd = self.cart_session.get('widget_data', {})
if not self.invoice_address.pk:
if self.invoice_address.pk:
wd_initial = {}
elif wd:
wd_initial = {
'name_parts': {
k[21:].replace('-', '_'): v
@@ -817,7 +824,9 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
'country': wd.get('invoice-address-country', ''),
}
else:
wd_initial = {}
wd_initial = {
'is_business': self._get_is_business_heuristic(),
}
initial = dict(wd_initial)
if self.cart_customer:
@@ -1026,6 +1035,25 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
ctx['cart_session'] = self.cart_session
ctx['invoice_address_asked'] = self.address_asked
def reduce_initial(v):
if isinstance(v, dict):
# try to flatten objects such as name_parts to a single string to determine whether they have any value set
return ''.join([v for k, v in v.items() if not k.startswith('_') and v])
else:
return v
def is_form_filled(form, ignore_keys=()):
return any([reduce_initial(v) for k, v in form.initial.items() if k not in ignore_keys])
ctx['invoice_address_open'] = (
self.request.event.settings.invoice_address_required or
self.request.event.settings.invoice_name_required or
'invoice' in self.request.GET or
# Checking for self.invoice_address.pk is not enough as when an invoice_address has been added and later edited to be empty, its not None.
# So check initial values as invoice_form can receive pre-filled values from invoice_address, widget-data or overwrites from plug-ins.
is_form_filled(self.invoice_form, ignore_keys=('is_business', 'country'))
)
if self.cart_customer:
if self.address_asked:
addresses = self.cart_customer.stored_addresses.all()
@@ -1114,6 +1142,31 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
ctx['profiles_data'] = profiles_list
return ctx
def _get_is_business_heuristic(self):
key = 'checkout_heuristic_is_business:' + str(self.event.pk)
cached_result = caches['default'].get(key)
if cached_result is None:
if caches['default'].add(key, False, timeout=10): # return False while query is running
QuestionsStep._update_is_business_heuristic.apply_async(args=(self.event.pk,))
return False
else:
return cached_result
@staticmethod
@app.task(base=EventTask)
def _update_is_business_heuristic(event):
result = InvoiceAddress.objects.filter(order__event=event).aggregate(
total=Count('*'), business=Sum(Cast('is_business', output_field=models.IntegerField())))
if result['total'] < 100:
result = InvoiceAddress.objects.filter(order__event__organizer=event.organizer).aggregate(
total=Count('*'), business=Sum(Cast('is_business', output_field=models.IntegerField())))
if result['business'] and result['total']:
is_business = result['business'] / result['total'] >= 0.6
else:
is_business = False
key = 'checkout_heuristic_is_business:' + str(event.pk)
caches['default'].set(key, is_business, timeout=12 * 3600) # 12 hours
class PaymentStep(CartMixin, TemplateFlowStep):
priority = 200
@@ -76,7 +76,7 @@
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Invoice information" %}
<a href="{% eventurl request.event "presale:event.checkout" step="questions" cart_namespace=cart_namespace|default_if_none:"" %}?invoice=1" aria-label="{% trans "Modify invoice information" %}" class="h6">
<a href="{% eventurl request.event "presale:event.checkout" step="questions" cart_namespace=cart_namespace|default_if_none:"" %}?invoice=1#invoice-details" aria-label="{% trans "Modify invoice information" %}" class="h6">
<span class="fa fa-edit" aria-hidden="true"></span>{% trans "Modify" %}
</a>
</h3>
@@ -34,7 +34,7 @@
</div>
</details>
{% if invoice_address_asked %}
<details class="panel panel-default" {% if event.settings.invoice_address_required or event.settings.invoice_name_required %}open{% endif %}>
<details class="panel panel-default" {% if invoice_address_open %}open{% endif %} id="invoice-details">
<summary class="panel-heading">
<h3 class="panel-title">
<strong>{% trans "Invoice information" %}{% if not event.settings.invoice_address_required and not event.settings.invoice_name_required %}
@@ -0,0 +1,66 @@
{% load i18n %}
{% load eventurl %}
{% if ev.location and show_location %}
<div class="info-row">
<span class="fa fa-map-marker fa-fw" aria-hidden="true" title="{% trans "Where does the event happen?" %}"></span>
<p><span class="sr-only">{% trans "Where does the event happen?" %}</span>
{{ ev.location|linebreaksbr }}
</p>
</div>
{% endif %}
{% if ev.settings.show_dates_on_frontpage %}
<div class="info-row">
<span class="fa fa-clock-o fa-fw" aria-hidden="true" title="{% trans "When does the event happen?" %}"></span>
<p><span class="sr-only">{% trans "When does the event happen?" %}</span>
{{ ev.get_date_range_display_as_html }}
{% if event.settings.show_times %}
<br>
<span data-time="{{ ev.date_from.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% with time_human=ev.date_from|date:"TIME_FORMAT" time_24=ev.date_from|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
Begin: {{ time }}
{% endblocktrans %}
{% endwith %}
</span>
{% if event.settings.show_date_to and ev.date_to %}
<br>
<span data-time="{{ ev.date_to.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% with time_human=ev.date_to|date:"TIME_FORMAT" time_24=ev.date_to|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
End: {{ time }}
{% endblocktrans %}
{% endwith %}
</span>
{% endif %}
{% endif %}
{% if ev.date_admission %}
<br>
{% if ev.date_admission|date:"SHORT_DATE_FORMAT" == ev.date_from|date:"SHORT_DATE_FORMAT" %}
<span data-time="{{ ev.date_admission.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% with time_human=ev.date_admission|date:"TIME_FORMAT" time_24=ev.date_admission|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
Admission: {{ time }}
{% endblocktrans %}
{% endwith %}
</span>
{% else %}
<span data-time="{{ ev.date_admission.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% with datetime_human=ev.date_admission|date:"SHORT_DATETIME_FORMAT" datetime_iso=ev.date_admission|time:"Y-m-d H:i" %}
{% blocktrans trimmed with datetime='<time datetime="'|add:datetime_iso|add:'">'|add:datetime_human|add:"</time>"|safe %}
Admission: {{ datetime }}
{% endblocktrans %}
{% endwith %}
</span>
{% endif %}
{% endif %}
<br>
{% if subevent %}
<a href="{% eventurl event "presale:event.ical.download" subevent=subevent.pk %}">
{% else %}
<a href="{% eventurl event "presale:event.ical.download" %}">
{% endif %}
{% trans "Add to Calendar" %}
</a>
</p>
</div>
{% endif %}
@@ -162,73 +162,8 @@
{% endif %}
{% if not cart_namespace or subevent %}
<div>
{% if ev.location %}
<div class="info-row">
<span class="fa fa-map-marker fa-fw" aria-hidden="true" title="{% trans "Where does the event happen?" %}"></span>
<p><span class="sr-only">{% trans "Where does the event happen?" %}</span>
{{ ev.location|linebreaksbr }}
</p>
</div>
{% endif %}
{% if ev.settings.show_dates_on_frontpage %}
<div class="info-row">
<span class="fa fa-clock-o fa-fw" aria-hidden="true" title="{% trans "When does the event happen?" %}"></span>
<p><span class="sr-only">{% trans "When does the event happen?" %}</span>
{{ ev.get_date_range_display_as_html }}
{% if event.settings.show_times %}
<br>
<span data-time="{{ ev.date_from.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% with time_human=ev.date_from|date:"TIME_FORMAT" time_24=ev.date_from|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
Begin: {{ time }}
{% endblocktrans %}
{% endwith %}
</span>
{% if event.settings.show_date_to and ev.date_to %}
<br>
<span data-time="{{ ev.date_to.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% with time_human=ev.date_to|date:"TIME_FORMAT" time_24=ev.date_to|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
End: {{ time }}
{% endblocktrans %}
{% endwith %}
</span>
{% endif %}
{% endif %}
{% if ev.date_admission %}
<br>
{% if ev.date_admission|date:"SHORT_DATE_FORMAT" == ev.date_from|date:"SHORT_DATE_FORMAT" %}
<span data-time="{{ ev.date_admission.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% with time_human=ev.date_admission|date:"TIME_FORMAT" time_24=ev.date_admission|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
Admission: {{ time }}
{% endblocktrans %}
{% endwith %}
</span>
{% else %}
<span data-time="{{ ev.date_admission.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% with datetime_human=ev.date_admission|date:"SHORT_DATETIME_FORMAT" datetime_iso=ev.date_admission|time:"Y-m-d H:i" %}
{% blocktrans trimmed with datetime='<time datetime="'|add:datetime_iso|add:'">'|add:datetime_human|add:"</time>"|safe %}
Admission: {{ datetime }}
{% endblocktrans %}
{% endwith %}
</span>
{% endif %}
{% endif %}
<br>
{% if subevent %}
<a href="{% eventurl event "presale:event.ical.download" subevent=subevent.pk %}">
{% else %}
<a href="{% eventurl event "presale:event.ical.download" %}">
{% endif %}
{% trans "Add to Calendar" %}
</a>
</p>
</div>
{% endif %}
{% include "pretixpresale/event/fragment_event_info.html" with event=request.event subevent=subevent ev=ev show_location=True %}
</div>
{% eventsignal event "pretix.presale.signals.front_page_top" request=request subevent=subevent %}
{% endif %}
@@ -13,63 +13,28 @@
{% include "pretixpresale/event/fragment_cart_box.html" with open=request.GET.show_cart %}
{% endif %}
<h2>{% trans "Voucher redemption" %}</h2>
{% if subevent %}
<h2>{% trans "Voucher redemption" %}</h2>
{% if request.GET.subevent and subevent.pk|stringformat:"i" != request.GET.subevent %}
<div class="alert alert-warning">
{% trans "This voucher is valid only for the following specific date and time." %}
</div>
{% endif %}
<h3>{{ subevent.name }}</h3>
{% with ev=subevent %}
<div class="info-row">
<span class="fa fa-clock-o fa-fw" aria-hidden="true"></span>
<p>
{{ ev.get_date_range_display_as_html }}
{% if event.settings.show_times %}
<br>
{% with time_human=ev.date_from|date:"TIME_FORMAT" time_24=ev.date_from|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
Begin: {{ time }}
{% endblocktrans %}
{% endwith %}
{% if event.settings.show_date_to and ev.date_to %}
<br>
{% with time_human=ev.date_to|date:"TIME_FORMAT" time_24=ev.date_to|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
End: {{ time }}
{% endblocktrans %}
{% endwith %}
{% endif %}
{% endif %}
{% if ev.date_admission %}
<br>
{% if ev.date_admission|date:"SHORT_DATE_FORMAT" == ev.date_from|date:"SHORT_DATE_FORMAT" %}
{% with time_human=ev.date_admission|date:"TIME_FORMAT" time_24=ev.date_admission|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
Admission: {{ time }}
{% endblocktrans %}
{% endwith %}
{% else %}
{% with datetime_human=ev.date_admission|date:"SHORT_DATETIME_FORMAT" datetime_iso=ev.date_admission|time:"Y-m-d H:i" %}
{% blocktrans trimmed with datetime='<time datetime="'|add:datetime_iso|add:'">'|add:datetime_human|add:"</time>"|safe %}
Admission: {{ datetime }}
{% endblocktrans %}
{% endwith %}
{% endif %}
{% endif %}
<br>
{% if subevent %}
<a href="{% eventurl event "presale:event.ical.download" subevent=subevent.pk %}">
{% else %}
<a href="{% eventurl event "presale:event.ical.download" %}">
{% endif %}
{% trans "Add to Calendar" %}
</a>
</p>
</div>
{% endwith %}
{% include "pretixpresale/event/fragment_event_info.html" with event=request.event subevent=subevent ev=subevent show_location=True %}
{% else %}
{% if event_logo and event_logo_show_title %}
<h2 class="content-header">
{{ event.name }}
{% if request.event.settings.show_dates_on_frontpage %}
<small>{{ event.get_date_range_display_as_html }}</small>
{% endif %}
</h2>
{% include "pretixpresale/event/fragment_event_info.html" with event=request.event subevent=None ev=request.event show_location=True %}
<h3>{% trans "Voucher redemption" %}</h3>
{% else %}
<h2>{% trans "Voucher redemption" %}</h2>
{% endif %}
{% endif %}
<p>
+6 -1
View File
@@ -496,7 +496,12 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
ctx['order'] = self.order
ctx['payment'] = self.payment
if 'order' in inspect.signature(self.payment.payment_provider.checkout_confirm_render).parameters:
ctx['payment_info'] = self.payment.payment_provider.checkout_confirm_render(self.request, order=self.order)
if 'info_data' in inspect.signature(self.payment.payment_provider.checkout_confirm_render).parameters:
ctx['payment_info'] = self.payment.payment_provider.checkout_confirm_render(
self.request, order=self.order, info_data=self.payment.info_data
)
else:
ctx['payment_info'] = self.payment.payment_provider.checkout_confirm_render(self.request, order=self.order)
else:
ctx['payment_info'] = self.payment.payment_provider.checkout_confirm_render(self.request)
ctx['payment_provider'] = self.payment.payment_provider

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