mirror of
https://github.com/pretix/pretix.git
synced 2025-12-13 12:42:26 +00:00
Compare commits
1 Commits
clean-docs
...
widget-sub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79567cc724 |
2
.clabot
2
.clabot
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"contributors": "https://crm.rami.io/cla/check/?project=pretix&checkContributor=",
|
||||
"message": "Hey there! :) Thank you very much for offering a contribution to pretix! For legal reasons, we need you to sign a Contributor License Agreement in order to be able to merge the code. Sorry for the hassle :( Please download the agreement from https://pretix.eu/about/en/cla and send a signed copy to support@pretix.eu. Feel free to also contact us there or via comments here if you have any questions!\n\nFeel free to ignore me on documentation changes, translations, and trivial PRs like typo fixes (and similar single-line changes) – we can merge these without a CLA as well.",
|
||||
"message": "Hey there! :) Thank you very much for offering a contribution to pretix! For legal reasons, we need you to sign a Contributor License Agreement in order to be able to merge the code. Sorry for the hassle :( Please download the agreement from https://pretix.eu/about/en/cla and send a signed copy to support@pretix.eu. Feel free to also contact us there or via comments here if you have any questions!",
|
||||
"label": "cla-signed"
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
If you discover a vulnerability with our software or server systems, please report it to us in private. Do not to attempt to harm our users, customer's data or our system's availability when looking for vulnerabilities.
|
||||
If you discover a vulnerability with our software or server systems, please report it to us in private. Do not to attempt to harm our users, customer's data or our system's availability when looking for vulneratbilities.
|
||||
|
||||
Please contact us at security@pretix.eu with full details and steps to reproduce and allow reasonable time for us to resolve the issue before publishing your findings. If you wish to encrypt your email, you can find our GPG key [here](https://pretix.eu/.well-known/security@pretix.eu.asc).
|
||||
|
||||
Please also see our [Responsible disclosure policy](https://docs.pretix.eu/trust/security/disclosure/).
|
||||
We're not large enough to run a formal bug bounty program, but if you find a serious vulnerability in our service, we will find a way to show our gratitude.
|
||||
|
||||
## Version support
|
||||
|
||||
@@ -18,5 +18,3 @@ subscribe to our [newsletter](https://pretix.eu/about/en/blog/) in the "News abo
|
||||
category, we will also send you an email on security issues.
|
||||
|
||||
Past security issues are listed [on our website](https://pretix.eu/about/en/security).
|
||||
|
||||
Please also see our [Release cycle](https://docs.pretix.eu/trust/lifecycle/release-cycle/) documentation.
|
||||
|
||||
@@ -48,6 +48,11 @@ seat objects The assigned se
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.14
|
||||
|
||||
This ``is_bundled`` attribute has been added and the cart creation endpoints have been updated.
|
||||
|
||||
|
||||
Cart position endpoints
|
||||
-----------------------
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Certificate download
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/certificate/ HTTP/1.1
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
@@ -38,7 +38,7 @@ Certificate download
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5 HTTP/1.1
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5 HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
|
||||
@@ -9,6 +9,14 @@ This page describes special APIs built for ticket scanning apps. For managing ch
|
||||
please also see :ref:`rest-checkinlists`. The check-in list API also contains endpoints to obtain statistics or log
|
||||
failed scans.
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
The endpoints listed on this page have been added.
|
||||
|
||||
.. versionchanged:: 4.18
|
||||
|
||||
The ``source_type`` parameter has been added.
|
||||
|
||||
.. _`rest-checkin-redeem`:
|
||||
|
||||
Checking a ticket in
|
||||
@@ -46,11 +54,6 @@ Checking a ticket in
|
||||
this request twice with the same nonce, the second request will also succeed but will always
|
||||
create only one check-in object even when the previous request was successful as well. This
|
||||
allows for a certain level of idempotency and enables you to re-try after a connection failure.
|
||||
:<json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the
|
||||
order) when building texts (currently only the ``reason_explanation`` response field).
|
||||
Defaults to ``false`` in which case the server will determine the language (currently
|
||||
the event default language, might change in the future with support for the
|
||||
``Accept-Language`` header).
|
||||
:>json string status: ``"ok"``, ``"incomplete"``, or ``"error"``
|
||||
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
|
||||
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
|
||||
@@ -59,9 +62,7 @@ Checking a ticket in
|
||||
will only include check-ins for the selected list. (2) An additional boolean property
|
||||
``require_attention`` will inform you whether either the order or the item have the
|
||||
``checkin_attention`` flag set. (3) If ``attendee_name`` is empty, it may automatically fall
|
||||
back to values from a parent product or from invoice addresses. (4) Additional properties
|
||||
``order__status``, ``order__valid_if_pending``, ``order__require_approval``, and
|
||||
``order__locale`` are included with details form the order for convenience.
|
||||
back to values from a parent product or from invoice addresses.
|
||||
:>json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
|
||||
:>json list checkin_texts: List of additional texts to show to the user.
|
||||
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
|
||||
|
||||
@@ -40,6 +40,10 @@ ignore_in_statistics boolean If ``true``, ch
|
||||
consider_tickets_used boolean If ``true`` (default), tickets checked in on this list will be considered "used" by other functionality, i.e. when checking if they can still be canceled.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
The ``addon_match`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2023.9
|
||||
|
||||
The ``ignore_in_statistics`` and ``consider_tickets_used`` attributes have been added.
|
||||
|
||||
@@ -34,6 +34,12 @@ password string Can only be set
|
||||
not be included in any responses.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionadded:: 4.0
|
||||
|
||||
.. versionchanged:: 4.3
|
||||
|
||||
Passwords can now be set through the API during customer creation.
|
||||
|
||||
.. versionchanged:: 2024.3
|
||||
|
||||
The attribute ``phone`` has been added.
|
||||
|
||||
@@ -35,10 +35,6 @@ 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).
|
||||
subevent_date_from datetime The first date time of a subevent to which this discount can be applied
|
||||
(or ``null``). Ignored in non-series events.
|
||||
subevent_date_until datetime The last date time of a subevent to which this discount can be applied
|
||||
(or ``null``). Ignored in non-series events.
|
||||
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 condition applies to.
|
||||
@@ -109,8 +105,6 @@ Endpoints
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"subevent_date_from": null,
|
||||
"subevent_date_until": null,
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
@@ -169,8 +163,6 @@ Endpoints
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"subevent_date_from": null,
|
||||
"subevent_date_until": null,
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
@@ -215,8 +207,6 @@ Endpoints
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"subevent_date_from": null,
|
||||
"subevent_date_until": null,
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
@@ -250,8 +240,6 @@ Endpoints
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"subevent_date_from": null,
|
||||
"subevent_date_until": null,
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
@@ -314,8 +302,6 @@ Endpoints
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"subevent_date_from": null,
|
||||
"subevent_date_until": null,
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
|
||||
@@ -61,6 +61,25 @@ public_url string The public, cus
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
|
||||
The ``clone_from`` parameter has been added to the event creation endpoint.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The ``with_availability_for`` parameter has been added.
|
||||
|
||||
The ``search`` query parameter has been added to filter events by their slug, name, or location in any language.
|
||||
|
||||
.. versionchanged:: 4.17
|
||||
|
||||
The ``public_url`` field has been added.
|
||||
|
||||
.. versionchanged:: 5.0
|
||||
|
||||
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
|
||||
added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/
|
||||
|
||||
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
||||
@@ -611,6 +630,10 @@ organizer level.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. versionchanged:: 4.18
|
||||
|
||||
The ``readonly`` flag has been added.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/settings/
|
||||
|
||||
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.
|
||||
|
||||
@@ -47,6 +47,11 @@ acceptor string Organizer slug
|
||||
this field was added.)
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.20
|
||||
|
||||
The ``owner_ticket`` and ``issuer`` attributes of the gift card and the ``info`` and ``acceptor`` attributes of the
|
||||
gift card transaction resource have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -111,6 +111,18 @@ internal_reference string Customer's refe
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The attributes ``fee_type`` and ``fee_internal_type`` have been added.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The attribute ``lines.event_location`` has been added.
|
||||
|
||||
.. versionchanged:: 4.6
|
||||
|
||||
The attribute ``lines.subevent`` has been added.
|
||||
|
||||
.. versionchanged:: 2023.8
|
||||
|
||||
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
||||
|
||||
@@ -64,6 +64,10 @@ hide_without_voucher boolean If ``true``, th
|
||||
meta_data object Values set for event-specific meta data parameters.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
|
||||
The ``meta_data`` and ``checkin_attention`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 2023.10
|
||||
|
||||
The ``free_price_suggestion`` attribute has been added.
|
||||
|
||||
@@ -211,6 +211,28 @@ bundles list of objects Definition of
|
||||
meta_data object Values set for event-specific meta data parameters.
|
||||
======================================= ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
|
||||
The attributes ``require_membership``, ``require_membership_types``, ``grant_membership_type``, ``grant_membership_duration_like_event``,
|
||||
``grant_membership_duration_days`` and ``grant_membership_duration_months`` have been added.
|
||||
|
||||
.. versionchanged:: 4.4
|
||||
|
||||
The attributes ``require_membership_hidden`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
|
||||
The ``variations[x].meta_data`` and ``variations[x].checkin_attention`` attributes have been added.
|
||||
The ``personalized`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.17
|
||||
|
||||
The ``validity_*`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 4.18
|
||||
|
||||
The ``media_policy`` and ``media_type`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 2023.10
|
||||
|
||||
The ``checkin_text`` and ``variations[x].checkin_text`` attributes have been added.
|
||||
|
||||
@@ -114,6 +114,34 @@ plugin_data object Additional data
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
|
||||
The ``customer`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The ``custom_followup_at`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.4
|
||||
|
||||
The ``item`` and ``variation`` query parameters have been added.
|
||||
|
||||
.. versionchanged:: 4.6
|
||||
|
||||
The ``subevent`` query parameters has been added.
|
||||
|
||||
.. versionchanged:: 4.8
|
||||
|
||||
The ``order.fees.id`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 4.15
|
||||
|
||||
The ``include`` query parameter has been added.
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
|
||||
The ``valid_if_pending`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2023.8
|
||||
|
||||
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
||||
@@ -232,6 +260,10 @@ pdf_data object Data object req
|
||||
plugin_data object Additional data added by plugins.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
|
||||
The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added.
|
||||
|
||||
.. versionchanged:: 2024.9
|
||||
|
||||
The attribute ``print_logs`` has been added.
|
||||
@@ -724,6 +756,10 @@ Fetching individual orders
|
||||
Order ticket download
|
||||
---------------------
|
||||
|
||||
.. versionchanged:: 4.10
|
||||
|
||||
The API now supports ticket downloads for pending orders if allowed by the event settings.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/download/(output)/
|
||||
|
||||
Download tickets for an order, identified by its order code. Depending on the chosen output, the response might
|
||||
@@ -1796,6 +1832,10 @@ Fetching individual positions
|
||||
Order position ticket download
|
||||
------------------------------
|
||||
|
||||
.. versionchanged:: 4.10
|
||||
|
||||
The API now supports ticket downloads for pending orders if allowed by the event settings.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/download/(output)/
|
||||
|
||||
Download tickets for one order position, identified by its internal ID.
|
||||
@@ -1848,6 +1888,15 @@ Order position ticket download
|
||||
Manipulating individual positions
|
||||
---------------------------------
|
||||
|
||||
.. versionchanged:: 4.8
|
||||
|
||||
The ``PATCH`` method now supports changing items, variations, subevents, seats, prices, and tax rules.
|
||||
The ``POST`` endpoint to add individual positions has been added.
|
||||
|
||||
.. versionadded:: 4.16
|
||||
|
||||
The endpoints to manage blocks have been added.
|
||||
|
||||
.. versionchanged:: 2024.9
|
||||
|
||||
The API now supports logging ticket and badge prints.
|
||||
@@ -1894,14 +1943,9 @@ Manipulating individual positions
|
||||
|
||||
* ``valid_until``
|
||||
|
||||
* ``secret``
|
||||
|
||||
Changing parameters such as ``item`` or ``price`` will **not** automatically trigger creation of a new invoice,
|
||||
you need to take care of that yourself.
|
||||
|
||||
Changing ``secret`` does not cause a new PDF ticket to be sent to the customer, nor does it cause the old secret
|
||||
to be added to the revocation list, even if your ticket generator uses one.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -2177,6 +2221,10 @@ multiple changes to an order at once within one transaction. This makes it possi
|
||||
attendees in an order without running into conflicts. This interface also offers some possibilities not available
|
||||
otherwise, such as splitting an order or changing fees.
|
||||
|
||||
.. versionchanged:: 4.8
|
||||
|
||||
This endpoint has been added to the system.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/change/
|
||||
|
||||
Performs a change operation on an order. You can supply the following fields:
|
||||
|
||||
@@ -25,6 +25,10 @@ public_url string The public, cus
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 4.17
|
||||
|
||||
The ``public_url`` field has been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/
|
||||
|
||||
Returns a list of all organizers the authenticated user/token has access to.
|
||||
|
||||
@@ -36,6 +36,11 @@ available_number integer Number of avail
|
||||
slightly out of date. ``null`` means unlimited.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The ``with_availability`` query parameter has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ Data shredders
|
||||
pretix and it's plugins include a number of data shredders that allow you to clear personal information from the system.
|
||||
This page shows you how to use these shredders through the API.
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
This feature has been added to the API.
|
||||
|
||||
.. warning::
|
||||
|
||||
Unlike the user interface, the API will not force you to download tax-relevant data before you delete it.
|
||||
|
||||
@@ -59,6 +59,15 @@ seat_category_mapping object An object mappi
|
||||
last_modified datetime Last modification of this object
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.15
|
||||
|
||||
The ``search`` query parameter has been added to filter sub-events by their name or location in any language.
|
||||
|
||||
.. versionchanged:: 5.0
|
||||
|
||||
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
|
||||
added.
|
||||
|
||||
.. versionchanged:: 2023.8.0
|
||||
|
||||
For the organizer-wide endpoint, the ``search`` query parameter has been modified to filter sub-events by their parent events slug too.
|
||||
@@ -66,6 +75,10 @@ last_modified datetime Last modificati
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 4.1
|
||||
|
||||
The ``with_availability_for`` parameter has been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
||||
|
||||
Returns a list of all sub-events of an event.
|
||||
|
||||
@@ -40,6 +40,10 @@ custom_rules object Dynamic rules s
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 4.6
|
||||
|
||||
The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 2023.6
|
||||
|
||||
The ``custom_rules`` attribute has been added.
|
||||
|
||||
@@ -39,6 +39,10 @@ can_change_vouchers boolean
|
||||
can_checkin_orders boolean
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.18
|
||||
|
||||
The ``can_manage_reusable_media`` permission has been added.
|
||||
|
||||
Team member resource
|
||||
--------------------
|
||||
|
||||
|
||||
@@ -178,6 +178,13 @@ You can then implement a view as you would normally do. It will be automatically
|
||||
* Your plugin is enabled
|
||||
* The locale is set correctly
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The ``event_url()`` wrapper has been added in 1.7 to replace the former ``@event_view`` decorator. The
|
||||
``event_url()`` wrapper is optional and using ``url()`` still works, but you will not be able to set the
|
||||
``require_live`` setting any more via the decorator. The ``@event_view`` decorator is now deprecated and
|
||||
does nothing.
|
||||
|
||||
REST API viewsets
|
||||
-----------------
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Contents:
|
||||
exporter
|
||||
ticketoutput
|
||||
payment
|
||||
payment_2.0
|
||||
email
|
||||
placeholder
|
||||
invoice
|
||||
|
||||
129
doc/development/api/payment_2.0.rst
Normal file
129
doc/development/api/payment_2.0.rst
Normal file
@@ -0,0 +1,129 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
.. _`payment2.0`:
|
||||
|
||||
Porting a payment provider from pretix 1.x to pretix 2.x
|
||||
========================================================
|
||||
|
||||
In pretix 2.x, we changed large parts of the payment provider API. This documentation details the changes we made
|
||||
and shows you how you can make an existing pretix 1.x payment provider compatible with pretix 2.x
|
||||
|
||||
Conceptual overview
|
||||
-------------------
|
||||
|
||||
In pretix 1.x, an order was always directly connected to a payment provider for the full life of an order. As long as
|
||||
an order was unpaid, this could still be changed in some cases, but once an order was paid, no changes to the payment
|
||||
provider were possible any more. Additionally, the internal state of orders allowed orders only to be fully paid or
|
||||
not paid at all. This leads to a couple of consequences:
|
||||
|
||||
* Payment-related functions (like "execute payment" or "do a refund") always operated on full orders.
|
||||
|
||||
* Changing the total of an order was basically impossible once an order was paid, since there was no concept of
|
||||
partial payments or partial refunds.
|
||||
|
||||
* Payment provider plugins needed to take complicated steps to detect cases that require human intervention, like e.g.
|
||||
|
||||
* An order has expired, no quota is left to revive it, but a payment has been received
|
||||
|
||||
* A payment has been received for a canceled order
|
||||
|
||||
* A payment has been received for an order that has already been paid with a different payment method
|
||||
|
||||
* An external payment service notified us of a refund/dispute
|
||||
|
||||
We noticed that we copied and repeated large portions of code in all our official payment provider plugins, just
|
||||
to deal with some of these cases.
|
||||
|
||||
* Sometimes, there is the need to mark an order as refunded within pretix, without automatically triggering a refund
|
||||
with an external API. Every payment method needed to implement a user interface for this independently.
|
||||
|
||||
* If a refund was not possible automatically, there was no way user to track which payments actually have been refunded
|
||||
manually and which are still left to do.
|
||||
|
||||
* When the payment with one payment provider failed and the user changed to a different payment provider, all
|
||||
information about the first payment was lost from the order object and could only be retrieved from order log data,
|
||||
which also made it hard to design a data shredder API to get rid of this data.
|
||||
|
||||
In pretix 2.x, we introduced two new models, :py:class:`OrderPayment <pretix.base.models.OrderPayment>` and
|
||||
:py:class:`OrderRefund <pretix.base.models.OrderRefund>`. Each instance of these is connected to an order and
|
||||
represents one single attempt to pay or refund a specific amount of money. Each one of these has an individual state,
|
||||
can individually fail or succeed, and carries an amount variable that can differ from the order total.
|
||||
|
||||
This has the following advantages:
|
||||
|
||||
* The system can now detect orders that are over- or underpaid, independent of the payment providers in use.
|
||||
|
||||
* Therefore, we can now allow partial payments, partial refunds, and changing paid orders, and automatically detect
|
||||
the cases listed above and notify the user.
|
||||
|
||||
Payment providers now interact with those payment and refund objects more than with orders.
|
||||
|
||||
Your to-do list
|
||||
---------------
|
||||
|
||||
Payment processing
|
||||
""""""""""""""""""
|
||||
|
||||
* The method ``BasePaymentProvider.order_pending_render`` has been removed and replaced by a new
|
||||
``BasePaymentProvider.payment_pending_render(request, payment)`` method that is passed an ``OrderPayment``
|
||||
object instead of an ``Order``.
|
||||
|
||||
* The method ``BasePaymentProvider.payment_form_render`` now receives a new ``total`` parameter.
|
||||
|
||||
* The method ``BasePaymentProvider.payment_perform`` has been removed and replaced by a new method
|
||||
``BasePaymentProvider.execute_payment(request, payment)`` that is passed an ``OrderPayment``
|
||||
object instead of an ``Order``.
|
||||
|
||||
* The function ``pretix.base.services.mark_order_paid`` has been removed, instead call ``payment.confirm()``
|
||||
on a pending ``OrderPayment`` object. If no further payments are required for this order, this will also
|
||||
mark the order as paid automatically. Note that ``payment.confirm()`` can still throw a ``QuotaExceededException``,
|
||||
however it will still mark the payment as complete (not the order!), so you should catch this exception and
|
||||
inform the user, but not abort the transaction.
|
||||
|
||||
* A new property ``BasePaymentProvider.abort_pending_allowed`` has been introduced. Only if set, the user will
|
||||
be able to retry a payment or switch the payment method when the order currently has a payment object in
|
||||
state ``"pending"``. This replaces ``BasePaymentProvider.order_can_retry``, which no longer exists.
|
||||
|
||||
* The methods ``BasePaymentProvider.retry_prepare`` and ``BasePaymentProvider.order_prepare`` have both been
|
||||
replaced by a new method ``BasePaymentProvider.payment_prepare(request, payment)`` that is passed an ``OrderPayment``
|
||||
object instead of an ``Order``. **Keep in mind that this payment object might have an amount property that
|
||||
differs from the order total, if the order is already partially paid.**
|
||||
|
||||
* The method ``BasePaymentProvider.order_paid_render`` has been removed.
|
||||
|
||||
* The method ``BasePaymentProvider.order_control_render`` has been removed and replaced by a new method
|
||||
``BasePaymentProvider.payment_control_render(request, payment)`` that is passed an ``OrderPayment``
|
||||
object instead of an ``Order``.
|
||||
|
||||
* There's no need to manually deal with excess payments or duplicate payments anymore, just setting the ``OrderPayment``
|
||||
methods to the correct state will do the job.
|
||||
|
||||
Creating refunds
|
||||
""""""""""""""""
|
||||
|
||||
* The methods ``BasePaymentProvider.order_control_refund_render`` and ``BasePaymentProvider.order_control_refund_perform``
|
||||
have been removed.
|
||||
|
||||
* Two new boolean methods ``BasePaymentProvider.payment_refund_supported(payment)`` and ``BasePaymentProvider.payment_partial_refund_supported(payment)``
|
||||
have been introduced. They should be set to return ``True`` if and only if the payment API allows to *automatically*
|
||||
transfer the money back to the customer.
|
||||
|
||||
* A new method ``BasePaymentProvider.execute_refund(refund)`` has been introduced. This method is called using a
|
||||
``OrderRefund`` object in ``"created"`` state and is expected to transfer the money back and confirm success with
|
||||
calling ``refund.done()``. This will only ever be called if either ``BasePaymentProvider.payment_refund_supported(payment)``
|
||||
or ``BasePaymentProvider.payment_partial_refund_supported(payment)`` return ``True``.
|
||||
|
||||
Processing external refunds
|
||||
"""""""""""""""""""""""""""
|
||||
|
||||
* If e.g. a webhook API notifies you that a payment has been disputed or refunded with the external API, you are
|
||||
expected to call ``OrderPayment.create_external_refund(self, amount, execution_date, info='{}')`` on this payment.
|
||||
This will create and return an appropriate ``OrderRefund`` object and send out a notification. However, it will not
|
||||
mark the order as refunded, but will ask the event organizer for a decision.
|
||||
|
||||
Data shredders
|
||||
""""""""""""""
|
||||
|
||||
* The method ``BasePaymentProvider.shred_payment_info`` is no longer passed an order, but instead **either**
|
||||
an ``OrderPayment`` **or** an ``OrderRefund``.
|
||||
@@ -84,8 +84,6 @@ A working example would be:
|
||||
restricted = False
|
||||
description = _("This plugin allows you to receive payments via PayPal")
|
||||
compatibility = "pretix>=2.7.0"
|
||||
settings_links = []
|
||||
navigation_links = []
|
||||
|
||||
|
||||
default_app_config = 'pretix_paypal.PaypalApp'
|
||||
@@ -187,28 +185,6 @@ your Django app label.
|
||||
with checking that the calling user is logged in, has appropriate permissions,
|
||||
etc. We plan on providing native support for this in a later version.
|
||||
|
||||
To make your plugin views easily discoverable, you can specify links for "Go to"
|
||||
and "Settings" buttons next to your entry on the plugin page. These links should be
|
||||
added to the ``navigation_links`` and ``settings_links``, respectively, in the
|
||||
``PretixPluginMeta`` class.
|
||||
|
||||
Each array entry consists of a tuple ``(label, urlname, kwargs)``. For the label,
|
||||
either a string or a tuple of strings can be specified. In the latter case, the provided
|
||||
strings will be merged with a separator indicating they are successive navigation steps
|
||||
the user would need to take to reach the page via the regular menu
|
||||
(e.g. "Payment > Bank transfer" as below).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
settings_links = [
|
||||
((_("Payment"), _("Bank transfer")), "control:event.settings.payment.provider", {"provider": "banktransfer"}),
|
||||
]
|
||||
navigation_links = [
|
||||
((_("Bank transfer"), _("Import bank data")), "plugins:banktransfer:import", {}),
|
||||
((_("Bank transfer"), _("Export refunds")), "plugins:banktransfer:refunds.list", {}),
|
||||
]
|
||||
|
||||
|
||||
.. _Django app: https://docs.djangoproject.com/en/3.0/ref/applications/
|
||||
.. _signal dispatcher: https://docs.djangoproject.com/en/3.0/topics/signals/
|
||||
.. _namespace packages: https://legacy.python.org/dev/peps/pep-0420/
|
||||
|
||||
@@ -30,13 +30,13 @@ dependencies = [
|
||||
"babel",
|
||||
"BeautifulSoup4==4.13.*",
|
||||
"bleach==6.2.*",
|
||||
"celery==5.5.*",
|
||||
"celery==5.4.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=44.0.0",
|
||||
"css-inline==0.14.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"Django[argon2]==4.2.*,>=4.2.15",
|
||||
"django-bootstrap3==25.1",
|
||||
"django-bootstrap3==24.3",
|
||||
"django-compressor==4.5.1",
|
||||
"django-countries==7.6.*",
|
||||
"django-filter==25.1",
|
||||
@@ -49,22 +49,22 @@ dependencies = [
|
||||
"django-localflavor==4.0",
|
||||
"django-markup",
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.6.*",
|
||||
"django-otp==1.5.*",
|
||||
"django-phonenumber-field==7.3.*",
|
||||
"django-redis==5.4.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.6.*",
|
||||
"djangorestframework==3.16.*",
|
||||
"djangorestframework==3.15.*",
|
||||
"dnspython==2.7.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==5.*",
|
||||
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"isoweek",
|
||||
"jsonschema",
|
||||
"kombu==5.5.*",
|
||||
"kombu==5.4.*",
|
||||
"libsass==0.23.*",
|
||||
"lxml",
|
||||
"markdown==3.8", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
"markdown==3.7", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||
"mt-940==4.30.*",
|
||||
"oauthlib==3.2.*",
|
||||
@@ -73,25 +73,25 @@ dependencies = [
|
||||
"paypalrestsdk==1.13.*",
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.10.*",
|
||||
"phonenumberslite==9.0.*",
|
||||
"Pillow==11.2.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==11.1.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==6.30.*",
|
||||
"protobuf==5.29.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.23.*",
|
||||
"pypdf==5.4.*",
|
||||
"pycryptodome==3.21.*",
|
||||
"pypdf==5.1.*",
|
||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==8.2",
|
||||
"qrcode==8.0",
|
||||
"redis==5.2.*",
|
||||
"reportlab==4.4.*",
|
||||
"reportlab==4.3.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==2.29.*",
|
||||
"sentry-sdk==2.22.*",
|
||||
"sepaxml==2.6.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
@@ -107,14 +107,14 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
memcached = ["pylibmc"]
|
||||
dev = [
|
||||
"aiohttp==3.12.*",
|
||||
"aiohttp==3.11.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.26.*",
|
||||
"flake8==7.2.*",
|
||||
"flake8==7.1.*",
|
||||
"freezegun",
|
||||
"isort==6.0.*",
|
||||
"pep8-naming==0.15.*",
|
||||
"pep8-naming==0.14.*",
|
||||
"potypo",
|
||||
"pytest-asyncio>=0.24",
|
||||
"pytest-cache",
|
||||
@@ -122,7 +122,7 @@ dev = [
|
||||
"pytest-django==4.*",
|
||||
"pytest-mock==3.14.*",
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.7.*",
|
||||
"pytest-xdist==3.6.*",
|
||||
"pytest==8.3.*",
|
||||
"responses",
|
||||
]
|
||||
|
||||
@@ -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__ = "2025.6.0.dev0"
|
||||
__version__ = "2025.3.0.dev0"
|
||||
|
||||
@@ -101,7 +101,6 @@ ALL_LANGUAGES = [
|
||||
('fi', _('Finnish')),
|
||||
('gl', _('Galician')),
|
||||
('el', _('Greek')),
|
||||
('he', _('Hebrew')),
|
||||
('id', _('Indonesian')),
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
@@ -122,8 +121,7 @@ LANGUAGES_OFFICIAL = {
|
||||
'en', 'de', 'de-informal'
|
||||
}
|
||||
LANGUAGES_RTL = {
|
||||
# When adding more right-to-left languages, also update pretix/static/pretixbase/scss/_rtl.scss
|
||||
'ar', 'he'
|
||||
'ar', 'hw'
|
||||
}
|
||||
LANGUAGES_INCUBATING = {
|
||||
'pt-br', 'gl',
|
||||
@@ -261,7 +259,7 @@ COMPRESS_FILTERS = {
|
||||
CURRENCIES = [
|
||||
c for c in currencies
|
||||
if c.alpha_3 not in {
|
||||
'USN', 'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
|
||||
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
|
||||
}
|
||||
]
|
||||
CURRENCY_PLACES = {
|
||||
|
||||
@@ -81,13 +81,6 @@ class SalesChannelMigrationMixin:
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if "sales_channels" in data:
|
||||
if data["sales_channels"] is None:
|
||||
raise ValidationError({
|
||||
"sales_channels": [
|
||||
"The legacy attribute 'sales_channels' cannot be set to None, it must be a list."
|
||||
]
|
||||
})
|
||||
|
||||
prefetch_related_objects([self.organizer], "sales_channels")
|
||||
all_channels = {
|
||||
s.identifier for s in
|
||||
@@ -96,7 +89,7 @@ class SalesChannelMigrationMixin:
|
||||
|
||||
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||
raise ValidationError({
|
||||
"all_sales_channels": [
|
||||
"limit_sales_channels": [
|
||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the list of all sales channels."
|
||||
]
|
||||
@@ -116,7 +109,6 @@ class SalesChannelMigrationMixin:
|
||||
else:
|
||||
data["all_sales_channels"] = False
|
||||
data["limit_sales_channels"] = data["sales_channels"]
|
||||
|
||||
del data["sales_channels"]
|
||||
|
||||
if data.get("all_sales_channels"):
|
||||
|
||||
@@ -176,7 +176,7 @@ class BaseCartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data.pop('_quotas')
|
||||
answers_data = validated_data.pop('answers', [])
|
||||
answers_data = validated_data.pop('answers')
|
||||
|
||||
attendee_name = validated_data.pop('attendee_name', '')
|
||||
if attendee_name and not validated_data.get('attendee_name_parts'):
|
||||
|
||||
@@ -84,7 +84,6 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
|
||||
type = serializers.ChoiceField(choices=Checkin.CHECKIN_TYPES, default=Checkin.TYPE_ENTRY)
|
||||
ignore_unpaid = serializers.BooleanField(default=False, required=False)
|
||||
questions_supported = serializers.BooleanField(default=True, required=False)
|
||||
use_order_locale = serializers.BooleanField(default=False, required=False)
|
||||
nonce = serializers.CharField(required=False, allow_null=True)
|
||||
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
||||
answers = serializers.JSONField(required=False, allow_null=True)
|
||||
|
||||
@@ -38,12 +38,11 @@ class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Discount
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
|
||||
'available_from', 'available_until', 'subevent_mode', 'subevent_date_from', 'subevent_date_until',
|
||||
'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', 'benefit_same_products', 'benefit_limit_products',
|
||||
'benefit_apply_to_addons', 'benefit_ignore_voucher_discounted',
|
||||
'condition_ignore_voucher_discounted')
|
||||
'available_from', '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',
|
||||
'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)
|
||||
|
||||
@@ -378,8 +378,6 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
instance._prefetched_objects_cache.clear()
|
||||
|
||||
# Item Meta properties
|
||||
if item_meta_properties is not None:
|
||||
current = list(event.item_meta_properties.all())
|
||||
@@ -400,8 +398,6 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
if prop.name not in list(item_meta_properties.keys()):
|
||||
prop.delete()
|
||||
|
||||
instance._prefetched_objects_cache.clear()
|
||||
|
||||
# Seats
|
||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
||||
current_mappings = {
|
||||
|
||||
@@ -607,7 +607,6 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
||||
order__valid_if_pending = serializers.SlugRelatedField(read_only=True, slug_field='valid_if_pending', source='order')
|
||||
order__require_approval = serializers.SlugRelatedField(read_only=True, slug_field='require_approval', source='order')
|
||||
order__locale = serializers.SlugRelatedField(read_only=True, slug_field='locale', source='order')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
@@ -616,7 +615,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval',
|
||||
'order__locale', 'valid_from', 'valid_until', 'blocked')
|
||||
'valid_from', 'valid_until', 'blocked')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1519,8 +1518,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
self.context['event'],
|
||||
order.sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
||||
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||
(cp.item_id, cp.subevent_id, cp.price, bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||
for cp in order_positions
|
||||
]
|
||||
)
|
||||
|
||||
@@ -251,7 +251,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = (
|
||||
'item', 'variation', 'subevent', 'seat', 'price', 'tax_rule', 'valid_from', 'valid_until', 'secret'
|
||||
'item', 'variation', 'subevent', 'seat', 'price', 'tax_rule', 'valid_from', 'valid_until'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -319,7 +319,6 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
||||
tax_rule = validated_data.get('tax_rule', instance.tax_rule)
|
||||
valid_from = validated_data.get('valid_from', instance.valid_from)
|
||||
valid_until = validated_data.get('valid_until', instance.valid_until)
|
||||
secret = validated_data.get('secret', instance.secret)
|
||||
|
||||
change_item = None
|
||||
if item != instance.item or variation != instance.variation:
|
||||
@@ -352,9 +351,6 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
||||
if valid_until != instance.valid_until:
|
||||
ocm.change_valid_until(instance, valid_until)
|
||||
|
||||
if secret != instance.secret:
|
||||
ocm.change_ticket_secret(instance, secret)
|
||||
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
instance.refresh_from_db()
|
||||
|
||||
@@ -426,9 +426,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
'organizer_logo_image_inherit',
|
||||
'organizer_logo_image',
|
||||
'privacy_url',
|
||||
'accessibility_url',
|
||||
'accessibility_title',
|
||||
'accessibility_text',
|
||||
'cookie_consent',
|
||||
'cookie_consent_dialog_title',
|
||||
'cookie_consent_dialog_text',
|
||||
|
||||
@@ -165,7 +165,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
|
||||
if not serializer.validated_data.get('position'):
|
||||
kwargs['position'] = OrderPosition.all.filter(
|
||||
order__event=self.request.event,
|
||||
secret=serializer.validated_data['raw_barcode']
|
||||
).first()
|
||||
|
||||
@@ -420,7 +419,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
|
||||
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
||||
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
||||
source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False):
|
||||
source_type='barcode', legacy_url_support=False, simulate=False, gate=None):
|
||||
if not checkinlists:
|
||||
raise ValidationError('No check-in list passed.')
|
||||
|
||||
@@ -694,11 +693,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
pass
|
||||
|
||||
# 6. Pass to our actual check-in logic
|
||||
if use_order_locale:
|
||||
locale = op.order.locale
|
||||
else:
|
||||
locale = op.order.event.settings.locale
|
||||
with language(locale):
|
||||
with language(op.order.event.settings.locale):
|
||||
try:
|
||||
perform_checkin(
|
||||
op=op,
|
||||
@@ -913,7 +908,6 @@ class CheckinRPCRedeemView(views.APIView):
|
||||
expand=self.request.query_params.getlist('expand'),
|
||||
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
||||
questions_supported=s.validated_data['questions_supported'],
|
||||
use_order_locale=s.validated_data['use_order_locale'],
|
||||
canceled_supported=True,
|
||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||
legacy_url_support=False,
|
||||
|
||||
@@ -185,7 +185,7 @@ with scopes_disabled():
|
||||
| Q(full_invoice_no__iexact=u)
|
||||
).values_list('order_id', flat=True)
|
||||
|
||||
matching_positions = OrderPosition.all.filter(
|
||||
matching_positions = OrderPosition.objects.filter(
|
||||
Q(order=OuterRef('pk')) & Q(
|
||||
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
|
||||
| Q(secret__istartswith=u)
|
||||
@@ -452,9 +452,10 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
comment = request.data.get('comment', None)
|
||||
cancellation_fee = request.data.get('cancellation_fee', None)
|
||||
if cancellation_fee:
|
||||
cancellation_fee = serializers.DecimalField(max_digits=13, decimal_places=2).to_internal_value(
|
||||
cancellation_fee,
|
||||
)
|
||||
try:
|
||||
cancellation_fee = float(Decimal(cancellation_fee))
|
||||
except:
|
||||
cancellation_fee = None
|
||||
|
||||
order = self.get_object()
|
||||
if not order.cancel_allowed():
|
||||
|
||||
@@ -35,22 +35,19 @@ def get_powered_by(request, safelink=True):
|
||||
d = gs.settings.license_check_input
|
||||
if d.get('poweredby_name'):
|
||||
if d.get('poweredby_url'):
|
||||
msg = gettext('<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>').format(
|
||||
name=d['poweredby_name'],
|
||||
a_name_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||
sl(d['poweredby_url']) if safelink else d['poweredby_url'],
|
||||
),
|
||||
a_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
||||
)
|
||||
n = '<a href="{}" target="_blank" rel="noopener">{}</a>'.format(
|
||||
sl(d['poweredby_url']) if safelink else d['poweredby_url'],
|
||||
d['poweredby_name']
|
||||
)
|
||||
else:
|
||||
msg = gettext('<a {a_attr}>powered by {name} based on pretix</a>').format(
|
||||
name=d['poweredby_name'],
|
||||
a_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
||||
)
|
||||
n = d['poweredby_name']
|
||||
|
||||
msg = gettext('powered by {name} based on <a {a_attr}>pretix</a>').format(
|
||||
name=n,
|
||||
a_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
||||
)
|
||||
)
|
||||
else:
|
||||
msg = gettext('<a %(a_attr)s>ticketing powered by pretix</a>') % {
|
||||
'a_attr': 'href="{}" target="_blank" rel="noopener"'.format(
|
||||
@@ -74,7 +71,7 @@ def contextprocessor(request):
|
||||
try:
|
||||
ctx['poweredby'] = get_powered_by(request, safelink=True)
|
||||
except Exception:
|
||||
ctx['poweredby'] = '<a href="https://pretix.eu/" target="_blank" rel="noopener">powered by pretix</a>'
|
||||
ctx['poweredby'] = 'powered by <a href="https://pretix.eu/" target="_blank" rel="noopener">pretix</a>'
|
||||
if settings.DEBUG and 'runserver' not in sys.argv:
|
||||
ctx['debug_warning'] = True
|
||||
elif 'runserver' in sys.argv:
|
||||
|
||||
@@ -712,7 +712,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
if name_scheme and len(name_scheme['fields']) > 1:
|
||||
for k, label, w in name_scheme['fields']:
|
||||
row.append(
|
||||
get_name_parts_localized(op.attendee_name_parts, k) if op.attendee_name_parts else ''
|
||||
get_name_parts_localized(op.attendee_name_parts, k)
|
||||
)
|
||||
row += [
|
||||
op.attendee_email,
|
||||
|
||||
@@ -108,10 +108,8 @@ class WaitingListExporter(ListExporter):
|
||||
_('Name'),
|
||||
_('Email'),
|
||||
_('Phone number'),
|
||||
_('Product'),
|
||||
_('Product ID'),
|
||||
_('Product name'),
|
||||
_('Variation'),
|
||||
_('Variation ID'),
|
||||
_('Event slug'),
|
||||
_('Event name'),
|
||||
pgettext_lazy('subevents', 'Date'), # Name of subevent
|
||||
@@ -148,9 +146,7 @@ class WaitingListExporter(ListExporter):
|
||||
entry.email,
|
||||
entry.phone,
|
||||
str(entry.item) if entry.item else "",
|
||||
str(entry.item.pk) if entry.item else "",
|
||||
str(entry.variation) if entry.variation else "",
|
||||
str(entry.variation.pk) if entry.variation else "",
|
||||
entry.event.slug,
|
||||
entry.event.name,
|
||||
entry.subevent.name if entry.subevent else "",
|
||||
|
||||
@@ -58,7 +58,6 @@ from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.timezone import get_current_timezone
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries import countries
|
||||
@@ -84,8 +83,8 @@ from pretix.base.services.tax import (
|
||||
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
|
||||
)
|
||||
from pretix.base.settings import (
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
|
||||
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS,
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
@@ -128,13 +127,7 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
if fname == 'title' and self.titles:
|
||||
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
|
||||
elif fname == 'salutation':
|
||||
widgets.append(Select(
|
||||
attrs=a,
|
||||
choices=[
|
||||
('', '---'),
|
||||
('empty', '({})'.format(pgettext_lazy("name_salutation", "not specified"))),
|
||||
] + PERSON_NAME_SALUTATIONS
|
||||
))
|
||||
widgets.append(Select(attrs=a, choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS))
|
||||
else:
|
||||
widgets.append(self.widget(attrs=a))
|
||||
super().__init__(widgets, attrs)
|
||||
@@ -184,7 +177,8 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
these_attrs.pop('data-no-required-attr', None)
|
||||
these_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip()
|
||||
these_attrs['data-size'] = self.scheme['fields'][i][2]
|
||||
these_attrs['aria-label'] = self.scheme['fields'][i][1]
|
||||
if len(self.widgets) > 1:
|
||||
these_attrs['aria-label'] = self.scheme['fields'][i][1]
|
||||
else:
|
||||
these_attrs = final_attrs
|
||||
output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer))
|
||||
@@ -251,10 +245,7 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
d.pop('validators', None)
|
||||
field = forms.ChoiceField(
|
||||
**d,
|
||||
choices=[
|
||||
('', '---'),
|
||||
('empty', '({})'.format(pgettext_lazy("name_salutation", "not specified"))),
|
||||
] + PERSON_NAME_SALUTATIONS
|
||||
choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS
|
||||
)
|
||||
else:
|
||||
field = forms.CharField(**defaults)
|
||||
@@ -730,7 +721,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
'data-country-information-url': reverse('js_helpers.states'),
|
||||
}),
|
||||
)
|
||||
c = [('', '---')]
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
|
||||
cc = None
|
||||
state = None
|
||||
@@ -870,23 +861,6 @@ class BaseQuestionsForm(forms.Form):
|
||||
attrs['data-min'] = q.valid_date_min.isoformat()
|
||||
if q.valid_date_max:
|
||||
attrs['data-max'] = q.valid_date_max.isoformat()
|
||||
if not help_text:
|
||||
if q.valid_date_min and q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date between {min} and {max}.',
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif q.valid_date_min:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date no earlier than {min}.',
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date no later than {max}.',
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
field = forms.DateField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
@@ -905,23 +879,6 @@ class BaseQuestionsForm(forms.Form):
|
||||
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATETIME:
|
||||
if not help_text:
|
||||
if q.valid_datetime_min and q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date and time between {min} and {max}.',
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif q.valid_datetime_min:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date and time no earlier than {min}.',
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
'Please enter a date and time no later than {max}.',
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
field = SplitDateTimeField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
@@ -1122,7 +1079,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.fields['country'].choices = CachedCountries()
|
||||
self.fields['country'].widget.attrs['data-country-information-url'] = reverse('js_helpers.states')
|
||||
|
||||
c = [('', '---')]
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = self.prefix + '-' if self.prefix else ''
|
||||
cc = None
|
||||
if fprefix + 'country' in self.data:
|
||||
@@ -1131,19 +1088,16 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
cc = str(self.initial['country'])
|
||||
elif self.instance and self.instance.country:
|
||||
cc = str(self.instance.country)
|
||||
state_label = pgettext_lazy('address', 'State')
|
||||
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
|
||||
if cc in COUNTRY_STATE_LABEL:
|
||||
state_label = COUNTRY_STATE_LABEL[cc]
|
||||
elif fprefix + 'state' in self.data:
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'state']
|
||||
|
||||
self.fields['state'] = forms.ChoiceField(
|
||||
label=state_label,
|
||||
label=pgettext_lazy('address', 'State'),
|
||||
required=False,
|
||||
choices=c,
|
||||
widget=forms.Select(attrs={
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Generated by Django 4.2.16 on 2025-02-28 13:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def remove_duplicates(apps, schema_editor):
|
||||
UserKnownLoginSource = apps.get_model("pretixbase", "UserKnownLoginSource")
|
||||
unique_fields = ["user", "agent_type", "device_type", "os_type", "country"]
|
||||
|
||||
duplicates = (
|
||||
UserKnownLoginSource.objects
|
||||
.values(*unique_fields)
|
||||
.order_by()
|
||||
.annotate(latest_id=models.Max('id'), count=models.Count('id'))
|
||||
.filter(count__gt=1)
|
||||
)
|
||||
|
||||
for duplicate in duplicates:
|
||||
(
|
||||
UserKnownLoginSource.objects
|
||||
.filter(**{x: duplicate[x] for x in unique_fields})
|
||||
.exclude(id=duplicate["latest_id"])
|
||||
.delete()
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0277_customerssoclient_require_pkce_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(remove_duplicates, migrations.RunPython.noop),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="userknownloginsource",
|
||||
unique_together={
|
||||
("user", "agent_type", "device_type", "os_type", "country")
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.2.19 on 2025-03-18 09:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0278_login_source_add_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='discount',
|
||||
name='subevent_date_from',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='discount',
|
||||
name='subevent_date_until',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.2.20 on 2025-05-14 14:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0279_discount_event_date_from_discount_event_date_until'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='max_extend',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.2.16 on 2025-05-20 11:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0280_cartposition_max_extend"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="is_remote",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -602,9 +602,6 @@ class UserKnownLoginSource(models.Model):
|
||||
country = FastCountryField(null=True, blank=True)
|
||||
last_seen = models.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'agent_type', 'device_type', 'os_type', 'country')
|
||||
|
||||
|
||||
class StaffSession(models.Model):
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
|
||||
@@ -36,9 +36,7 @@ from django_scopes import ScopedManager
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
|
||||
PositionInfo = namedtuple('PositionInfo',
|
||||
['item_id', 'subevent_id', 'subevent_date_from', 'line_price_gross', 'is_addon_to',
|
||||
'voucher_discount'])
|
||||
PositionInfo = namedtuple('PositionInfo', ['item_id', 'subevent_id', 'line_price_gross', 'is_addon_to', 'voucher_discount'])
|
||||
|
||||
|
||||
class Discount(LoggedModel):
|
||||
@@ -173,17 +171,6 @@ class Discount(LoggedModel):
|
||||
"access to sold-out quota will still receive the discount."),
|
||||
)
|
||||
|
||||
subevent_date_from = models.DateTimeField(
|
||||
verbose_name=pgettext_lazy("subevent", "Available for dates starting from"),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
subevent_date_until = models.DateTimeField(
|
||||
verbose_name=pgettext_lazy("subevent", "Available for dates starting until"),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
# more feature ideas:
|
||||
# - max_usages_per_order
|
||||
# - promote_to_user_if_almost_satisfied
|
||||
@@ -368,15 +355,11 @@ class Discount(LoggedModel):
|
||||
# First, filter out everything not even covered by our product scope
|
||||
condition_candidates = [
|
||||
idx
|
||||
for idx, (item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, voucher_discount) in
|
||||
positions.items()
|
||||
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount) in positions.items()
|
||||
if (
|
||||
(self.condition_all_products or item_id in limit_products) and
|
||||
(self.condition_apply_to_addons or not is_addon_to) and
|
||||
(not self.condition_ignore_voucher_discounted or voucher_discount is None or voucher_discount == Decimal('0.00'))
|
||||
and (not subevent_id or (
|
||||
self.subevent_date_from is None or subevent_date_from >= self.subevent_date_from)) and (
|
||||
self.subevent_date_until is None or subevent_date_from <= self.subevent_date_until)
|
||||
)
|
||||
]
|
||||
|
||||
@@ -386,8 +369,7 @@ class Discount(LoggedModel):
|
||||
benefit_products = {p.pk for p in self.benefit_limit_products.all()}
|
||||
benefit_candidates = [
|
||||
idx
|
||||
for idx, (item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, voucher_discount) in
|
||||
positions.items()
|
||||
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
|
||||
|
||||
@@ -60,7 +60,6 @@ from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import format_html
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
@@ -172,7 +171,7 @@ class EventMixin:
|
||||
self.date_to.astimezone(tz), ("D" if short else "l")
|
||||
)
|
||||
|
||||
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False, try_to_show_times=False) -> str:
|
||||
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date and the end date
|
||||
of the event with respect to the current locale and to the ``show_date_to``
|
||||
@@ -181,48 +180,36 @@ class EventMixin:
|
||||
tz = tz or self.timezone
|
||||
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
|
||||
df, dt = self.date_from, self.date_from
|
||||
show_times = try_to_show_times
|
||||
else:
|
||||
df, dt = self.date_from, self.date_to
|
||||
show_times = try_to_show_times and self.settings.show_times and (
|
||||
# Show times if start and end are on the same day ("08:00-10:00")
|
||||
dt.astimezone(tz).date() == df.astimezone(tz).date() or
|
||||
# Show times if start and end are on consecutive days and less than 24h ("23:00-03:00")
|
||||
(dt.astimezone(tz).date() == df.astimezone(tz).date() + timedelta(days=1) and
|
||||
dt.astimezone(tz).time() < df.astimezone(tz).time())
|
||||
)
|
||||
d = daterange(df.astimezone(tz), dt.astimezone(tz), as_html)
|
||||
|
||||
if show_times:
|
||||
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
|
||||
time_str = _date(self.date_from.astimezone(tz), "TIME_FORMAT")
|
||||
else:
|
||||
time_str = '{}–{}'.format(
|
||||
_date(self.date_from.astimezone(tz), "TIME_FORMAT"),
|
||||
_date(self.date_to.astimezone(tz), "TIME_FORMAT"),
|
||||
)
|
||||
|
||||
if as_html:
|
||||
d = format_html(
|
||||
d + ' <time datetime="{}" data-timezone="{}" data-time-short>{}</time>',
|
||||
self.date_from.isoformat(),
|
||||
str(self.timezone),
|
||||
time_str,
|
||||
)
|
||||
else:
|
||||
d = d + ' ' + time_str
|
||||
|
||||
return d
|
||||
|
||||
def get_date_range_display_with_times(self) -> str: # Helper for usage from templates
|
||||
return self.get_date_range_display(try_to_show_times=True)
|
||||
|
||||
def get_date_range_display_with_times_as_html(self) -> str: # Helper for usage from templates
|
||||
return self.get_date_range_display(try_to_show_times=True, as_html=True)
|
||||
return daterange(df.astimezone(tz), dt.astimezone(tz), as_html)
|
||||
|
||||
def get_date_range_display_as_html(self, tz=None, force_show_end=False) -> str:
|
||||
return self.get_date_range_display(tz, force_show_end, as_html=True)
|
||||
|
||||
def get_time_range_display(self, tz=None, force_show_end=False) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start time and sometimes the end time
|
||||
of the event with respect to the current locale and to the ``show_date_to``
|
||||
setting. Dates are not shown. This is usually used in combination with get_date_range_display
|
||||
"""
|
||||
tz = tz or self.timezone
|
||||
|
||||
show_date_to = self.date_to and (self.settings.show_date_to or force_show_end) and (
|
||||
# Show date to if start and end are on the same day ("08:00-10:00")
|
||||
self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() or
|
||||
# Show date to if start and end are on consecutive days and less than 24h ("23:00-03:00")
|
||||
(self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() + timedelta(days=1) and
|
||||
self.date_to.astimezone(tz).time() < self.date_from.astimezone(tz).time())
|
||||
# Do not show end time if this is a 5-day event because there's no way to make it understandable
|
||||
)
|
||||
if show_date_to:
|
||||
return '{} – {}'.format(
|
||||
_date(self.date_from.astimezone(tz), "TIME_FORMAT"),
|
||||
_date(self.date_to.astimezone(tz), "TIME_FORMAT"),
|
||||
)
|
||||
return _date(self.date_from.astimezone(tz), "TIME_FORMAT")
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
return pytz_deprecation_shim.timezone(self.settings.timezone)
|
||||
@@ -616,11 +603,6 @@ class Event(EventMixin, LoggedModel):
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
)
|
||||
is_remote = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("This event is remote or partially remote."),
|
||||
help_text=_("This will be used to let users know if the event is in a different timezone and let’s us calculate users’ local times."),
|
||||
)
|
||||
geo_lat = models.FloatField(
|
||||
verbose_name=_("Latitude"),
|
||||
null=True, blank=True,
|
||||
|
||||
@@ -821,8 +821,7 @@ class Item(LoggedModel):
|
||||
def ask_attendee_data(self):
|
||||
return self.admission and self.personalized
|
||||
|
||||
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None,
|
||||
include_bundled=False, force_fixed_gross_price=False):
|
||||
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False):
|
||||
price = price if price is not None else self.default_price
|
||||
|
||||
bundled_sum = Decimal('0.00')
|
||||
@@ -851,7 +850,7 @@ class Item(LoggedModel):
|
||||
else:
|
||||
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
|
||||
override_tax_rate=override_tax_rate, currency=currency or self.event.currency,
|
||||
subtract_from_gross=bundled_sum, force_fixed_gross_price=force_fixed_gross_price)
|
||||
subtract_from_gross=bundled_sum)
|
||||
|
||||
if bundled_sum:
|
||||
t.name = "MIXED!"
|
||||
@@ -1837,7 +1836,7 @@ class Question(LoggedModel):
|
||||
))
|
||||
llen = len(answer.split(','))
|
||||
elif all(isinstance(o, QuestionOption) for o in answer):
|
||||
return answer
|
||||
return o
|
||||
else:
|
||||
l_ = list(self.options.filter(
|
||||
Q(pk__in=[a for a in answer if isinstance(a, int) or a.isdigit()]) |
|
||||
@@ -1916,15 +1915,6 @@ class Question(LoggedModel):
|
||||
if event != item.event:
|
||||
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||
|
||||
def clean(self):
|
||||
if self.valid_date_max and self.valid_date_min and self.valid_date_min > self.valid_date_max:
|
||||
raise ValidationError(_("The maximum date must not be before the minimum value."))
|
||||
if self.valid_datetime_max and self.valid_datetime_min and self.valid_datetime_min > self.valid_datetime_max:
|
||||
raise ValidationError(_("The maximum date must not be before the minimum value."))
|
||||
if self.valid_number_max and self.valid_number_min and self.valid_number_min > self.valid_number_max:
|
||||
raise ValidationError(_("The maximum value must not be lower than the minimum value."))
|
||||
super().clean()
|
||||
|
||||
|
||||
class QuestionOption(models.Model):
|
||||
question = models.ForeignKey('Question', related_name='options', on_delete=models.CASCADE)
|
||||
|
||||
@@ -1199,8 +1199,6 @@ class Order(LockModel, LoggedModel):
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2859,8 +2857,6 @@ class OrderPosition(AbstractPosition):
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3098,10 +3094,7 @@ class CartPosition(AbstractPosition):
|
||||
verbose_name=_("Expiration date"),
|
||||
db_index=True
|
||||
)
|
||||
max_extend = models.DateTimeField(
|
||||
verbose_name=_("Limit for extending expiration date"),
|
||||
null=True
|
||||
)
|
||||
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=7, decimal_places=2, default=Decimal('0.00'),
|
||||
verbose_name=_('Tax rate')
|
||||
|
||||
@@ -286,8 +286,6 @@ class WaitingListEntry(LoggedModel):
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
@@ -92,73 +93,15 @@ class PaymentProviderForm(Form):
|
||||
cleaned_data = super().clean()
|
||||
for k, v in self.fields.items():
|
||||
val = cleaned_data.get(k)
|
||||
if hasattr(v, '_required') and v._required and not val:
|
||||
if v._required and not val:
|
||||
self.add_error(k, _('This field is required.'))
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class GiftCardPaymentForm(PaymentProviderForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
self.testmode = kwargs.pop('testmode')
|
||||
self.positions = kwargs.pop('positions')
|
||||
self.used_cards = kwargs.pop('used_cards')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if "code" not in cleaned_data:
|
||||
return cleaned_data
|
||||
|
||||
code = cleaned_data["code"].strip()
|
||||
msg = ""
|
||||
for p in self.positions:
|
||||
if p.item.issue_giftcard:
|
||||
msg = _("You cannot pay with gift cards when buying a gift card.")
|
||||
self.add_error('code', msg)
|
||||
return cleaned_data
|
||||
try:
|
||||
event = self.event
|
||||
gc = event.organizer.accepted_gift_cards.get(
|
||||
secret=code
|
||||
)
|
||||
if gc.currency != event.currency:
|
||||
msg = _("This gift card does not support this currency.")
|
||||
elif gc.testmode and not self.testmode:
|
||||
msg = _("This gift card can only be used in test mode.")
|
||||
elif not gc.testmode and self.testmode:
|
||||
msg = _("Only test gift cards can be used in test mode.")
|
||||
elif gc.expires and gc.expires < time_machine_now():
|
||||
msg = _("This gift card is no longer valid.")
|
||||
elif gc.value <= Decimal("0.00"):
|
||||
msg = _("All credit on this gift card has been used.")
|
||||
|
||||
if msg:
|
||||
self.add_error('code', msg)
|
||||
return cleaned_data
|
||||
|
||||
if gc.pk in self.used_cards:
|
||||
self.add_error('code', _("This gift card is already used for your payment."))
|
||||
return cleaned_data
|
||||
except GiftCard.DoesNotExist:
|
||||
if event.vouchers.filter(code__iexact=code).exists():
|
||||
msg = _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
||||
"the product selection.")
|
||||
self.add_error('code', msg)
|
||||
else:
|
||||
msg = _("This gift card is not known.")
|
||||
self.add_error('code', msg)
|
||||
except GiftCard.MultipleObjectsReturned:
|
||||
msg = _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event.")
|
||||
self.add_error('code', msg)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class BasePaymentProvider:
|
||||
"""
|
||||
This is the base class for all payment providers.
|
||||
"""
|
||||
payment_form_template_name = 'pretixpresale/event/checkout_payment_form_default.html'
|
||||
|
||||
def __init__(self, event: Event):
|
||||
self.event = event
|
||||
@@ -387,18 +330,18 @@ class BasePaymentProvider:
|
||||
label=_('Enable payment method'),
|
||||
required=False,
|
||||
)),
|
||||
('_availability_start',
|
||||
RelativeDateField(
|
||||
label=_('Available from'),
|
||||
help_text=_('Users will not be able to choose this payment provider before the given date.'),
|
||||
required=False,
|
||||
)),
|
||||
('_availability_date',
|
||||
RelativeDateField(
|
||||
label=_('Available until'),
|
||||
help_text=_('Users will not be able to choose this payment provider after the given date.'),
|
||||
required=False,
|
||||
)),
|
||||
('_availability_start',
|
||||
RelativeDateField(
|
||||
label=_('Available from'),
|
||||
help_text=_('Users will not be able to choose this payment provider before the given date.'),
|
||||
required=False,
|
||||
)),
|
||||
('_total_min',
|
||||
forms.DecimalField(
|
||||
label=_('Minimum order total'),
|
||||
@@ -689,6 +632,11 @@ class BasePaymentProvider:
|
||||
the ``_restrict_countries`` and ``_restrict_to_sales_channels`` setting.
|
||||
|
||||
:param total: The total value without the payment method fee, after taxes.
|
||||
|
||||
.. versionchanged:: 1.17.0
|
||||
|
||||
The ``total`` parameter has been added. For backwards compatibility, this method is called again
|
||||
without this parameter if it raises a ``TypeError`` on first try.
|
||||
"""
|
||||
timing = self._is_available_by_time(cart_id=get_or_create_cart_id(request))
|
||||
pricing = True
|
||||
@@ -746,7 +694,7 @@ class BasePaymentProvider:
|
||||
:param order: Only set when this is a change to a new payment method for an existing order.
|
||||
"""
|
||||
form = self.payment_form(request)
|
||||
template = get_template(self.payment_form_template_name)
|
||||
template = get_template('pretixpresale/event/checkout_payment_form_default.html')
|
||||
ctx = {'request': request, 'form': form}
|
||||
return template.render(ctx)
|
||||
|
||||
@@ -1360,9 +1308,6 @@ class OffsettingProvider(BasePaymentProvider):
|
||||
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
|
||||
return _('Balanced against orders: %s' % ', '.join(payment.info_data['orders']))
|
||||
|
||||
def refund_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
|
||||
return self.payment_control_render(request, payment)
|
||||
|
||||
|
||||
class GiftCardPayment(BasePaymentProvider):
|
||||
identifier = "giftcard"
|
||||
@@ -1370,49 +1315,6 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
multi_use_supported = True
|
||||
execute_payment_needs_user = False
|
||||
verbose_name = _("Gift card")
|
||||
payment_form_class = GiftCardPaymentForm
|
||||
payment_form_template_name = 'pretixcontrol/giftcards/checkout.html'
|
||||
|
||||
def payment_form(self, request: HttpRequest) -> Form:
|
||||
# Unfortunately, in payment_form we do not know if we're in checkout
|
||||
# or in an existing order. But we need to do the validation logic in the
|
||||
# form to get the error messages in the right places for accessbility :-(
|
||||
if 'checkout' in request.resolver_match.url_name:
|
||||
cs = cart_session(request)
|
||||
used_cards = [
|
||||
p.get('info_data', {}).get('gift_card')
|
||||
for p in cs.get('payments', [])
|
||||
if p.get('info_data', {}).get('gift_card')
|
||||
]
|
||||
positions = get_cart(request)
|
||||
testmode = self.event.testmode
|
||||
else:
|
||||
used_cards = []
|
||||
order = self.event.orders.get(code=request.resolver_match.kwargs["order"])
|
||||
positions = order.positions.all()
|
||||
testmode = order.testmode
|
||||
|
||||
form = self.payment_form_class(
|
||||
event=self.event,
|
||||
used_cards=used_cards,
|
||||
positions=positions,
|
||||
testmode=testmode,
|
||||
data=(request.POST if request.method == 'POST' and request.POST.get("payment") == self.identifier else None),
|
||||
prefix='payment_%s' % self.identifier,
|
||||
initial={
|
||||
k.replace('payment_%s_' % self.identifier, ''): v
|
||||
for k, v in request.session.items()
|
||||
if k.startswith('payment_%s_' % self.identifier)
|
||||
}
|
||||
)
|
||||
form.fields = self.payment_form_fields
|
||||
|
||||
for k, v in form.fields.items():
|
||||
v._required = v.required
|
||||
v.required = False
|
||||
v.widget.is_required = False
|
||||
|
||||
return form
|
||||
|
||||
@property
|
||||
def public_name(self) -> str:
|
||||
@@ -1445,19 +1347,6 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
f.move_to_end("_enabled", last=False)
|
||||
return f
|
||||
|
||||
@property
|
||||
def payment_form_fields(self):
|
||||
fields = [
|
||||
(
|
||||
"code",
|
||||
forms.CharField(
|
||||
label=_("Gift card code"),
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
return OrderedDict(fields)
|
||||
|
||||
@property
|
||||
def test_mode_message(self) -> str:
|
||||
return _("In test mode, only test cards will work.")
|
||||
@@ -1468,6 +1357,11 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
def order_change_allowed(self, order: Order) -> bool:
|
||||
return super().order_change_allowed(order) and self.event.organizer.has_gift_cards
|
||||
|
||||
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
|
||||
return get_template('pretixcontrol/giftcards/checkout.html').render({
|
||||
'request': request,
|
||||
})
|
||||
|
||||
def checkout_confirm_render(self, request, order=None, info_data=None) -> str:
|
||||
return get_template('pretixcontrol/giftcards/checkout_confirm.html').render({
|
||||
'info_data': info_data,
|
||||
@@ -1535,6 +1429,21 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
def _add_giftcard_to_cart(self, cs, gc):
|
||||
from pretix.base.services.cart import add_payment_to_cart_session
|
||||
|
||||
if gc.currency != self.event.currency:
|
||||
raise ValidationError(_("This gift card does not support this currency."))
|
||||
if gc.testmode and not self.event.testmode:
|
||||
raise ValidationError(_("This gift card can only be used in test mode."))
|
||||
if not gc.testmode and self.event.testmode:
|
||||
raise ValidationError(_("Only test gift cards can be used in test mode."))
|
||||
if gc.expires and gc.expires < time_machine_now():
|
||||
raise ValidationError(_("This gift card is no longer valid."))
|
||||
if gc.value <= Decimal("0.00"):
|
||||
raise ValidationError(_("All credit on this gift card has been used."))
|
||||
|
||||
for p in cs.get('payments', []):
|
||||
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
|
||||
raise ValidationError(_("This gift card is already used for your payment."))
|
||||
|
||||
add_payment_to_cart_session(
|
||||
cs,
|
||||
self,
|
||||
@@ -1546,32 +1455,73 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
)
|
||||
|
||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
||||
form = self.payment_form(request)
|
||||
if not form.is_valid():
|
||||
return False
|
||||
for p in get_cart(request):
|
||||
if p.item.issue_giftcard:
|
||||
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
|
||||
return
|
||||
|
||||
gc = self.event.organizer.accepted_gift_cards.get(
|
||||
secret=form.cleaned_data["code"]
|
||||
)
|
||||
cs = cart_session(request)
|
||||
self._add_giftcard_to_cart(cs, gc)
|
||||
return True
|
||||
try:
|
||||
gc = self.event.organizer.accepted_gift_cards.get(
|
||||
secret=request.POST.get("giftcard").strip()
|
||||
)
|
||||
cs = cart_session(request)
|
||||
try:
|
||||
self._add_giftcard_to_cart(cs, gc)
|
||||
return True
|
||||
except ValidationError as e:
|
||||
messages.error(request, str(e.message))
|
||||
return
|
||||
except GiftCard.DoesNotExist:
|
||||
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
|
||||
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
||||
"the product selection."))
|
||||
else:
|
||||
messages.error(request, _("This gift card is not known."))
|
||||
except GiftCard.MultipleObjectsReturned:
|
||||
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
|
||||
|
||||
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str, None]:
|
||||
form = self.payment_form(request)
|
||||
if not form.is_valid():
|
||||
return False
|
||||
gc = self.event.organizer.accepted_gift_cards.get(
|
||||
secret=form.cleaned_data["code"]
|
||||
)
|
||||
payment.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
'gift_card_secret': gc.secret,
|
||||
'retry': True
|
||||
}
|
||||
payment.amount = min(payment.amount, gc.value)
|
||||
payment.save()
|
||||
return True
|
||||
for p in payment.order.positions.all():
|
||||
if p.item.issue_giftcard:
|
||||
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
|
||||
return
|
||||
|
||||
try:
|
||||
gc = self.event.organizer.accepted_gift_cards.get(
|
||||
secret=request.POST.get("giftcard").strip()
|
||||
)
|
||||
if gc.currency != self.event.currency:
|
||||
messages.error(request, _("This gift card does not support this currency."))
|
||||
return
|
||||
if gc.testmode and not payment.order.testmode:
|
||||
messages.error(request, _("This gift card can only be used in test mode."))
|
||||
return
|
||||
if not gc.testmode and payment.order.testmode:
|
||||
messages.error(request, _("Only test gift cards can be used in test mode."))
|
||||
return
|
||||
if gc.expires and gc.expires < time_machine_now():
|
||||
messages.error(request, _("This gift card is no longer valid."))
|
||||
return
|
||||
if gc.value <= Decimal("0.00"):
|
||||
messages.error(request, _("All credit on this gift card has been used."))
|
||||
return
|
||||
payment.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
'gift_card_secret': gc.secret,
|
||||
'retry': True
|
||||
}
|
||||
payment.amount = min(payment.amount, gc.value)
|
||||
payment.save()
|
||||
|
||||
return True
|
||||
except GiftCard.DoesNotExist:
|
||||
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard").strip()).exists():
|
||||
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
||||
"the product selection."))
|
||||
else:
|
||||
messages.error(request, _("This gift card is not known."))
|
||||
except GiftCard.MultipleObjectsReturned:
|
||||
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
|
||||
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment, is_early_special_case=False) -> str:
|
||||
for p in payment.order.positions.all():
|
||||
|
||||
@@ -45,7 +45,6 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import DatabaseError, transaction
|
||||
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
|
||||
from django.db.models.aggregates import Min
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import (
|
||||
@@ -276,10 +275,7 @@ class CartManager:
|
||||
}
|
||||
|
||||
def __init__(self, event: Event, cart_id: str, sales_channel: SalesChannel,
|
||||
invoice_address: InvoiceAddress=None, widget_data=None, reservation_time: timedelta=None):
|
||||
"""
|
||||
Creates a new CartManager for an event.
|
||||
"""
|
||||
invoice_address: InvoiceAddress=None, widget_data=None, expiry=None):
|
||||
self.event = event
|
||||
self.cart_id = cart_id
|
||||
self.real_now_dt = now()
|
||||
@@ -290,17 +286,11 @@ class CartManager:
|
||||
self._subevents_cache = {}
|
||||
self._variations_cache = {}
|
||||
self._seated_cache = {}
|
||||
self._expiry = None
|
||||
self._explicit_expiry = expiry
|
||||
self.invoice_address = invoice_address
|
||||
self._widget_data = widget_data or {}
|
||||
self._sales_channel = sales_channel
|
||||
self.num_extended_positions = 0
|
||||
|
||||
if reservation_time:
|
||||
self._reservation_time = reservation_time
|
||||
else:
|
||||
self._reservation_time = timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
|
||||
self._expiry = self.real_now_dt + self._reservation_time
|
||||
self._max_expiry_extend = self.real_now_dt + (self._reservation_time * 11)
|
||||
|
||||
@property
|
||||
def positions(self):
|
||||
@@ -315,6 +305,14 @@ class CartManager:
|
||||
self._seated_cache[item, subevent] = item.seat_category_mappings.filter(subevent=subevent).exists()
|
||||
return self._seated_cache[item, subevent]
|
||||
|
||||
def _calculate_expiry(self):
|
||||
if self._explicit_expiry:
|
||||
self._expiry = self._explicit_expiry
|
||||
else:
|
||||
self._expiry = self.real_now_dt + timedelta(
|
||||
minutes=self.event.settings.get('reservation_time', as_type=int)
|
||||
)
|
||||
|
||||
def _check_presale_dates(self):
|
||||
if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
|
||||
raise CartError(error_messages['not_started'])
|
||||
@@ -331,27 +329,9 @@ class CartManager:
|
||||
raise CartError(error_messages['payment_ended'])
|
||||
|
||||
def _extend_expiry_of_valid_existing_positions(self):
|
||||
# real_now_dt is initialized at CartManager instantiation, so it's slightly in the past. Add a small
|
||||
# delta to reduce risk of extending already expired CartPositions.
|
||||
padded_now_dt = self.real_now_dt + timedelta(seconds=5)
|
||||
|
||||
# Make sure we do not extend past the max_extend timestamp, allowing users to extend their valid positions up
|
||||
# to 11 times the reservation time. If we add new positions to the cart while valid positions exist, the new
|
||||
# positions' reservation will also be limited to max_extend of the oldest position.
|
||||
# Only after all positions expire, an ExtendOperation may reset max_extend to another 11x reservation_time.
|
||||
max_extend_existing = self.positions.filter(expires__gt=padded_now_dt).aggregate(m=Min('max_extend'))['m']
|
||||
if max_extend_existing:
|
||||
self._expiry = min(self._expiry, max_extend_existing)
|
||||
self._max_expiry_extend = max_extend_existing
|
||||
|
||||
# Extend this user's cart session to ensure all items in the cart expire at the same time
|
||||
# We can extend the reservation of items which are not yet expired without risk
|
||||
if self._expiry > padded_now_dt:
|
||||
self.num_extended_positions += self.positions.filter(
|
||||
expires__gt=padded_now_dt, expires__lt=self._expiry,
|
||||
).update(
|
||||
expires=self._expiry,
|
||||
)
|
||||
self.positions.filter(expires__gt=self.real_now_dt).update(expires=self._expiry)
|
||||
|
||||
def _delete_out_of_timeframe(self):
|
||||
err = None
|
||||
@@ -1266,7 +1246,6 @@ class CartManager:
|
||||
item=op.item,
|
||||
variation=op.variation,
|
||||
expires=self._expiry,
|
||||
max_extend=self._max_expiry_extend,
|
||||
cart_id=self.cart_id,
|
||||
voucher=op.voucher,
|
||||
addon_to=op.addon_to if op.addon_to else None,
|
||||
@@ -1315,9 +1294,7 @@ class CartManager:
|
||||
event=self.event,
|
||||
item=b.item,
|
||||
variation=b.variation,
|
||||
expires=self._expiry,
|
||||
max_extend=self._max_expiry_extend,
|
||||
cart_id=self.cart_id,
|
||||
expires=self._expiry, cart_id=self.cart_id,
|
||||
voucher=None,
|
||||
addon_to=cp,
|
||||
subevent=b.subevent,
|
||||
@@ -1344,14 +1321,12 @@ class CartManager:
|
||||
op.position.delete()
|
||||
elif available_count == 1:
|
||||
op.position.expires = self._expiry
|
||||
op.position.max_extend = self._max_expiry_extend
|
||||
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()
|
||||
if op.position.pk not in deleted_positions:
|
||||
try:
|
||||
op.position.save(force_update=True, update_fields=['expires', 'max_extend', 'listed_price', 'price_after_voucher'])
|
||||
self.num_extended_positions += 1
|
||||
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
|
||||
@@ -1423,8 +1398,7 @@ class CartManager:
|
||||
self.event,
|
||||
self._sales_channel.identifier,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
|
||||
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||
for cp in positions
|
||||
]
|
||||
)
|
||||
@@ -1441,11 +1415,14 @@ class CartManager:
|
||||
def commit(self):
|
||||
self._check_presale_dates()
|
||||
self._check_max_cart_size()
|
||||
self._calculate_expiry()
|
||||
|
||||
err = self._delete_out_of_timeframe()
|
||||
err = self.extend_expired_positions() or err
|
||||
err = err or self._check_min_per_voucher()
|
||||
|
||||
self.real_now_dt = now()
|
||||
|
||||
self._extend_expiry_of_valid_existing_positions()
|
||||
err = self._perform_operations() or err
|
||||
self.recompute_final_prices_and_taxes()
|
||||
@@ -1654,31 +1631,6 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> dict:
|
||||
"""
|
||||
Resets the expiry time of a cart to the configured reservation time of this event.
|
||||
Limited to 11x the reservation time.
|
||||
|
||||
:param event: The event ID in question
|
||||
:param cart_id: The cart ID of the cart to modify
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
except SalesChannel.DoesNotExist:
|
||||
raise CartError("Invalid sales channel.")
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||
cm.commit()
|
||||
return {"success": cm.num_extended_positions, "expiry": cm._expiry, "max_expiry_extend": cm._max_expiry_extend}
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: List[dict], cart_id: str=None, locale='en',
|
||||
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
|
||||
@@ -120,8 +120,7 @@ class CrossSellingService:
|
||||
self.event,
|
||||
self.sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
|
||||
bool(cp.addon_to), cp.is_bundled,
|
||||
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled,
|
||||
cp.listed_price - cp.price_after_voucher)
|
||||
for cp in self.cartpositions
|
||||
],
|
||||
|
||||
@@ -531,7 +531,7 @@ def send_invoices_to_organizer(sender, **kwargs):
|
||||
if i.event.settings.invoice_email_organizer:
|
||||
with language(i.event.settings.locale):
|
||||
mail(
|
||||
email=[e.strip() for e in i.event.settings.invoice_email_organizer.split(",")],
|
||||
email=i.event.settings.invoice_email_organizer,
|
||||
subject=_('New invoice: {number}').format(number=i.number),
|
||||
template=LazyI18nString.from_gettext(_(
|
||||
'Hello,\n\n'
|
||||
|
||||
@@ -875,8 +875,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
event,
|
||||
sales_channel.identifier,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
|
||||
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||
for cp in sorted_positions
|
||||
]
|
||||
)
|
||||
@@ -1564,7 +1563,6 @@ class OrderChangeManager:
|
||||
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
|
||||
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff'))
|
||||
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
|
||||
ChangeSecretOperation = namedtuple('ChangeSecretOperation', ('position', 'new_secret'))
|
||||
ChangeValidFromOperation = namedtuple('ChangeValidFromOperation', ('position', 'valid_from'))
|
||||
ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until'))
|
||||
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
|
||||
@@ -1672,9 +1670,6 @@ class OrderChangeManager:
|
||||
def regenerate_secret(self, position: OrderPosition):
|
||||
self._operations.append(self.RegenerateSecretOperation(position))
|
||||
|
||||
def change_ticket_secret(self, position: OrderPosition, new_secret: str):
|
||||
self._operations.append(self.ChangeSecretOperation(position, new_secret))
|
||||
|
||||
def change_valid_from(self, position: OrderPosition, new_value: datetime):
|
||||
self._operations.append(self.ChangeValidFromOperation(position, new_value))
|
||||
|
||||
@@ -1689,8 +1684,7 @@ class OrderChangeManager:
|
||||
|
||||
def change_price(self, position: OrderPosition, price: Decimal):
|
||||
tax_rule = self._current_tax_rules().get(position.pk, position.tax_rule) or TaxRule.zero()
|
||||
price = tax_rule.tax(price, base_price_is='gross', invoice_address=self._invoice_address,
|
||||
force_fixed_gross_price=True)
|
||||
price = tax_rule.tax(price, base_price_is='gross')
|
||||
|
||||
if position.issued_gift_cards.exists():
|
||||
raise OrderError(self.error_messages['gift_card_change'])
|
||||
@@ -1755,8 +1749,7 @@ class OrderChangeManager:
|
||||
self._operations.append(self.AddFeeOperation(fee, fee.value))
|
||||
|
||||
def change_fee(self, fee: OrderFee, value: Decimal):
|
||||
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross', invoice_address=self._invoice_address,
|
||||
force_fixed_gross_price=True)
|
||||
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross')
|
||||
self._totaldiff += value.gross - fee.value
|
||||
self._invoice_dirty = True
|
||||
self._operations.append(self.FeeValueOperation(fee, value, value.gross - fee.value))
|
||||
@@ -1791,8 +1784,7 @@ class OrderChangeManager:
|
||||
if price is None:
|
||||
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
|
||||
elif not isinstance(price, TaxedPrice):
|
||||
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address,
|
||||
force_fixed_gross_price=True)
|
||||
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(self.error_messages['tax_rule_country_blocked'])
|
||||
|
||||
@@ -2221,79 +2213,73 @@ class OrderChangeManager:
|
||||
nextposid = self.order.all_positions.aggregate(m=Max('positionid'))['m'] + 1
|
||||
split_positions = []
|
||||
secret_dirty = set()
|
||||
position_cache = {}
|
||||
fee_cache = {}
|
||||
|
||||
for op in self._operations:
|
||||
if isinstance(op, self.ItemOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.item', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'old_item': position.item.pk,
|
||||
'old_variation': position.variation.pk if position.variation else None,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'old_item': op.position.item.pk,
|
||||
'old_variation': op.position.variation.pk if op.position.variation else None,
|
||||
'new_item': op.item.pk,
|
||||
'new_variation': op.variation.pk if op.variation else None,
|
||||
'old_price': position.price,
|
||||
'addon_to': position.addon_to_id,
|
||||
'new_price': position.price
|
||||
'old_price': op.position.price,
|
||||
'addon_to': op.position.addon_to_id,
|
||||
'new_price': op.position.price
|
||||
})
|
||||
position.item = op.item
|
||||
position.variation = op.variation
|
||||
position._calculate_tax()
|
||||
op.position.item = op.item
|
||||
op.position.variation = op.variation
|
||||
op.position._calculate_tax()
|
||||
|
||||
if position.voucher_budget_use is not None and position.voucher and not position.addon_to_id:
|
||||
listed_price = get_listed_price(position.item, position.variation, position.subevent)
|
||||
if not position.item.tax_rule or position.item.tax_rule.price_includes_tax:
|
||||
price_after_voucher = max(position.price, position.voucher.calculate_price(listed_price))
|
||||
if op.position.voucher_budget_use is not None and op.position.voucher and not op.position.addon_to_id:
|
||||
listed_price = get_listed_price(op.position.item, op.position.variation, op.position.subevent)
|
||||
if not op.position.item.tax_rule or op.position.item.tax_rule.price_includes_tax:
|
||||
price_after_voucher = max(op.position.price, op.position.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
price_after_voucher = max(position.price - position.tax_value, position.voucher.calculate_price(listed_price))
|
||||
position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||
secret_dirty.add(position)
|
||||
position.save()
|
||||
price_after_voucher = max(op.position.price - op.position.tax_value, op.position.voucher.calculate_price(listed_price))
|
||||
op.position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||
secret_dirty.add(op.position)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.MembershipOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.membership', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'old_membership_id': position.used_membership_id,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'old_membership_id': op.position.used_membership_id,
|
||||
'new_membership_id': op.membership.pk if op.membership else None,
|
||||
})
|
||||
position.used_membership = op.membership
|
||||
position.save()
|
||||
op.position.used_membership = op.membership
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SeatOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.seat', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'old_seat': position.seat.name if position.seat else "-",
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'old_seat': op.position.seat.name if op.position.seat else "-",
|
||||
'new_seat': op.seat.name if op.seat else "-",
|
||||
'old_seat_id': position.seat.pk if position.seat else None,
|
||||
'old_seat_id': op.position.seat.pk if op.position.seat else None,
|
||||
'new_seat_id': op.seat.pk if op.seat else None,
|
||||
})
|
||||
position.seat = op.seat
|
||||
secret_dirty.add(position)
|
||||
position.save()
|
||||
op.position.seat = op.seat
|
||||
secret_dirty.add(op.position)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SubeventOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'old_subevent': position.subevent.pk,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'old_subevent': op.position.subevent.pk,
|
||||
'new_subevent': op.subevent.pk,
|
||||
'old_price': position.price,
|
||||
'new_price': position.price
|
||||
'old_price': op.position.price,
|
||||
'new_price': op.position.price
|
||||
})
|
||||
position.subevent = op.subevent
|
||||
secret_dirty.add(position)
|
||||
if position.voucher_budget_use is not None and position.voucher and not position.addon_to_id:
|
||||
listed_price = get_listed_price(position.item, position.variation, position.subevent)
|
||||
if not position.item.tax_rule or position.item.tax_rule.price_includes_tax:
|
||||
price_after_voucher = max(position.price, position.voucher.calculate_price(listed_price))
|
||||
op.position.subevent = op.subevent
|
||||
secret_dirty.add(op.position)
|
||||
if op.position.voucher_budget_use is not None and op.position.voucher and not op.position.addon_to_id:
|
||||
listed_price = get_listed_price(op.position.item, op.position.variation, op.position.subevent)
|
||||
if not op.position.item.tax_rule or op.position.item.tax_rule.price_includes_tax:
|
||||
price_after_voucher = max(op.position.price, op.position.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
price_after_voucher = max(position.price - position.tax_value, position.voucher.calculate_price(listed_price))
|
||||
position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||
position.save()
|
||||
price_after_voucher = max(op.position.price - op.position.tax_value, op.position.voucher.calculate_price(listed_price))
|
||||
op.position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||
op.position.save()
|
||||
elif isinstance(op, self.AddFeeOperation):
|
||||
self.order.log_action('pretix.event.order.changed.addfee', user=self.user, auth=self.auth, data={
|
||||
'fee': op.fee.pk,
|
||||
@@ -2302,79 +2288,70 @@ class OrderChangeManager:
|
||||
op.fee._calculate_tax()
|
||||
op.fee.save()
|
||||
elif isinstance(op, self.FeeValueOperation):
|
||||
fee = fee_cache.setdefault(op.fee.pk, op.fee)
|
||||
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
|
||||
'fee': fee.pk,
|
||||
'old_price': fee.value,
|
||||
'fee': op.fee.pk,
|
||||
'old_price': op.fee.value,
|
||||
'new_price': op.value.gross
|
||||
})
|
||||
fee.value = op.value.gross
|
||||
fee._calculate_tax()
|
||||
fee.save()
|
||||
op.fee.value = op.value.gross
|
||||
op.fee._calculate_tax()
|
||||
op.fee.save()
|
||||
elif isinstance(op, self.PriceOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'old_price': position.price,
|
||||
'addon_to': position.addon_to_id,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'old_price': op.position.price,
|
||||
'addon_to': op.position.addon_to_id,
|
||||
'new_price': op.price.gross
|
||||
})
|
||||
position.price = op.price.gross
|
||||
position.tax_rate = op.price.rate
|
||||
position.tax_value = op.price.tax
|
||||
position.tax_code = op.price.code
|
||||
position.save(update_fields=['price', 'tax_rate', 'tax_value', 'tax_code'])
|
||||
op.position.price = op.price.gross
|
||||
op.position.tax_rate = op.price.rate
|
||||
op.position.tax_value = op.price.tax
|
||||
op.position.tax_code = op.price.code
|
||||
op.position.save()
|
||||
elif isinstance(op, self.TaxRuleOperation):
|
||||
if isinstance(op.position, OrderPosition):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'addon_to': position.addon_to_id,
|
||||
'old_taxrule': position.tax_rule.pk if position.tax_rule else None,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'addon_to': op.position.addon_to_id,
|
||||
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rule else None,
|
||||
'new_taxrule': op.tax_rule.pk
|
||||
})
|
||||
position._calculate_tax(op.tax_rule)
|
||||
position.save()
|
||||
elif isinstance(op.position, OrderFee):
|
||||
fee = fee_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
|
||||
'fee': fee.pk,
|
||||
'fee_type': fee.fee_type,
|
||||
'old_taxrule': fee.tax_rule.pk if fee.tax_rule else None,
|
||||
'fee': op.position.pk,
|
||||
'fee_type': op.position.fee_type,
|
||||
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rule else None,
|
||||
'new_taxrule': op.tax_rule.pk
|
||||
})
|
||||
fee._calculate_tax(op.tax_rule)
|
||||
fee.save()
|
||||
op.position._calculate_tax(op.tax_rule)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.CancelFeeOperation):
|
||||
fee = fee_cache.setdefault(op.fee.pk, op.fee)
|
||||
self.order.log_action('pretix.event.order.changed.cancelfee', user=self.user, auth=self.auth, data={
|
||||
'fee': fee.pk,
|
||||
'fee_type': fee.fee_type,
|
||||
'old_price': fee.value,
|
||||
'fee': op.fee.pk,
|
||||
'fee_type': op.fee.fee_type,
|
||||
'old_price': op.fee.value,
|
||||
})
|
||||
fee.canceled = True
|
||||
fee.save(update_fields=['canceled'])
|
||||
op.fee.canceled = True
|
||||
op.fee.save(update_fields=['canceled'])
|
||||
elif isinstance(op, self.CancelOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
for gc in position.issued_gift_cards.all():
|
||||
for gc in op.position.issued_gift_cards.all():
|
||||
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
|
||||
if gc.value < position.price:
|
||||
if gc.value < op.position.price:
|
||||
raise OrderError(_(
|
||||
'A position can not be canceled since the gift card {card} purchased in this order has '
|
||||
'already been redeemed.').format(
|
||||
card=gc.secret
|
||||
))
|
||||
else:
|
||||
gc.transactions.create(value=-position.price, order=self.order, acceptor=self.order.event.organizer)
|
||||
gc.transactions.create(value=-op.position.price, order=self.order, acceptor=self.order.event.organizer)
|
||||
|
||||
for m in position.granted_memberships.with_usages().all():
|
||||
for m in op.position.granted_memberships.with_usages().all():
|
||||
m.canceled = True
|
||||
m.save()
|
||||
|
||||
for opa in position.addons.all():
|
||||
opa = position_cache.setdefault(opa.pk, opa)
|
||||
for opa in op.position.addons.all():
|
||||
for gc in opa.issued_gift_cards.all():
|
||||
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
|
||||
if gc.value < opa.position.price:
|
||||
@@ -2408,22 +2385,22 @@ class OrderChangeManager:
|
||||
)
|
||||
opa.save(update_fields=['canceled', 'secret'])
|
||||
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'old_item': position.item.pk,
|
||||
'old_variation': position.variation.pk if position.variation else None,
|
||||
'old_price': position.price,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'old_item': op.position.item.pk,
|
||||
'old_variation': op.position.variation.pk if op.position.variation else None,
|
||||
'old_price': op.position.price,
|
||||
'addon_to': None,
|
||||
})
|
||||
position.canceled = True
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
op.position.canceled = True
|
||||
if op.position.voucher:
|
||||
Voucher.objects.filter(pk=op.position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
||||
event=self.event, position=op.position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
|
||||
)
|
||||
if position in secret_dirty:
|
||||
secret_dirty.remove(position)
|
||||
position.save(update_fields=['canceled', 'secret'])
|
||||
if op.position in secret_dirty:
|
||||
secret_dirty.remove(op.position)
|
||||
op.position.save(update_fields=['canceled', 'secret'])
|
||||
elif isinstance(op, self.AddOperation):
|
||||
pos = OrderPosition.objects.create(
|
||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||
@@ -2448,28 +2425,13 @@ class OrderChangeManager:
|
||||
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
|
||||
})
|
||||
elif isinstance(op, self.SplitOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
split_positions.append(position)
|
||||
split_positions.append(op.position)
|
||||
elif isinstance(op, self.RegenerateSecretOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
position.web_secret = generate_secret()
|
||||
position.save(update_fields=["web_secret"])
|
||||
op.position.web_secret = generate_secret()
|
||||
op.position.save(update_fields=["web_secret"])
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=position, force_invalidate=True, save=True
|
||||
event=self.event, position=op.position, force_invalidate=True, save=True
|
||||
)
|
||||
if position in secret_dirty:
|
||||
secret_dirty.remove(position)
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
|
||||
'order': self.order.pk})
|
||||
self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
})
|
||||
elif isinstance(op, self.ChangeSecretOperation):
|
||||
if OrderPosition.all.filter(order__event=self.event, secret=op.new_secret).exists():
|
||||
raise OrderError('You cannot assign a position secret that already exists.')
|
||||
op.position.secret = op.new_secret
|
||||
op.position.save(update_fields=["secret"])
|
||||
if op.position in secret_dirty:
|
||||
secret_dirty.remove(op.position)
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
|
||||
@@ -2479,68 +2441,64 @@ class OrderChangeManager:
|
||||
'positionid': op.position.positionid,
|
||||
})
|
||||
elif isinstance(op, self.ChangeValidFromOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.valid_from', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'new_value': op.valid_from.isoformat() if op.valid_from else None,
|
||||
'old_value': position.valid_from.isoformat() if position.valid_from else None,
|
||||
'old_value': op.position.valid_from.isoformat() if op.position.valid_from else None,
|
||||
})
|
||||
position.valid_from = op.valid_from
|
||||
position.save(update_fields=['valid_from'])
|
||||
secret_dirty.add(position)
|
||||
op.position.valid_from = op.valid_from
|
||||
op.position.save(update_fields=['valid_from'])
|
||||
secret_dirty.add(op.position)
|
||||
elif isinstance(op, self.ChangeValidUntilOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.valid_until', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'new_value': op.valid_until.isoformat() if op.valid_until else None,
|
||||
'old_value': position.valid_until.isoformat() if position.valid_until else None,
|
||||
'old_value': op.position.valid_until.isoformat() if op.position.valid_until else None,
|
||||
})
|
||||
position.valid_until = op.valid_until
|
||||
position.save(update_fields=['valid_until'])
|
||||
secret_dirty.add(position)
|
||||
op.position.valid_until = op.valid_until
|
||||
op.position.save(update_fields=['valid_until'])
|
||||
secret_dirty.add(op.position)
|
||||
elif isinstance(op, self.AddBlockOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.add_block', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'block_name': op.block_name,
|
||||
})
|
||||
if position.blocked:
|
||||
if op.block_name not in position.blocked:
|
||||
position.blocked = position.blocked + [op.block_name]
|
||||
if op.position.blocked:
|
||||
if op.block_name not in op.position.blocked:
|
||||
op.position.blocked = op.position.blocked + [op.block_name]
|
||||
else:
|
||||
position.blocked = [op.block_name]
|
||||
op.position.blocked = [op.block_name]
|
||||
if op.ignore_from_quota_while_blocked is not None:
|
||||
position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
|
||||
position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
|
||||
if position.blocked:
|
||||
position.blocked_secrets.update_or_create(
|
||||
op.position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
|
||||
op.position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
|
||||
if op.position.blocked:
|
||||
op.position.blocked_secrets.update_or_create(
|
||||
event=self.event,
|
||||
secret=position.secret,
|
||||
secret=op.position.secret,
|
||||
defaults={
|
||||
'blocked': True,
|
||||
'updated': now(),
|
||||
}
|
||||
)
|
||||
elif isinstance(op, self.RemoveBlockOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.remove_block', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'position': op.position.pk,
|
||||
'positionid': op.position.positionid,
|
||||
'block_name': op.block_name,
|
||||
})
|
||||
if position.blocked and op.block_name in position.blocked:
|
||||
position.blocked = [b for b in position.blocked if b != op.block_name]
|
||||
if not position.blocked:
|
||||
position.blocked = None
|
||||
if op.position.blocked and op.block_name in op.position.blocked:
|
||||
op.position.blocked = [b for b in op.position.blocked if b != op.block_name]
|
||||
if not op.position.blocked:
|
||||
op.position.blocked = None
|
||||
if op.ignore_from_quota_while_blocked is not None:
|
||||
position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
|
||||
position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
|
||||
if not position.blocked:
|
||||
op.position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
|
||||
op.position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
|
||||
if not op.position.blocked:
|
||||
try:
|
||||
bs = position.blocked_secrets.get(secret=position.secret)
|
||||
bs = op.position.blocked_secrets.get(secret=op.position.secret)
|
||||
bs.blocked = False
|
||||
bs.save()
|
||||
except BlockedTicketSecret.DoesNotExist:
|
||||
@@ -2768,11 +2726,7 @@ class OrderChangeManager:
|
||||
|
||||
def _check_complete_cancel(self):
|
||||
current = self.order.positions.count()
|
||||
cancels = sum([
|
||||
1 + o.position.addons.count() for o in self._operations if isinstance(o, self.CancelOperation)
|
||||
]) + len([
|
||||
o for o in self._operations if isinstance(o, self.SplitOperation)
|
||||
])
|
||||
cancels = len([o for o in self._operations if isinstance(o, (self.CancelOperation, self.SplitOperation))])
|
||||
adds = len([o for o in self._operations if isinstance(o, self.AddOperation)])
|
||||
if current > 0 and current - cancels + adds < 1:
|
||||
raise OrderError(self.error_messages['complete_cancel'])
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
@@ -163,14 +162,14 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
|
||||
|
||||
|
||||
def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
|
||||
positions: List[Tuple[int, Optional[int], Optional[datetime], Decimal, bool, bool, Decimal]],
|
||||
positions: List[Tuple[int, Optional[int], Decimal, bool, bool, Decimal]],
|
||||
collect_potential_discounts: Optional[defaultdict]=None) -> List[Tuple[Decimal, Optional[Discount]]]:
|
||||
"""
|
||||
Applies any dynamic discounts to a cart
|
||||
|
||||
:param event: Event the cart belongs to
|
||||
:param sales_channel: Sales channel the cart was created with
|
||||
:param positions: Tuple of the form ``(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
|
||||
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
|
||||
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
|
||||
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
|
||||
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
|
||||
@@ -192,14 +191,12 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
|
||||
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
||||
for discount in discount_qs:
|
||||
result = discount.apply({
|
||||
idx: PositionInfo(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, voucher_discount)
|
||||
for
|
||||
idx, (item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)
|
||||
in enumerate(positions)
|
||||
idx: PositionInfo(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
|
||||
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount) in enumerate(positions)
|
||||
if not is_bundled and idx not in new_prices
|
||||
}, collect_potential_discounts)
|
||||
for k in result.keys():
|
||||
result[k] = (result[k], discount)
|
||||
new_prices.update(result)
|
||||
|
||||
return [new_prices.get(idx, (p[3], None)) for idx, p in enumerate(positions)]
|
||||
return [new_prices.get(idx, (p[2], None)) for idx, p in enumerate(positions)]
|
||||
|
||||
@@ -134,13 +134,13 @@ def order_overview(
|
||||
qs = qs.filter(item__admission=True)
|
||||
items = items.filter(admission=True)
|
||||
|
||||
if date_from and isinstance(date_from, date) and not isinstance(date_from, datetime):
|
||||
if date_from and isinstance(date_from, date):
|
||||
date_from = make_aware(datetime.combine(
|
||||
date_from,
|
||||
time(hour=0, minute=0, second=0, microsecond=0)
|
||||
), event.timezone)
|
||||
|
||||
if date_until and isinstance(date_until, date) and not isinstance(date_until, datetime):
|
||||
if date_until and isinstance(date_until, date):
|
||||
date_until = make_aware(datetime.combine(
|
||||
date_until + timedelta(days=1),
|
||||
time(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
@@ -62,9 +62,6 @@ class VATIDTemporaryError(VATIDError):
|
||||
|
||||
def _validate_vat_id_NO(vat_id, country_code):
|
||||
# Inspired by vat_moss library
|
||||
if not vat_id.startswith("NO"):
|
||||
# prefix is not usually used in Norway, but expected by vat_moss library
|
||||
vat_id = "NO" + vat_id
|
||||
try:
|
||||
vat_id = vat_moss.id.normalize(vat_id)
|
||||
except ValueError:
|
||||
|
||||
@@ -71,7 +71,6 @@ from pretix.base.reldate import (
|
||||
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
|
||||
SerializerRelativeDateField, SerializerRelativeDateTimeField,
|
||||
)
|
||||
from pretix.base.validators import multimail_validate
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
|
||||
)
|
||||
@@ -1234,18 +1233,14 @@ DEFAULTS = {
|
||||
'invoice_email_organizer': {
|
||||
'default': '',
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'form_class': forms.EmailField,
|
||||
'serializer_class': serializers.EmailField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Email address to receive a copy of each invoice"),
|
||||
help_text=_("Each newly created invoice will be sent to this email address shortly after creation. You can "
|
||||
"use this for an automated import of invoices to your accounting system. The invoice will be "
|
||||
"the only attachment of the email."),
|
||||
validators=[multimail_validate],
|
||||
),
|
||||
'serializer_kwargs': dict(
|
||||
validators=[multimail_validate],
|
||||
),
|
||||
)
|
||||
},
|
||||
'show_items_outside_presale_period': {
|
||||
'default': 'True',
|
||||
@@ -2063,38 +2058,6 @@ DEFAULTS = {
|
||||
),
|
||||
'serializer_class': I18nURLField,
|
||||
},
|
||||
'accessibility_url': {
|
||||
'default': None,
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nURLFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Accessibility information URL"),
|
||||
help_text=_("This should point e.g. to a part of your website that explains how your ticket shop complies "
|
||||
"with accessibility regulation."),
|
||||
widget=I18nTextInput,
|
||||
),
|
||||
'serializer_class': I18nURLField,
|
||||
},
|
||||
'accessibility_title': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("Accessibility information")),
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Accessibility information title"),
|
||||
widget=I18nTextInput,
|
||||
),
|
||||
'serializer_class': I18nURLField,
|
||||
},
|
||||
'accessibility_text': {
|
||||
'default': None,
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Accessibility information text"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
),
|
||||
'serializer_class': I18nURLField,
|
||||
},
|
||||
'confirm_texts': {
|
||||
'default': LazyI18nStringList(),
|
||||
'type': LazyI18nStringList,
|
||||
@@ -2145,7 +2108,7 @@ DEFAULTS = {
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Event description"),
|
||||
widget=I18nTextarea,
|
||||
widget=I18nMarkdownTextarea,
|
||||
help_text=_(
|
||||
"You can use this to share information with your attendees, such as travel information or the link to a digital event. "
|
||||
"If you keep it empty, we will put a link to the event shop, the admission time, and your organizer name in there. "
|
||||
@@ -2815,7 +2778,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
),
|
||||
},
|
||||
'theme_color_success': {
|
||||
'default': '#408252',
|
||||
'default': '#50a167',
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
@@ -2920,8 +2883,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
||||
'in the page header. If you use a white background, we show your logo with a size of up '
|
||||
'to 1140x120 pixels. Otherwise the maximum size is 1120x120 pixels. You '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
),
|
||||
@@ -2964,8 +2926,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
'in the page header. If you use a white background, we show your logo with a size of up '
|
||||
'to 1140x120 pixels. Otherwise the maximum size is 1120x120 pixels. You '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
),
|
||||
@@ -3026,7 +2987,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
||||
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
||||
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
|
||||
'if only the center square is shown. If you do not fill this, we will use the logo given above.')
|
||||
'only the center square is shown. If you do not fill this, we will use the logo given above.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
@@ -3330,8 +3291,6 @@ Your {organizer} team""")) # noqa: W291
|
||||
label=_('Validity of gift card codes in years'),
|
||||
help_text=_('If you set a number here, gift cards will by default expire at the end of the year after this '
|
||||
'many years. If you keep it empty, gift cards do not have an explicit expiry date.'),
|
||||
min_value=0,
|
||||
max_value=99,
|
||||
)
|
||||
},
|
||||
'cookie_consent': {
|
||||
@@ -3744,21 +3703,13 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
|
||||
# are actually *used* in postal addresses. This is obviously not complete and opinionated.
|
||||
# Country: [(List of subdivision types as defined by pycountry), (short or long form to be used)]
|
||||
'AU': (['State', 'Territory'], 'short'),
|
||||
'BR': (['Federal district', 'State'], 'short'),
|
||||
'BR': (['State'], 'short'),
|
||||
'CA': (['Province', 'Territory'], 'short'),
|
||||
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
|
||||
'JP': (['Prefecture'], 'long'),
|
||||
'MY': (['State', 'Federal territory'], 'long'),
|
||||
'MX': (['State', 'Federal district'], 'short'),
|
||||
'US': (['State', 'Outlying area', 'District'], 'short'),
|
||||
'IT': (['Province', 'Free municipal consortium', 'Metropolitan city', 'Autonomous province',
|
||||
'Free municipal consortium', 'Decentralized regional entity'], 'short'),
|
||||
}
|
||||
COUNTRY_STATE_LABEL = {
|
||||
# Countries in which the "State" field should not be called "State"
|
||||
'CA': pgettext_lazy('address', 'Province'),
|
||||
'JP': pgettext_lazy('address', 'Prefecture'),
|
||||
'IT': pgettext_lazy('address', 'Province'),
|
||||
}
|
||||
|
||||
settings_hierarkey = Hierarkey(attribute_name='settings')
|
||||
|
||||
@@ -46,6 +46,7 @@ app_cache = {}
|
||||
|
||||
|
||||
def _populate_app_cache():
|
||||
global app_cache
|
||||
apps.check_apps_ready()
|
||||
for ac in apps.app_configs.values():
|
||||
app_cache[ac.name] = ac
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<code>Host: {{ request.headers.Host }}</code>
|
||||
{% if xfh %}
|
||||
<br>
|
||||
<code>X-Forwarded-Host: {{ xfh }}</code>
|
||||
<code>X-Forwarded-For: {{ xfh }}</code>
|
||||
{% if not settings.USE_X_FORWARDED_HOST %}({% trans "ignored" %}){% endif %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{% load i18n %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 18 14"
|
||||
class="{{ cls }}" role="img" aria-label="{% trans "Seat" %}">
|
||||
<path d="M7.713 3.573c-.787-.124-1.677.472-1.511 1.529l.857 3.473c.116.579.578 1.086 1.317 1.086h3.166v3.504c0 1.108 1.556 1.113 1.556.019V8.682c0-.536-.376-1.116-1.099-1.116L9.52 7.563l-.752-2.936c-.147-.648-.583-.981-1.055-1.055v.001Z"></path>
|
||||
<path d="M4.48 5.835a.6.6 0 0 0-.674.725l.71 3.441c.287 1.284 1.39 2.114 2.495 2.114h2.273c.807 0 .811-1.215-.01-1.215H7.188c-.753 0-1.375-.45-1.563-1.289l-.672-3.293c-.062-.3-.26-.452-.474-.483ZM7.433.102a1.468 1.468 0 1 0 0 2.937 1.469 1.469 0 1 0 0-2.937Z"></path>
|
||||
</svg>
|
||||
@@ -4,7 +4,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=false">
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #eee;
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
{{ event.name }}
|
||||
<br>
|
||||
{% if event.has_subevents and ev.name|upper != event.name|upper %}{{ ev.name }}<br>{% endif %}
|
||||
{{ ev.get_date_range_display_with_times }}
|
||||
{{ ev.get_date_range_display }}
|
||||
{% if event.settings.show_times %}
|
||||
{{ ev.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -104,7 +107,10 @@
|
||||
{% if groupkey.2.name|upper != event.name|upper %}
|
||||
{{ groupkey.2.name }} ·
|
||||
{% endif %}
|
||||
{{ groupkey.2.get_date_range_display_with_times }}
|
||||
{{ groupkey.2.get_date_range_display }}
|
||||
{% if event.settings.show_times %}
|
||||
{{ groupkey.2.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
{% if groupkey.2.location %}
|
||||
<br>
|
||||
{{ groupkey.2.location|oneline }}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django import template
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from pretix.helpers.templatetags.simple_block_tag import (
|
||||
register_simple_block_tag,
|
||||
)
|
||||
|
||||
from django.utils.translation import gettext_lazy as _ # NOQA
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register_simple_block_tag(register)
|
||||
def dialog(content, html_id, title, description, *args, **kwargs):
|
||||
format_kwargs = {
|
||||
"id": html_id,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"icon": format_html('<div class="modal-card-icon"><span class="fa fa-{}" aria-hidden="true"></span></div>', kwargs["icon"]) if "icon" in kwargs else "",
|
||||
"alert": mark_safe('role="alertdialog"') if kwargs.get("alert", "False") != "False" else "",
|
||||
"content": content,
|
||||
}
|
||||
result = """
|
||||
<dialog {alert}
|
||||
id="{id}" class="modal-card"
|
||||
aria-labelledby="{id}-title"
|
||||
aria-describedby="{id}-description">
|
||||
<form method="dialog" class="modal-card-inner form-horizontal">
|
||||
{icon}
|
||||
<div class="modal-card-content">
|
||||
<h2 id="{id}-title" class="modal-card-title h3">{title}</h2>
|
||||
<p id="{id}-description" class="modal-card-description">{description}</p>
|
||||
{content}
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
"""
|
||||
return format_html(result, **format_kwargs)
|
||||
@@ -73,7 +73,6 @@ class EventSlugBanlistValidator(BanlistValidator):
|
||||
'customer',
|
||||
'account',
|
||||
'lead',
|
||||
'accessibility',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -21,15 +21,12 @@
|
||||
#
|
||||
import pycountry
|
||||
from django.http import JsonResponse
|
||||
from django.utils.translation import pgettext
|
||||
|
||||
from pretix.base.addressvalidation import (
|
||||
COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED,
|
||||
)
|
||||
from pretix.base.models.tax import VAT_ID_COUNTRIES
|
||||
from pretix.base.settings import (
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
|
||||
)
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
|
||||
|
||||
def states(request):
|
||||
@@ -38,11 +35,7 @@ def states(request):
|
||||
'street': {'required': True},
|
||||
'zipcode': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
|
||||
'city': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
|
||||
'state': {
|
||||
'visible': cc in COUNTRIES_WITH_STATE_IN_ADDRESS,
|
||||
'required': cc in COUNTRIES_WITH_STATE_IN_ADDRESS,
|
||||
'label': COUNTRY_STATE_LABEL.get(cc, pgettext('address', 'State')),
|
||||
},
|
||||
'state': {'visible': cc in COUNTRIES_WITH_STATE_IN_ADDRESS, 'required': cc in COUNTRIES_WITH_STATE_IN_ADDRESS},
|
||||
'vat_id': {'visible': cc in VAT_ID_COUNTRIES, 'required': False},
|
||||
}
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
|
||||
@@ -80,4 +80,4 @@ def serve_metrics(request):
|
||||
|
||||
content = "\n".join(output) + "\n"
|
||||
|
||||
return HttpResponse(content, content_type="text/plain;version=1.0.0;escaping=allow-utf-8")
|
||||
return HttpResponse(content)
|
||||
|
||||
@@ -68,7 +68,7 @@ class AsyncMixin:
|
||||
def get_check_url(self, task_id, ajax):
|
||||
return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '')
|
||||
|
||||
def _ajax_response_data(self, value):
|
||||
def _ajax_response_data(self):
|
||||
return {}
|
||||
|
||||
def _return_ajax_result(self, res, timeout=.5):
|
||||
@@ -85,7 +85,7 @@ class AsyncMixin:
|
||||
logger.warning('Ignored ResponseError in AsyncResult.get()')
|
||||
except ConnectionError:
|
||||
# Redis probably just restarted, let's just report not ready and retry next time
|
||||
data = self._ajax_response_data(None)
|
||||
data = self._ajax_response_data()
|
||||
data.update({
|
||||
'async_id': res.id,
|
||||
'ready': False
|
||||
@@ -93,7 +93,7 @@ class AsyncMixin:
|
||||
return data
|
||||
|
||||
state, info = res.state, res.info
|
||||
data = self._ajax_response_data(info)
|
||||
data = self._ajax_response_data()
|
||||
data.update({
|
||||
'async_id': res.id,
|
||||
'ready': ready,
|
||||
@@ -102,21 +102,23 @@ class AsyncMixin:
|
||||
if ready:
|
||||
if state == states.SUCCESS and not isinstance(info, Exception):
|
||||
smes = self.get_success_message(info)
|
||||
if smes and 'ajax_dont_redirect' not in self.request.GET and 'ajax_dont_redirect' not in self.request.POST:
|
||||
if smes:
|
||||
messages.success(self.request, smes)
|
||||
# TODO: Do not store message if the ajax client states that it will not redirect
|
||||
# but handle the message itself
|
||||
data.update({
|
||||
'redirect': self.get_success_url(info),
|
||||
'success': True,
|
||||
'message': str(smes)
|
||||
'message': str(self.get_success_message(info))
|
||||
})
|
||||
else:
|
||||
smes = self.get_error_message(info)
|
||||
if smes and 'ajax_dont_redirect' not in self.request.GET and 'ajax_dont_redirect' not in self.request.POST:
|
||||
messages.error(self.request, smes)
|
||||
messages.error(self.request, self.get_error_message(info))
|
||||
# TODO: Do not store message if the ajax client states that it will not redirect
|
||||
# but handle the message itself
|
||||
data.update({
|
||||
'redirect': self.get_error_url(),
|
||||
'success': False,
|
||||
'message': str(smes)
|
||||
'message': str(self.get_error_message(info))
|
||||
})
|
||||
elif state == 'PROGRESS':
|
||||
data.update({
|
||||
|
||||
@@ -43,7 +43,7 @@ from django.utils.translation import get_language
|
||||
from django_scopes import scope
|
||||
|
||||
from pretix.base.models.auth import StaffSession
|
||||
from pretix.base.settings import COUNTRY_STATE_LABEL, GlobalSettingsObject
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.control.navigation import (
|
||||
get_event_navigation, get_global_navigation, get_organizer_navigation,
|
||||
)
|
||||
@@ -81,13 +81,13 @@ def _default_context(request):
|
||||
'DEBUG': settings.DEBUG,
|
||||
}
|
||||
_html_head = []
|
||||
if getattr(request, 'event', None) and request.user.is_authenticated:
|
||||
if hasattr(request, 'event') and request.user.is_authenticated:
|
||||
for receiver, response in html_head.send(request.event, request=request):
|
||||
_html_head.append(response)
|
||||
ctx['html_head'] = "".join(_html_head)
|
||||
|
||||
_js_payment_weekdays_disabled = '[]'
|
||||
if getattr(request, 'event', None) and getattr(request, 'organizer', None) and request.user.is_authenticated:
|
||||
if getattr(request, 'event', None) and hasattr(request, 'organizer') and request.user.is_authenticated:
|
||||
ctx['nav_items'] = get_event_navigation(request)
|
||||
|
||||
if request.event.settings.get('payment_term_weekdays'):
|
||||
@@ -140,7 +140,6 @@ def _default_context(request):
|
||||
ctx['js_time_format'] = get_javascript_format('TIME_INPUT_FORMATS')
|
||||
ctx['js_locale'] = get_moment_locale()
|
||||
ctx['select2locale'] = get_language()[:2]
|
||||
ctx['COUNTRY_STATE_LABEL'] = COUNTRY_STATE_LABEL
|
||||
|
||||
ctx['warning_update_available'] = False
|
||||
ctx['warning_update_check_active'] = False
|
||||
|
||||
@@ -45,8 +45,6 @@ class DiscountForm(I18nModelForm):
|
||||
'limit_sales_channels',
|
||||
'available_from',
|
||||
'available_until',
|
||||
'subevent_date_from',
|
||||
'subevent_date_until',
|
||||
'subevent_mode',
|
||||
'condition_all_products',
|
||||
'condition_limit_products',
|
||||
@@ -64,8 +62,6 @@ class DiscountForm(I18nModelForm):
|
||||
field_classes = {
|
||||
'available_from': SplitDateTimeField,
|
||||
'available_until': SplitDateTimeField,
|
||||
'subevent_date_from': SplitDateTimeField,
|
||||
'subevent_date_until': SplitDateTimeField,
|
||||
'condition_limit_products': ItemMultipleChoiceField,
|
||||
'benefit_limit_products': ItemMultipleChoiceField,
|
||||
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||
@@ -74,8 +70,6 @@ class DiscountForm(I18nModelForm):
|
||||
'subevent_mode': forms.RadioSelect,
|
||||
'available_from': SplitDateTimePickerWidget(),
|
||||
'available_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_available_from_0'}),
|
||||
'subevent_date_from': SplitDateTimePickerWidget(),
|
||||
'subevent_date_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_subevent_date_from_0'}),
|
||||
'condition_limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '<[name$=all_products]',
|
||||
'class': 'scrolling-multiple-choice',
|
||||
|
||||
@@ -175,7 +175,6 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
'presale_start',
|
||||
'presale_end',
|
||||
'location',
|
||||
'is_remote',
|
||||
'geo_lat',
|
||||
'geo_lon',
|
||||
]
|
||||
@@ -449,7 +448,6 @@ class EventUpdateForm(I18nModelForm):
|
||||
'presale_start',
|
||||
'presale_end',
|
||||
'location',
|
||||
'is_remote',
|
||||
'geo_lat',
|
||||
'geo_lon',
|
||||
'all_sales_channels',
|
||||
@@ -1477,9 +1475,7 @@ 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 and country_code not in {"IT"}:
|
||||
# Special case for Italy: Provinces are used in addresses, but are too low-level to
|
||||
# have influence on taxes, so we avoid the bloat in the list of selectable countries.
|
||||
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)
|
||||
|
||||
@@ -70,7 +70,6 @@ from pretix.helpers.database import (
|
||||
)
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
from pretix.helpers.i18n import get_format_without_seconds, i18ncomp
|
||||
from pretix.helpers.models import flatten_choices
|
||||
|
||||
PAYMENT_PROVIDERS = []
|
||||
|
||||
@@ -178,10 +177,10 @@ class FilterForm(forms.Form):
|
||||
elif isinstance(v, Model):
|
||||
val = '"' + str(v) + '"'
|
||||
elif isinstance(f, forms.MultipleChoiceField):
|
||||
valdict = dict(flatten_choices(f.choices))
|
||||
valdict = dict(f.choices)
|
||||
val = ' or '.join([str(valdict.get(m)) for m in v])
|
||||
elif isinstance(f, forms.ChoiceField):
|
||||
val = str(dict(flatten_choices(f.choices)).get(v))
|
||||
val = str(dict(f.choices).get(v))
|
||||
elif isinstance(v, datetime):
|
||||
val = date_format(v, 'SHORT_DATETIME_FORMAT')
|
||||
elif isinstance(v, Decimal):
|
||||
@@ -197,6 +196,7 @@ class OrderFilterForm(FilterForm):
|
||||
label=_('Search for…'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search for…'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -267,10 +267,9 @@ class OrderFilterForm(FilterForm):
|
||||
Q(invoice_no__in=invoice_nos)
|
||||
| Q(full_invoice_no__iexact=u)
|
||||
).values_list('order_id', flat=True)
|
||||
matching_positions = OrderPosition.all.filter(
|
||||
matching_positions = OrderPosition.objects.filter(
|
||||
Q(
|
||||
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
|
||||
| Q(company__icontains=u)
|
||||
| Q(secret__istartswith=u)
|
||||
| Q(pseudonymization_id__istartswith=u)
|
||||
)
|
||||
@@ -850,18 +849,12 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
).distinct()
|
||||
for q in self.event.questions.all():
|
||||
if fdata.get(f'question_{q.pk}'):
|
||||
if q.type in (Question.TYPE_BOOLEAN, Question.TYPE_NUMBER):
|
||||
if q.type == Question.TYPE_BOOLEAN:
|
||||
answers = QuestionAnswer.objects.filter(
|
||||
question_id=q.pk,
|
||||
orderposition__order_id=OuterRef('pk'),
|
||||
answer__exact=fdata.get(f'question_{q.pk}')
|
||||
)
|
||||
elif q.type in (Question.TYPE_DATE, Question.TYPE_TIME, Question.TYPE_DATETIME):
|
||||
answers = QuestionAnswer.objects.filter(
|
||||
question_id=q.pk,
|
||||
orderposition__order_id=OuterRef('pk'),
|
||||
answer__exact=str(fdata.get(f'question_{q.pk}'))
|
||||
)
|
||||
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
answers = QuestionAnswer.objects.filter(
|
||||
question_id=q.pk,
|
||||
@@ -987,6 +980,7 @@ class OrderPaymentSearchFilterForm(forms.Form):
|
||||
label=_('Search for…'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search for…'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False,
|
||||
)
|
||||
@@ -1248,6 +1242,7 @@ class SubEventFilterForm(FilterForm):
|
||||
label=_('Event name'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Event name'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -1380,6 +1375,7 @@ class OrganizerFilterForm(FilterForm):
|
||||
label=_('Organizer name'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Organizer name'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -1437,6 +1433,7 @@ class GiftCardFilterForm(FilterForm):
|
||||
label=_('Search query'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search query'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -1488,6 +1485,7 @@ class CustomerFilterForm(FilterForm):
|
||||
label=_('Search query'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search query'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -1560,6 +1558,7 @@ class ReusableMediaFilterForm(FilterForm):
|
||||
label=_('Search query'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search query'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -1614,6 +1613,7 @@ class TeamFilterForm(FilterForm):
|
||||
label=_('Search query'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search query'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -1695,6 +1695,7 @@ class EventFilterForm(FilterForm):
|
||||
label=_('Event name'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Event name'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -1877,6 +1878,7 @@ class CheckinListAttendeeFilterForm(FilterForm):
|
||||
label=_('Search attendee…'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search attendee…'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -2025,6 +2027,7 @@ class UserFilterForm(FilterForm):
|
||||
label=_('Search query'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search query'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -2116,6 +2119,7 @@ class VoucherFilterForm(FilterForm):
|
||||
label=_('Search voucher'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search voucher'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
@@ -2593,6 +2597,7 @@ class DeviceFilterForm(FilterForm):
|
||||
label=_('Search query'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search query'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -25,8 +25,6 @@ import socket
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.forms import SecretKeySettingsField, SettingsForm
|
||||
@@ -56,15 +54,6 @@ class SMTPMailForm(SettingsForm):
|
||||
smtp_password = SecretKeySettingsField(
|
||||
label=_("Password"),
|
||||
required=False,
|
||||
validators=[RegexValidator(
|
||||
r"^[A-Za-z0-9!\"#$%&'()*+,./:;<=>?@\^_`{}|~-]+$",
|
||||
message=format_lazy(
|
||||
_("The password contains characters not supported by our email system. Please only use characters "
|
||||
"A-Z, a-z, 0-9, and common special characters ({characters})."),
|
||||
|
||||
characters=r'!"#$%%&\'()*+,-./:;<=>?@\^_`{}|~'
|
||||
)
|
||||
)]
|
||||
)
|
||||
smtp_use_tls = forms.BooleanField(
|
||||
label=_("Use STARTTLS"),
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import os.path
|
||||
from datetime import date, datetime, time
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -69,7 +68,6 @@ from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.control.forms import SplitDateTimeField
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.helpers.hierarkey import clean_filename
|
||||
from pretix.helpers.money import change_decimal_field
|
||||
|
||||
|
||||
@@ -725,9 +723,6 @@ class OrderMailForm(forms.Form):
|
||||
help_text=_("Will be ignored if tickets exceed a given size limit to ensure email deliverability."),
|
||||
required=False
|
||||
)
|
||||
attach_new_order = forms.BooleanField(
|
||||
required=False
|
||||
)
|
||||
attach_invoices = forms.ModelMultipleChoiceField(
|
||||
label=_("Attach invoices"),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
@@ -764,12 +759,6 @@ class OrderMailForm(forms.Form):
|
||||
self.fields['attach_invoices'].queryset = order.invoices.all()
|
||||
self._set_field_placeholders('message', ['event', 'order'])
|
||||
self._set_field_placeholders('subject', ['event', 'order'])
|
||||
if order.event.settings.mail_attachment_new_order:
|
||||
self.fields['attach_new_order'].label = _('Attach {file}').format(
|
||||
file=clean_filename(os.path.basename(order.event.settings.mail_attachment_new_order.name))
|
||||
)
|
||||
else:
|
||||
del self.fields['attach_new_order']
|
||||
|
||||
|
||||
class OrderPositionMailForm(OrderMailForm):
|
||||
|
||||
@@ -498,9 +498,6 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'privacy_url',
|
||||
'accessibility_url',
|
||||
'accessibility_title',
|
||||
'accessibility_text',
|
||||
'cookie_consent',
|
||||
'cookie_consent_dialog_title',
|
||||
'cookie_consent_dialog_text',
|
||||
@@ -525,8 +522,7 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
required=False,
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
'in the page header. If you use a white background, we show your logo with a size of up '
|
||||
'to 1140x120 pixels. Otherwise the maximum size is 1120x120 pixels. You '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
)
|
||||
|
||||
@@ -178,13 +178,6 @@ class SubEventBulkEditForm(I18nModelForm):
|
||||
widgets = {
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
if self.prefix + "name" in self.data.getlist('_bulk'):
|
||||
if not data.get("name"):
|
||||
self.add_error("name", _("This field is required."))
|
||||
return data
|
||||
|
||||
def save(self, commit=True):
|
||||
objs = list(self.queryset)
|
||||
fields = set()
|
||||
|
||||
@@ -43,6 +43,7 @@ from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
@@ -285,7 +286,7 @@ class OrderChangedSplit(OrderChangeLogEntryType):
|
||||
_('Position #{posid} ({old_item}, {old_price}) split into new order: {order}'),
|
||||
old_item=escape(old_item),
|
||||
posid=data.get('positionid', '?'),
|
||||
order=format_html('<a href="{}">{}</a>', url, data['new_order']),
|
||||
order=format_html(mark_safe('<a href="{}">{}</a>'), url, data['new_order']),
|
||||
old_price=money_filter(Decimal(data['old_price']), event.currency),
|
||||
)
|
||||
|
||||
@@ -302,7 +303,7 @@ class OrderChangedSplitFrom(OrderLogEntryType):
|
||||
})
|
||||
return format_html(
|
||||
_('This order has been created by splitting the order {order}'),
|
||||
order=format_html('<a href="{}">{}</a>', url, data['original_order']),
|
||||
order=format_html(mark_safe('<a href="{}">{}</a>'), url, data['original_order']),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ def get_event_navigation(request: HttpRequest):
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name in ('event.settings.payment', 'event.settings.payment.provider'),
|
||||
'active': url.url_name == 'event.settings.payment',
|
||||
},
|
||||
{
|
||||
'label': _('Plugins'),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<input class="form-control" name="token" placeholder="{% trans "Token" %}" autocomplete="one-time-code"
|
||||
type="text" required="required" autofocus="autofocus" id="webauthn-response">
|
||||
</div>
|
||||
<div class="alert alert-danger hidden" id="webauthn-error">
|
||||
<div class="sr-only alert alert-danger" id="webauthn-error">
|
||||
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
|
||||
</div>
|
||||
{% if jsondata %}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
{% load statici18n %}
|
||||
{% load eventsignal %}
|
||||
{% load eventurl %}
|
||||
{% load dialog %}
|
||||
{% load icon %}
|
||||
<!DOCTYPE html>
|
||||
<html{% if rtl %} dir="rtl" class="rtl"{% endif %}>
|
||||
<head>
|
||||
@@ -35,7 +33,6 @@
|
||||
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "cropper/cropper.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "rrule/rrule.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/gettextstub.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
|
||||
@@ -466,16 +463,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ajaxerr" class="modal-wrapper" hidden>
|
||||
<div id="ajaxerr">
|
||||
</div>
|
||||
{% dialog "loadingmodal" "" "" icon="cog rotating" %}
|
||||
<p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success">
|
||||
<div id="loadingmodal">
|
||||
<div class="modal-card">
|
||||
<div class="modal-card-icon">
|
||||
<i class="fa fa-cog big-rotating-icon"></i>
|
||||
</div>
|
||||
<div class="modal-card-content">
|
||||
<h3></h3>
|
||||
<p class="text"></p>
|
||||
<p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success">
|
||||
</div>
|
||||
</div>
|
||||
<div class="steps">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="steps">
|
||||
</div>
|
||||
{% enddialog %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{% endfor %}
|
||||
<p>
|
||||
{% blocktrans trimmed count count=cnt %}
|
||||
Are you sure you want to permanently delete the check-ins of <strong>one ticket</strong>?
|
||||
Are you sure you want to permanently delete the check-ins of <strong>one ticket</strong>.
|
||||
{% plural %}
|
||||
Are you sure you want to permanently delete the check-ins of <strong>{{ count }} tickets</strong>?
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<td>{{ e.item }}{% if e.variation %} – {{ e.variation }}{% endif %}</td>
|
||||
{% if request.event.has_subevents and not checkinlist.subevent %}
|
||||
<td>
|
||||
{{ e.subevent.name }} – {{ e.subevent.get_date_range_display_with_times }}
|
||||
{{ e.subevent.name }} – {{ e.subevent.get_date_range_display }} {{ e.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if seats %}
|
||||
|
||||
@@ -127,7 +127,8 @@
|
||||
{% if request.event.has_subevents %}
|
||||
{% if cl.subevent %}
|
||||
<td>
|
||||
{{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display_with_times }}
|
||||
{{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }}
|
||||
{{ cl.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<p class="plugin-description">{{ plugin.description|safe }}</p>
|
||||
<p>{{ plugin.description|safe }}</p>
|
||||
{% if plugin.restricted and plugin.module not in request.event.settings.allowed_restricted_plugins %}
|
||||
<p class="text-muted">
|
||||
<span class="fa fa-info-circle" aria-hidden="true"></span>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="delete" value="yes" />
|
||||
<strong>{% trans "Permanently delete all orders created in test mode" %}</strong>
|
||||
<b>{% trans "Permanently delete all orders created in test mode" %}</b>
|
||||
</label>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
|
||||
@@ -48,19 +48,19 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<br>
|
||||
<td colspan="3">
|
||||
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
|
||||
<a href="{{ plugin_settings_url }}#tab-0-1-open" class="btn btn-default">
|
||||
<i class="fa fa-plus"></i> {% trans "Enable additional payment plugins" %}
|
||||
</a>
|
||||
{% blocktrans trimmed with plugin_settings_href='href="'|add:plugin_settings_url|add:'"'|safe %}
|
||||
There are no payment providers available. Please go to the
|
||||
<a {{ plugin_settings_href }}>plugin settings</a> and activate one or more payment plugins.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Deadlines" %}</legend>
|
||||
|
||||
@@ -10,40 +10,20 @@
|
||||
software functionality, connect your event to third-party services, or apply other forms of customizations.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-lg-10">
|
||||
<p><input type="search" id="plugin_search_input" class="form-control" placeholder="{% trans "Search" %}"></p>
|
||||
</div>
|
||||
<div class="col-lg-2 text-right">
|
||||
<p class="btn-group btn-group-flex" data-toggle="buttons">
|
||||
<label class="btn btn-primary-if-active active"><input type="radio" name="plugin_state_filter" value="all" checked> {% trans "All" %}</label>
|
||||
<label class="btn btn-primary-if-active"><input type="radio" name="plugin_state_filter" value="active"> {% trans "Active" %}</label>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form action="" method="post" class="form-horizontal form-plugins">
|
||||
{% csrf_token %}
|
||||
<div id="plugin_search_results" class="panel panel-default collapse">
|
||||
<div class="panel-heading">
|
||||
<button type="button" class="close" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
{% trans "Search results" %}
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="plugin-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="plugin_tabs"><div class="tabbed-form">
|
||||
{% endif %}
|
||||
<div class="tabbed-form">
|
||||
{% for cat, catlabel, plist, has_pictures in plugins %}
|
||||
<fieldset data-plugin-category="{{ cat }}" data-plugin-category-label="{{ catlabel }}">
|
||||
<fieldset>
|
||||
<legend>{{ catlabel }}</legend>
|
||||
<div class="plugin-list">
|
||||
{% for plugin, is_active, settings_links, navigation_links in plist %}
|
||||
<div class="plugin-container {% if plugin.featured %}featured-plugin{% endif %}" id="plugin_{{ plugin.module }}" data-plugin-module="{{ plugin.module }}" data-plugin-name="{{ plugin.name }}">
|
||||
{% for plugin in plist %}
|
||||
<div class="plugin-container {% if plugin.featured %}featured-plugin{% endif %}" id="plugin_{{ plugin.module }}">
|
||||
{% if plugin.featured %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
@@ -69,8 +49,8 @@
|
||||
{% if show_meta %}
|
||||
<span class="text-muted text-sm">{{ plugin.version }}</span>
|
||||
{% endif %}
|
||||
{% if is_active %}
|
||||
<span class="label label-success" data-is-active>
|
||||
{% if plugin.module in plugins_active %}
|
||||
<span class="label label-success">
|
||||
<span class="fa fa-check" aria-hidden="true"></span>
|
||||
{% trans "Active" %}
|
||||
</span>
|
||||
@@ -86,32 +66,8 @@
|
||||
<div class="plugin-action">
|
||||
<span class="text-muted">{% trans "Not available" %}</span>
|
||||
</div>
|
||||
{% elif is_active %}
|
||||
{% elif plugin.module in plugins_active %}
|
||||
<div class="plugin-action flip">
|
||||
{% if navigation_links %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle{% if plugin.featured %} btn-lg{% endif %}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans "Open plugin settings" %}">
|
||||
<span class="fa fa-compass"></span> {% trans "Go to" %} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for link in navigation_links %}
|
||||
<li><a href="{{ link.0 }}">{{ link.1 }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if settings_links %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle{% if plugin.featured %} btn-lg{% endif %}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans "Open plugin settings" %}">
|
||||
<span class="fa fa-cog"></span> {% trans "Settings" %} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for link in settings_links %}
|
||||
<li><a href="{{ link.0 }}">{{ link.1 }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button class="btn btn-default{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||
value="disable">{% trans "Disable" %}</button>
|
||||
</div>
|
||||
@@ -130,7 +86,6 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
</div></div>
|
||||
</div>
|
||||
</form>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/plugins.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
{% bootstrap_field sform.locale layout="control" %}
|
||||
{% bootstrap_field sform.timezone layout="control" %}
|
||||
{% bootstrap_field sform.region layout="control" %}
|
||||
{% bootstrap_field form.is_remote layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Customer and attendee data" %}</legend>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
section of your website:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<pre><link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" version=widget_version_default %}" crossorigin>
|
||||
<script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language version=widget_version_default %}" async crossorigin></script></pre>
|
||||
<pre><link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}" crossorigin>
|
||||
<script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async crossorigin></script></pre>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Then, copy the following code to the place of your website where you want the widget to show up:
|
||||
@@ -32,7 +32,7 @@
|
||||
{% abseventurl request.event "presale:event.index" as indexurl %}
|
||||
{% endif %}
|
||||
{% if form.cleaned_data.compatibility_mode %}
|
||||
<pre><div class="pretix-widget-compat" event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}></div>
|
||||
<pre><div class="pretix-widget-compat" event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %} single-item-select="button"></div>
|
||||
<noscript>
|
||||
<div class="pretix-widget">
|
||||
<div class="pretix-widget-info-message">
|
||||
@@ -45,7 +45,7 @@
|
||||
</noscript>
|
||||
</pre>
|
||||
{% else %}
|
||||
<pre><pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}></pretix-widget>
|
||||
<pre><pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %} single-item-select="button"></pretix-widget>
|
||||
<noscript>
|
||||
<div class="pretix-widget">
|
||||
<div class="pretix-widget-info-message">
|
||||
|
||||
@@ -110,26 +110,23 @@
|
||||
{% if not hide_orga %}<td>{{ e.organizer }}</td>{% endif %}
|
||||
<td class="event-date-col">
|
||||
{% if e.has_subevents %}
|
||||
<span class="fa fa-fw- fa-calendar"></span>
|
||||
{% trans "Event series" %}
|
||||
<br>
|
||||
<span class="text-muted">
|
||||
{% if e.min_from %}
|
||||
{{ e.min_from|date:"SHORT_DATETIME_FORMAT" }} –<br>
|
||||
{{ e.max_fromto|default_if_none:e.max_to|default_if_none:e.max_from|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
{% trans "No dates" context "subevent" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{{ e.min_from|default_if_none:""|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
{{ e.get_short_date_from_display }}
|
||||
{% if e.settings.show_date_to and e.date_to %}
|
||||
–<br>
|
||||
{% endif %}
|
||||
{% if e.has_subevents %}
|
||||
<span class="label label-default">{% trans "Series" %}</span>
|
||||
{% endif %}
|
||||
{% if e.settings.show_date_to and e.date_to %}
|
||||
–<br>
|
||||
{% if e.has_subevents %}
|
||||
{{ e.max_fromto|default_if_none:e.max_from|default_if_none:e.max_to|default_if_none:""|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
{{ e.get_short_date_to_display }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if e.settings.timezone != request.timezone %}
|
||||
<span class="fa fa-globe text-muted" data-toggle="tooltip" title="{{ e.tzname }}"></span>
|
||||
<span class="fa fa-globe text-muted" data-toggle="tooltip" title="{{ e.timezone }}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
|
||||
{{ request.event.settings.payment_giftcard_public_description|rich_text }}
|
||||
|
||||
{% bootstrap_form form layout='checkout' %}
|
||||
<input name="giftcard" class="form-control" placeholder="{% trans "Gift card code" %}">
|
||||
|
||||
@@ -40,10 +40,6 @@
|
||||
<tr data-dnd-id="{{ c.id }}">
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
#{{ c.pk }}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ c.get_category_type_display }}
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
{% bootstrap_field form.condition_ignore_voucher_discounted layout="control" %}
|
||||
{% if form.subevent_mode %}
|
||||
{% bootstrap_field form.subevent_mode layout="control" %}
|
||||
{% bootstrap_field form.subevent_date_from layout="control" %}
|
||||
{% bootstrap_field form.subevent_date_until layout="control" %}
|
||||
{% endif %}
|
||||
<div class="form-group form-alternatives">
|
||||
<label class="col-md-3 control-label">
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="text-right flip">{% trans "Default price" %}</th>
|
||||
<th class="action-col-2"><span class="sr-only">Edit</span></th>
|
||||
</tr>
|
||||
@@ -112,14 +111,6 @@
|
||||
<span class="fa fa-bars fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if i.requires_seat %}
|
||||
<span data-toggle="tooltip"
|
||||
title="{% if request.event.has_subevents %}{% trans "Product assigned to seating plan for one or more dates" context "subevent" %}{% else %}{% trans "Product assigned to seating plan" %}{% endif %}">
|
||||
{% include "icons/seat.svg" with cls="svg-icon text-muted" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if i.category.is_addon %}
|
||||
<span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
</td>
|
||||
{% if request.event.has_subevents %}
|
||||
<td>
|
||||
{{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }}
|
||||
{{ q.subevent.name }} – {{ q.subevent.get_date_range_display }} {{ q.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
{% load eventsignal %}
|
||||
{% load l10n %}
|
||||
{% load phone_format %}
|
||||
{% load getitem %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Order details: {{ code }}
|
||||
@@ -398,7 +397,7 @@
|
||||
{% elif c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-fw {% if c.list.consider_tickets_used %}text-success fa-check{% else %}text-muted fa-check-circle-o{% endif %}" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
<span class="fa fa-fw fa-check {% if c.list.consider_tickets_used %}text-success{% else %}text-muted{% endif %}" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -413,7 +412,10 @@
|
||||
{% endif %}
|
||||
{% if line.seat %}
|
||||
<br />
|
||||
{% include "icons/seat.svg" with cls="svg-icon" %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 4.7624999 3.7041668" class="svg-icon">
|
||||
<path
|
||||
d="m 1.9592032,1.8522629e-4 c -0.21468,0 -0.38861,0.17394000371 -0.38861,0.38861000371 0,0.21466 0.17393,0.38861 0.38861,0.38861 0.21468,0 0.3886001,-0.17395 0.3886001,-0.38861 0,-0.21467 -0.1739201,-0.38861000371 -0.3886001,-0.38861000371 z m 0.1049,0.84543000371 c -0.20823,-0.0326 -0.44367,0.12499 -0.39998,0.40462997 l 0.20361,1.01854 c 0.0306,0.15316 0.15301,0.28732 0.3483,0.28732 h 0.8376701 v 0.92708 c 0,0.29313 0.41187,0.29447 0.41187,0.005 v -1.19115 c 0,-0.14168 -0.0995,-0.29507 -0.29094,-0.29507 l -0.65578,-10e-4 -0.1757,-0.87644 C 2.3042533,0.95300523 2.1890432,0.86500523 2.0641032,0.84547523 Z m -0.58549,0.44906997 c -0.0946,-0.0134 -0.20202,0.0625 -0.17829,0.19172 l 0.18759,0.91054 c 0.0763,0.33956 0.36802,0.55914 0.66042,0.55914 h 0.6015201 c 0.21356,0 0.21448,-0.32143 -0.003,-0.32143 H 2.1954632 c -0.19911,0 -0.36364,-0.11898 -0.41341,-0.34107 l -0.17777,-0.87126 c -0.0165,-0.0794 -0.0688,-0.11963 -0.12557,-0.12764 z"/>
|
||||
</svg>
|
||||
{{ line.seat }}
|
||||
{% endif %}
|
||||
{% if line.voucher %}
|
||||
@@ -426,7 +428,11 @@
|
||||
{% endif %}
|
||||
{% if line.subevent %}
|
||||
<br/>
|
||||
<span class="fa fa-calendar fa-fw"></span> {{ line.subevent.name }} · {{ line.subevent.get_date_range_display_with_times }}
|
||||
<span class="fa fa-calendar fa-fw"></span> {{ line.subevent.name }} · {{ line.subevent.get_date_range_display }}
|
||||
{% if event.settings.show_times %}
|
||||
<span class="fa fa-clock-o"></span>
|
||||
{{ line.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if line.used_membership %}
|
||||
<br /><span class="fa fa-id-card fa-fw" aria-hidden="true"></span>
|
||||
@@ -554,8 +560,8 @@
|
||||
{% if line.street or line.zipcode or line.city or line.country %}
|
||||
{{ line.street|default_if_none:""|linebreaksbr }}<br>
|
||||
{{ line.zipcode|default_if_none:"" }} {{ line.city|default_if_none:"" }}<br>
|
||||
{% if line.state %}{{ line.state_for_address }}<br>{% endif %}
|
||||
{{ line.country.name|default_if_none:"" }}
|
||||
{% if line.state %}<br>{{ line.state }}{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
@@ -953,7 +959,7 @@
|
||||
<dt>{% trans "Country" %}</dt>
|
||||
<dd>{{ order.invoice_address.country.name|default:order.invoice_address.country_old }}</dd>
|
||||
{% if order.invoice_address.state %}
|
||||
<dt>{% trans "State" context "address" as state_label %}{{ COUNTRY_STATE_LABEL|getitem:order.invoice_address.country.code|default:state_label }}</dt>
|
||||
<dt>{% trans "State" context "address" %}</dt>
|
||||
<dd>{{ order.invoice_address.state_name }}</dd>
|
||||
{% endif %}
|
||||
{% if request.event.settings.invoice_address_vatid %}
|
||||
|
||||
@@ -51,44 +51,6 @@
|
||||
{{ log.parsed_data.subject }}</strong>
|
||||
</p>
|
||||
<pre>{{ log.parsed_data.message }}</pre>
|
||||
<ul class="list-unstyled">
|
||||
{% comment %}
|
||||
{# Unfortunately, we do not have reliable info whether tickets were attached. #}
|
||||
{% if log.parsed_data.attach_tickets %}
|
||||
<li><span class="fa fa-files-o fa-fw"></span> {% trans "Tickets" %}</li>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
{% if log.parsed_data.attach_ical %}
|
||||
<li><span class="fa fa-calendar-o fa-fw"></span> {% trans "Calendar invite" %}</li>
|
||||
{% endif %}
|
||||
{% if log.parsed_data.invoices %}
|
||||
{% for i in log.parsed_invoices %}
|
||||
<li>
|
||||
<span class="fa fa-file-o fa-fw"></span>
|
||||
<a href="{% url "control:event.invoice.download" invoice=i.pk event=request.event.slug organizer=request.event.organizer.slug %}" target="_blank">
|
||||
{% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %}
|
||||
{{ i.number }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if log.parsed_data.attach_other_files %}
|
||||
{% for f in log.parsed_other_files %}
|
||||
<li>
|
||||
<span class="fa fa-file-o fa-fw"></span>
|
||||
{{ f }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if log.parsed_data.attach_cached_files %}
|
||||
{% for f in log.parsed_data.attach_cached_files %}
|
||||
<li>
|
||||
<span class="fa fa-file-o fa-fw"></span>
|
||||
{{ f }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -19,9 +19,6 @@
|
||||
{% bootstrap_field form.subject layout='horizontal' %}
|
||||
{% bootstrap_field form.message layout='horizontal' %}
|
||||
{% bootstrap_field form.attach_tickets layout='horizontal' %}
|
||||
{% if form.attach_new_order %}
|
||||
{% bootstrap_field form.attach_new_order layout='horizontal' %}
|
||||
{% endif %}
|
||||
{% if form.attach_invoices %}
|
||||
{% bootstrap_field form.attach_invoices layout='horizontal' %}
|
||||
{% endif %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user